diff --git a/composer.json b/composer.json index 9ce7358439..ba39243944 100644 --- a/composer.json +++ b/composer.json @@ -50,8 +50,6 @@ "scripts": { "pre-autoload-dump": "Drupal\\Core\\Composer\\Composer::preAutoloadDump", "post-autoload-dump": "Drupal\\Core\\Composer\\Composer::ensureHtaccess", - "post-package-install": "Drupal\\Core\\Composer\\Composer::vendorTestCodeCleanup", - "post-package-update": "Drupal\\Core\\Composer\\Composer::vendorTestCodeCleanup", "drupal-phpunit-upgrade-check": "Drupal\\Core\\Composer\\Composer::upgradePHPUnit", "drupal-phpunit-upgrade": "@composer update phpunit/phpunit phpspec/prophecy symfony/yaml --with-dependencies --no-progress", "phpcs": "phpcs --standard=core/phpcs.xml.dist --runtime-set installed_paths $($COMPOSER_BINARY config vendor-dir)/drupal/coder/coder_sniffer --", @@ -62,5 +60,8 @@ "type": "composer", "url": "https://packages.drupal.org/8" } - ] + ], + "require-dev": { + "composer/composer": "^1.8" + } } diff --git a/composer.lock b/composer.lock index 41e5a866f1..7d5e540e67 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": "d764df12b2cff2be49d45e95ecbeb5d7", + "content-hash": "d893afc1da2c9fa30925841809a51e05", "packages": [ { "name": "asm89/stack-cors", @@ -3026,6 +3026,247 @@ ], "time": "2018-01-07T19:17:08+00:00" }, + { + "name": "composer/ca-bundle", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/composer/ca-bundle.git", + "reference": "8afa52cd417f4ec417b4bfe86b68106538a87660" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/8afa52cd417f4ec417b4bfe86b68106538a87660", + "reference": "8afa52cd417f4ec417b4bfe86b68106538a87660", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "ext-pcre": "*", + "php": "^5.3.2 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5", + "psr/log": "^1.0", + "symfony/process": "^2.5 || ^3.0 || ^4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\CaBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.", + "keywords": [ + "cabundle", + "cacert", + "certificate", + "ssl", + "tls" + ], + "time": "2018-10-18T06:09:13+00:00" + }, + { + "name": "composer/composer", + "version": "1.8.0", + "source": { + "type": "git", + "url": "https://github.com/composer/composer.git", + "reference": "d8aef3af866b28786ce9b8647e52c42496436669" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/composer/zipball/d8aef3af866b28786ce9b8647e52c42496436669", + "reference": "d8aef3af866b28786ce9b8647e52c42496436669", + "shasum": "" + }, + "require": { + "composer/ca-bundle": "^1.0", + "composer/semver": "^1.0", + "composer/spdx-licenses": "^1.2", + "composer/xdebug-handler": "^1.1", + "justinrainbow/json-schema": "^3.0 || ^4.0 || ^5.0", + "php": "^5.3.2 || ^7.0", + "psr/log": "^1.0", + "seld/jsonlint": "^1.4", + "seld/phar-utils": "^1.0", + "symfony/console": "^2.7 || ^3.0 || ^4.0", + "symfony/filesystem": "^2.7 || ^3.0 || ^4.0", + "symfony/finder": "^2.7 || ^3.0 || ^4.0", + "symfony/process": "^2.7 || ^3.0 || ^4.0" + }, + "conflict": { + "symfony/console": "2.8.38" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7", + "phpunit/phpunit-mock-objects": "^2.3 || ^3.0" + }, + "suggest": { + "ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages", + "ext-zip": "Enabling the zip extension allows you to unzip archives", + "ext-zlib": "Allow gzip compression of HTTP requests" + }, + "bin": [ + "bin/composer" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.8-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\": "src/Composer" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Composer helps you declare, manage and install dependencies of PHP projects, ensuring you have the right stack everywhere.", + "homepage": "https://getcomposer.org/", + "keywords": [ + "autoload", + "dependency", + "package" + ], + "time": "2018-12-03T09:31:16+00:00" + }, + { + "name": "composer/spdx-licenses", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/composer/spdx-licenses.git", + "reference": "7a9556b22bd9d4df7cad89876b00af58ef20d3a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/7a9556b22bd9d4df7cad89876b00af58ef20d3a2", + "reference": "7a9556b22bd9d4df7cad89876b00af58ef20d3a2", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5", + "phpunit/phpunit-mock-objects": "2.3.0 || ^3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Spdx\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "SPDX licenses list and validation library.", + "keywords": [ + "license", + "spdx", + "validator" + ], + "time": "2018-11-01T09:45:54+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "dc523135366eb68f22268d069ea7749486458562" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/dc523135366eb68f22268d069ea7749486458562", + "reference": "dc523135366eb68f22268d069ea7749486458562", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0", + "psr/log": "^1.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "time": "2018-11-29T10:59:02+00:00" + }, { "name": "doctrine/instantiator", "version": "1.0.5", @@ -3088,6 +3329,12 @@ "url": "https://git.drupal.org/project/coder.git", "reference": "984c54a7b1e8f27ff1c32348df69712afd86b17f" }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/klausi/coder/zipball/984c54a7b1e8f27ff1c32348df69712afd86b17f", + "reference": "984c54a7b1e8f27ff1c32348df69712afd86b17f", + "shasum": "" + }, "require": { "ext-mbstring": "*", "php": ">=5.4.0", @@ -3382,6 +3629,72 @@ ], "time": "2016-10-04T09:27:04+00:00" }, + { + "name": "justinrainbow/json-schema", + "version": "5.2.7", + "source": { + "type": "git", + "url": "https://github.com/justinrainbow/json-schema.git", + "reference": "8560d4314577199ba51bf2032f02cd1315587c23" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/8560d4314577199ba51bf2032f02cd1315587c23", + "reference": "8560d4314577199ba51bf2032f02cd1315587c23", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.1", + "json-schema/json-schema-test-suite": "1.2.0", + "phpunit/phpunit": "^4.8.35" + }, + "bin": [ + "bin/validate-json" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "JsonSchema\\": "src/JsonSchema/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bruno Prieto Reis", + "email": "bruno.p.reis@gmail.com" + }, + { + "name": "Justin Rainbow", + "email": "justin.rainbow@gmail.com" + }, + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + }, + { + "name": "Robert Schönthal", + "email": "seroscho@googlemail.com" + } + ], + "description": "A library to validate a json schema.", + "homepage": "https://github.com/justinrainbow/json-schema", + "keywords": [ + "json", + "schema" + ], + "time": "2018-02-14T22:26:30+00:00" + }, { "name": "mikey179/vfsStream", "version": "v1.6.5", @@ -4288,6 +4601,99 @@ "homepage": "https://github.com/sebastianbergmann/version", "time": "2015-06-21T13:59:46+00:00" }, + { + "name": "seld/jsonlint", + "version": "1.7.1", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/jsonlint.git", + "reference": "d15f59a67ff805a44c50ea0516d2341740f81a38" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/d15f59a67ff805a44c50ea0516d2341740f81a38", + "reference": "d15f59a67ff805a44c50ea0516d2341740f81a38", + "shasum": "" + }, + "require": { + "php": "^5.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + }, + "bin": [ + "bin/jsonlint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Seld\\JsonLint\\": "src/Seld/JsonLint/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "JSON Linter", + "keywords": [ + "json", + "linter", + "parser", + "validator" + ], + "time": "2018-01-24T12:46:19+00:00" + }, + { + "name": "seld/phar-utils", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/phar-utils.git", + "reference": "7009b5139491975ef6486545a39f3e6dad5ac30a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/7009b5139491975ef6486545a39f3e6dad5ac30a", + "reference": "7009b5139491975ef6486545a39f3e6dad5ac30a", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Seld\\PharUtils\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be" + } + ], + "description": "PHAR file format utilities, for when PHP phars you up", + "keywords": [ + "phra" + ], + "time": "2015-10-13T18:44:15+00:00" + }, { "name": "squizlabs/php_codesniffer", "version": "2.8.1", @@ -4533,6 +4939,105 @@ "homepage": "https://symfony.com", "time": "2018-07-26T10:03:52+00:00" }, + { + "name": "symfony/filesystem", + "version": "v4.2.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "2f4c8b999b3b7cadb2a69390b01af70886753710" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/2f4c8b999b3b7cadb2a69390b01af70886753710", + "reference": "2f4c8b999b3b7cadb2a69390b01af70886753710", + "shasum": "" + }, + "require": { + "php": "^7.1.3", + "symfony/polyfill-ctype": "~1.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Filesystem Component", + "homepage": "https://symfony.com", + "time": "2018-11-11T19:52:12+00:00" + }, + { + "name": "symfony/finder", + "version": "v4.2.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "e53d477d7b5c4982d0e1bfd2298dbee63d01441d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/e53d477d7b5c4982d0e1bfd2298dbee63d01441d", + "reference": "e53d477d7b5c4982d0e1bfd2298dbee63d01441d", + "shasum": "" + }, + "require": { + "php": "^7.1.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Finder Component", + "homepage": "https://symfony.com", + "time": "2018-11-11T19:52:12+00:00" + }, { "name": "symfony/phpunit-bridge", "version": "v3.4.15", diff --git a/core/composer.json b/core/composer.json index 8dfd8a10f2..a59df438a6 100644 --- a/core/composer.json +++ b/core/composer.json @@ -194,6 +194,7 @@ "core/lib/Drupal/Component/Plugin/composer.json", "core/lib/Drupal/Component/ProxyBuilder/composer.json", "core/lib/Drupal/Component/Render/composer.json", + "core/lib/Drupal/Component/Scaffold/composer.json", "core/lib/Drupal/Component/Serialization/composer.json", "core/lib/Drupal/Component/Transliteration/composer.json", "core/lib/Drupal/Component/Utility/composer.json", diff --git a/core/drupalci.yml b/core/drupalci.yml index 2085b9737b..c01c31ffe2 100644 --- a/core/drupalci.yml +++ b/core/drupalci.yml @@ -3,17 +3,6 @@ # https://www.drupal.org/drupalorg/docs/drupal-ci/customizing-drupalci-testing build: assessment: - validate_codebase: - phplint: - csslint: - halt-on-fail: false - eslint: - # A test must pass eslinting standards check in order to continue processing. - halt-on-fail: false - phpcs: - # phpcs will use core's specified version of Coder. - sniff-all-files: false - halt-on-fail: false testing: # run_tests task is executed several times in order of performance speeds. # halt-on-fail can be set on the run_tests tasks in order to fail fast. @@ -24,27 +13,3 @@ build: testgroups: '--all' suppress-deprecations: false halt-on-fail: false - run_tests.kernel: - types: 'PHPUnit-Kernel' - testgroups: '--all' - suppress-deprecations: false - halt-on-fail: false - run_tests.simpletest: - types: 'Simpletest' - testgroups: '--all' - suppress-deprecations: false - halt-on-fail: false - run_tests.functional: - types: 'PHPUnit-Functional' - testgroups: '--all' - suppress-deprecations: false - halt-on-fail: false - run_tests.javascript: - concurrency: 15 - types: 'PHPUnit-FunctionalJavascript' - testgroups: '--all' - suppress-deprecations: false - halt-on-fail: false - # Run nightwatch testing. - # @see https://www.drupal.org/project/drupal/issues/2869825 - nightwatchjs: diff --git a/core/lib/Drupal/Component/Scaffold/CommandProvider.php b/core/lib/Drupal/Component/Scaffold/CommandProvider.php new file mode 100644 index 0000000000..22350a9b97 --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/CommandProvider.php @@ -0,0 +1,21 @@ +setName('drupal:scaffold') + ->setDescription('Update the Drupal scaffold files.'); + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) { + $handler = new Handler($this->getComposer(), $this->getIO()); + $handler->downloadScaffold(); + // Generate the autoload.php file after generating the scaffold files. + $handler->generateAutoload(); + } + +} diff --git a/core/lib/Drupal/Component/Scaffold/FileFetcher.php b/core/lib/Drupal/Component/Scaffold/FileFetcher.php new file mode 100644 index 0000000000..c5907427ff --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/FileFetcher.php @@ -0,0 +1,136 @@ +remoteFilesystem = $remoteFilesystem; + $this->io = $io; + $this->source = $source; + // TODO: this should be injectable. + $this->fs = new Filesystem(); + $this->progress = $progress; + } + + /** + * Downloads all required files and writes it to the file system. + * + * @param string $version + * The version of the scaffold file to be retrieved. + * @param string $destination + * The location on the filesystem where we will place the file. + * @param bool $override + * Whether the file should be overridden or left in place. + */ + public function fetch($version, $destination, $override) { + foreach ($this->filenames as $sourceFilename => $filename) { + $target = "$destination/$filename"; + if ($override || !file_exists($target)) { + $url = $this->getUri($sourceFilename, $version); + $this->fs->ensureDirectoryExists($destination . '/' . dirname($filename)); + if ($this->progress) { + $this->io->writeError(" - $filename ($url): ", FALSE); + $this->remoteFilesystem->copy($url, $url, $target, $this->progress); + // Used to put a new line because the remote file system does not put + // one. + $this->io->writeError(''); + } + else { + $this->remoteFilesystem->copy($url, $url, $target, $this->progress); + } + } + } + } + + /** + * Set filenames. + * + * @param array $filenames + * An array of filenames to retrieve from the drupal git repository. + */ + public function setFilenames(array $filenames) { + $this->filenames = $filenames; + } + + /** + * Replace filename and version in the source pattern with their values. + * + * @param string $filename + * The filename to retrieve. + * @param string $version + * The version of the git drupal repository. + * + * @return string + * A uri of the filename/version combination. + */ + protected function getUri($filename, $version) { + $map = [ + '{path}' => $filename, + '{version}' => $version, + ]; + return str_replace(array_keys($map), array_values($map), $this->source); + } + +} diff --git a/core/lib/Drupal/Component/Scaffold/Handler.php b/core/lib/Drupal/Component/Scaffold/Handler.php new file mode 100644 index 0000000000..5aeaac64fa --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/Handler.php @@ -0,0 +1,649 @@ + ['tests', 'driver-testsuite'], + 'behat/mink-browserkit-driver' => ['tests'], + 'behat/mink-goutte-driver' => ['tests'], + 'drupal/coder' => ['coder_sniffer/Drupal/Test', 'coder_sniffer/DrupalPractice/Test'], + 'doctrine/cache' => ['tests'], + 'doctrine/collections' => ['tests'], + 'doctrine/common' => ['tests'], + 'doctrine/inflector' => ['tests'], + 'doctrine/instantiator' => ['tests'], + 'egulias/email-validator' => ['documentation', 'tests'], + 'fabpot/goutte' => ['Goutte/Tests'], + 'guzzlehttp/promises' => ['tests'], + 'guzzlehttp/psr7' => ['tests'], + 'jcalderonzumba/gastonjs' => ['docs', 'examples', 'tests'], + 'jcalderonzumba/mink-phantomjs-driver' => ['tests'], + 'masterminds/html5' => ['test'], + 'mikey179/vfsStream' => ['src/test'], + 'paragonie/random_compat' => ['tests'], + 'phpdocumentor/reflection-docblock' => ['tests'], + 'phpunit/php-code-coverage' => ['tests'], + 'phpunit/php-timer' => ['tests'], + 'phpunit/php-token-stream' => ['tests'], + 'phpunit/phpunit' => ['tests'], + 'phpunit/php-mock-objects' => ['tests'], + 'sebastian/comparator' => ['tests'], + 'sebastian/diff' => ['tests'], + 'sebastian/environment' => ['tests'], + 'sebastian/exporter' => ['tests'], + 'sebastian/global-state' => ['tests'], + 'sebastian/recursion-context' => ['tests'], + 'stack/builder' => ['tests'], + 'symfony/browser-kit' => ['Tests'], + 'symfony/class-loader' => ['Tests'], + 'symfony/console' => ['Tests'], + 'symfony/css-selector' => ['Tests'], + 'symfony/debug' => ['Tests'], + 'symfony/dependency-injection' => ['Tests'], + 'symfony/dom-crawler' => ['Tests'], + // @see \Drupal\Tests\Component\EventDispatcher\ContainerAwareEventDispatcherTest + // 'symfony/event-dispatcher' => ['Tests'], + 'symfony/http-foundation' => ['Tests'], + 'symfony/http-kernel' => ['Tests'], + 'symfony/process' => ['Tests'], + 'symfony/psr-http-message-bridge' => ['Tests'], + 'symfony/routing' => ['Tests'], + 'symfony/serializer' => ['Tests'], + 'symfony/translation' => ['Tests'], + 'symfony/validator' => ['Tests', 'Resources'], + 'symfony/yaml' => ['Tests'], + 'symfony-cmf/routing' => ['Test', 'Tests'], + 'twig/twig' => ['doc', 'ext', 'test'], + ]; + + /** + * Handler constructor. + * + * @param \Composer\Composer $composer + * The primary composer application object. + * @param \Composer\IO\IOInterface $io + * The composer IO object for printing messages to the console. + */ + public function __construct(Composer $composer, IOInterface $io) { + $this->composer = $composer; + $this->io = $io; + $this->progress = TRUE; + + // Pre-load all of the plugins classes so that when we update the + // drupal/drupal-scaffold plugin itself, we do not run into issues with + // api mismatches between versions. + $this->manualLoad(); + } + + /** + * Pre-load all of our sources on initial load. + * + * This ensures that we will not get a more recent version of one of + * our classes e.g. after a 'composer update' operation. + */ + protected function manualLoad() { + $src_dir = __DIR__; + + $classes = [ + 'CommandProvider', + 'DrupalScaffoldCommand', + 'FileFetcher', + 'PrestissimoFileFetcher', + ]; + + foreach ($classes as $src) { + if (!class_exists('\\Drupal\\Component\\Scaffold\\' . $src)) { + include "{$src_dir}/{$src}.php"; + } + } + } + + /** + * Determines if drupal/core is being changed by the install or update. + * + * @param \Composer\DependencyResolver\Operation\OperationInterface $operation + * Determines what type of Composer package operation is occurring. + * + * @return mixed + * Returns 'drupal/core' if core is being updated, otherwise returns null. + */ + protected function getCorePackage(OperationInterface $operation) { + if ($operation instanceof InstallOperation) { + $package = $operation->getPackage(); + } + elseif ($operation instanceof UpdateOperation) { + $package = $operation->getTargetPackage(); + } + if (isset($package) && $package instanceof PackageInterface && $package->getName() == 'drupal/core') { + return $package; + } + return NULL; + } + + /** + * Get the command options. + * + * @param \Composer\Plugin\CommandEvent $event + * The Composer event that called this listener. + */ + public function onCmdBeginsEvent(CommandEvent $event) { + if ($event->getInput()->hasOption('no-progress')) { + $this->progress = !($event->getInput()->getOption('no-progress')); + } + else { + $this->progress = TRUE; + } + } + + /** + * Event handler that fires after an install or update command. + * + * Files to be processed are marked, and potentially problematic test files + * are removed from vendor packages. + * + * @param \Composer\Installer\PackageEvent $event + * The install or update event object from Composer. + */ + public function onPostPackageEvent(PackageEvent $event) { + $package = $this->getCorePackage($event->getOperation()); + if ($package) { + // By explicitly setting the core package, the onPostCmdEvent() will + // process the scaffolding automatically. + $this->drupalCorePackage = $package; + } + + $vendor_dir = $event->getComposer()->getConfig()->get('vendor-dir'); + $io = $event->getIO(); + + // Get target package if we're updating, package otherwise. + $operation = $event->getOperation(); + if ($operation->getJobType() == 'update') { + $package = $operation->getTargetPackage(); + } + else { + $package = $operation->getPackage(); + } + + // Get the case-adjusted package name, which is also the path to the package + // within the vendor directory. + $package_name = static::findPackageKey($package->getName()); + if ($package_name) { + $cleanup_paths = static::$packageToCleanup[$package_name]; + static::doTestCodeCleanup($vendor_dir, $package_name, $cleanup_paths, $io); + } + } + + /** + * Post install command event to execute the scaffolding. + * + * @param \Composer\Script\Event $event + * The Composer event. + */ + public function onPostCmdEvent(Event $event) { + // Only install the scaffolding if drupal/core was installed, + // AND there are no scaffolding files present. + if (isset($this->drupalCorePackage)) { + $this->downloadScaffold(); + // Generate the autoload.php file after generating the scaffold files. + $this->generateAutoload(); + } + } + + /** + * Downloads drupal scaffold files for the current process. + */ + public function downloadScaffold() { + $drupal_core_package = $this->getDrupalCorePackage(); + $webroot = realpath($this->getWebRoot()); + + // Collect options, excludes and settings files. + $options = $this->getOptions(); + $files = array_diff($this->getIncludes(), $this->getExcludes()); + + // Call any pre-scaffold scripts that may be defined. + $dispatcher = new EventDispatcher($this->composer, $this->io); + $dispatcher->dispatch(self::PRE_DRUPAL_SCAFFOLD_CMD); + + $version = $this->getDrupalCoreVersion($drupal_core_package); + + $remoteFs = new RemoteFilesystem($this->io); + + $fetcher = new PrestissimoFileFetcher($remoteFs, $options['source'], $this->io, $this->progress, $this->composer->getConfig()); + $fetcher->setFilenames(array_combine($files, $files)); + $fetcher->fetch($version, $webroot, TRUE); + + $fetcher->setFilenames($this->getInitial()); + $fetcher->fetch($version, $webroot, FALSE); + + // Call post-scaffold scripts. + $dispatcher->dispatch(self::POST_DRUPAL_SCAFFOLD_CMD); + } + + /** + * Generate the autoload file at the Drupal root. + * + * Include the autoload file that Composer generated. + */ + public function generateAutoload() { + $vendor_path = $this->getVendorPath(); + $webroot = $this->getWebRoot(); + + // Calculate the relative path from the webroot (location of the + // project autoload.php) to the vendor directory. + $fs = new SymfonyFilesystem(); + $relative_vendor_path = $fs->makePathRelative($vendor_path, realpath($webroot)); + + $fs->dumpFile($webroot . "/autoload.php", $this->autoLoadContents($relative_vendor_path)); + } + + /** + * Build the contents of the docroot autoload file. + * + * This allows the vendor directory to live outside of the docroot. + * + * @param string $relativeVendorPath + * Path to the vendor directory, relative to the Drupal root. + * + * @return string + * The contents of the autoload.php for the docroot directory. + */ + protected function autoLoadContents($relativeVendorPath) { + $relativeVendorPath = rtrim($relativeVendorPath, '/'); + + $autoload_contents = <<composer->getConfig(); + $filesystem = new Filesystem(); + $filesystem->ensureDirectoryExists($config->get('vendor-dir')); + $vendor_path = $filesystem->normalizePath(realpath($config->get('vendor-dir'))); + + return $vendor_path; + } + + /** + * Returns the drupal core package object. + * + * Look up the Drupal core package object, or return it from where we cached + * it in the $drupalCorePackage field. + * + * @return \Composer\Package\PackageInterface + * Composer package for drupal/core. + */ + public function getDrupalCorePackage() { + if (!isset($this->drupalCorePackage)) { + $this->drupalCorePackage = $this->getPackage('drupal/core'); + } + return $this->drupalCorePackage; + } + + /** + * Returns the Drupal core version for the given package. + * + * @param \Composer\Package\PackageInterface $drupalCorePackage + * The Composer package for drupal/core. + * + * @return string + * The version number of drupal/core in this installation. + */ + protected function getDrupalCoreVersion(PackageInterface $drupalCorePackage) { + $version = $drupalCorePackage->getPrettyVersion(); + if ($drupalCorePackage->getStability() == 'dev' && substr($version, -4) == '-dev') { + $version = substr($version, 0, -4); + return $version; + } + return $version; + } + + /** + * Retrieve the path to the web root. + * + * @return string + * The webroot. Well, actually, the drupal root. + */ + public function getWebRoot() { + $drupal_core_package = $this->getDrupalCorePackage(); + $installation_manager = $this->composer->getInstallationManager(); + $core_path = $installation_manager->getInstallPath($drupal_core_package); + // Webroot is the parent path of the drupal core installation path. + $webroot = dirname($core_path); + + return $webroot; + } + + /** + * Retrieve a package from the current composer process. + * + * @param string $name + * Name of the package to get from the current composer installation. + * + * @return \Composer\Package\PackageInterface + * A Composer Package matching the given name. + */ + protected function getPackage($name) { + return $this->composer->getRepositoryManager()->getLocalRepository()->findPackage($name, '*'); + } + + /** + * Retrieve excludes from optional "extra" configuration. + * + * @return string[] + * Any scaffold files that we would like to exclude from the default config. + */ + protected function getExcludes() { + return $this->getNamedOptionList('excludes', 'getExcludesDefault'); + } + + /** + * Retrieve list of additional files from optional "extra" configuration. + * + * @return string[] + * Additional scaffold files we want to retrieve. + */ + protected function getIncludes() { + return $this->getNamedOptionList('includes', 'getIncludesDefault'); + } + + /** + * Retrieve list of initial files from optional "extra" configuration. + * + * @return string[] + * A list of files that should only be scaffolded on primary installation. + */ + protected function getInitial() { + return $this->getNamedOptionList('initial', 'getInitialDefault'); + } + + /** + * Gets a configuration value from extra config for drupal-scaffold. + * + * Retrieve a named list of options from optional "extra" configuration. + * Respects 'omit-defaults', and either includes or does not include the + * default values, as requested. + * + * @param string $optionName + * The particular option value to retrieve. + * @param string $defaultFn + * Function to gather defaults from for this option. + * + * @return string[] + * Array of defined values in the extra config. + */ + protected function getNamedOptionList($optionName, $defaultFn) { + $options = $this->getOptions(); + $result = []; + if (empty($options['omit-defaults'])) { + $result = $this->$defaultFn(); + } + $result = array_merge($result, (array) $options[$optionName]); + + return $result; + } + + /** + * Retrieve excludes from optional "extra" configuration. + * + * The 'source' option is the url pattern where we locate the files, e.g. + * Github: https://raw.githubusercontent.com/drupal/drupal/{version}/{path} + * Drupal.org: https://cgit.drupalcode.org/drupal/plain/{path}?h={version} + * + * @return string[] + * An array of plugin configuration options. + */ + protected function getOptions() { + $extra = $this->composer->getPackage()->getExtra() + ['drupal-scaffold' => []]; + $options = $extra['drupal-scaffold'] + [ + 'omit-defaults' => FALSE, + 'excludes' => [], + 'includes' => [], + 'initial' => [], + 'source' => 'https://cgit.drupalcode.org/drupal/plain/{path}?h={version}', + ]; + return $options; + } + + /** + * Holds default excludes. + * + * @return string[] + * Nothing is excluded by default. + */ + protected function getExcludesDefault() { + return []; + } + + /** + * Holds default settings files list. + * + * @return string[] + * The default scaffolding files that come with drupal. + */ + protected function getIncludesDefault() { + + $common = [ + '.csslintrc', + '.editorconfig', + '.eslintignore', + '.eslintrc.json', + '.gitattributes', + '.ht.router.php', + '.htaccess', + 'autoload.php', + 'example.gitignore', + 'index.php', + 'INSTALL.txt', + 'README.txt', + 'robots.txt', + 'update.php', + 'web.config', + 'modules/README.txt', + 'profiles/README.txt', + 'sites/default/default.settings.php', + 'sites/default/default.services.yml', + 'sites/development.services.yml', + 'sites/example.settings.local.php', + 'sites/example.sites.php', + 'sites/README.txt', + 'themes/README.txt', + ]; + + sort($common); + return $common; + } + + /** + * Holds default initial files. + * + * @return string[] + * Empty array for no initial default. + */ + protected function getInitialDefault() { + return []; + } + + /** + * Remove possibly problematic test files from a single vendor package. + * + * @param string $vendor_dir + * Full path to the vendor directory. + * @param string $package_path + * The package path within the vendor directory. Should also happen to be + * the package name. Example: psr/log. + * @param string[] $cleanup_paths + * An array of relative paths within the vendor path which should be + * removed. + * @param \Composer\IO\IOInterface $io + * IO object provided by Composer. + */ + protected static function doTestCodeCleanup($vendor_dir, $package_path, array $cleanup_paths, IOInterface $io) { + $package_dir = $vendor_dir . '/' . $package_path; + if (is_dir($package_dir)) { + $io->write(sprintf(" Test code cleanup for %s", $package_path), TRUE, $io::VERY_VERBOSE); + foreach ($cleanup_paths as $cleanup_path) { + $cleanup_dir = $package_dir . '/' . $cleanup_path; + if (is_dir($cleanup_dir)) { + // Try to clean up. + if (static::deleteRecursive($cleanup_dir)) { + $io->write(sprintf(" Removing directory '%s'", $cleanup_path), TRUE, $io::VERY_VERBOSE); + } + else { + // Always display a message if this fails as it means something + // has gone wrong. Therefore the message has to include the + // package name as the first informational message might not + // exist. + $io->write(sprintf(" Failure removing directory '%s' in package %s.", $cleanup_path, $package_path), TRUE, IOInterface::NORMAL); + } + } + else { + // If the package has changed or the --prefer-dist version does not + // include the directory this is not an error. + $io->write(sprintf(" Directory '%s' does not exist", $cleanup_dir), TRUE, $io::VERY_VERBOSE); + } + } + $io->write('', TRUE, $io::VERY_VERBOSE); + } + } + + /** + * Find the array key for a given package name with a case-insensitive search. + * + * @param string $package_name + * The package name from composer. This is always already lower case. + * + * @return string|null + * The string key, or NULL if none was found. + */ + protected static function findPackageKey($package_name) { + $package_key = NULL; + // In most cases the package name is already used as the array key. + if (isset(static::$packageToCleanup[$package_name])) { + $package_key = $package_name; + } + else { + // Handle any mismatch in case between the package name and array key. + // For example, the array key 'mikey179/vfsStream' needs to be found + // when composer returns a package name of 'mikey179/vfsstream'. + foreach (static::$packageToCleanup as $key => $dirs) { + if (strtolower($key) === $package_name) { + $package_key = $key; + break; + } + } + } + return $package_key; + } + + /** + * Helper method to remove directories and the files they contain. + * + * @param string $path + * The directory or file to remove. It must exist. + * + * @return bool + * TRUE on success or FALSE on failure. + */ + protected static function deleteRecursive($path) { + if (is_file($path) || is_link($path)) { + return unlink($path); + } + $success = TRUE; + $dir = dir($path); + while (($entry = $dir->read()) !== FALSE) { + if ($entry == '.' || $entry == '..') { + continue; + } + $entry_path = $path . '/' . $entry; + $success = static::deleteRecursive($entry_path) && $success; + } + $dir->close(); + + return rmdir($path) && $success; + } + +} diff --git a/core/lib/Drupal/Component/Scaffold/LICENSE.txt b/core/lib/Drupal/Component/Scaffold/LICENSE.txt new file mode 100644 index 0000000000..94fb84639c --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/core/lib/Drupal/Component/Scaffold/Plugin.php b/core/lib/Drupal/Component/Scaffold/Plugin.php new file mode 100644 index 0000000000..81f27d8ea2 --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/Plugin.php @@ -0,0 +1,92 @@ +handler = new Handler($composer, $io); + } + + /** + * {@inheritdoc} + */ + public function getCapabilities() { + return [ + 'Composer\Plugin\Capability\CommandProvider' => 'Drupal\Component\Scaffold\CommandProvider', + ]; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + return [ + PackageEvents::POST_PACKAGE_INSTALL => 'postPackage', + PackageEvents::POST_PACKAGE_UPDATE => 'postPackage', + ScriptEvents::POST_UPDATE_CMD => 'postCmd', + ScriptEvents::POST_CREATE_PROJECT_CMD => 'postCmd', + PluginEvents::COMMAND => 'cmdBegins', + ]; + } + + /** + * Command begins event callback. + * + * @param \Composer\Plugin\CommandEvent $event + * Composer event sent on command execution. + */ + public function cmdBegins(CommandEvent $event) { + $this->handler->onCmdBeginsEvent($event); + } + + /** + * Post package event behaviour. + * + * @param \Composer\Installer\PackageEvent $event + * Composer package event sent on install/update/remove. + */ + public function postPackage(PackageEvent $event) { + $this->handler->onPostPackageEvent($event); + } + + /** + * Post command event callback. + * + * @param \Composer\Script\Event $event + * Composer script event sent when commands are run. + */ + public function postCmd(Event $event) { + $this->handler->onPostCmdEvent($event); + } + +} diff --git a/core/lib/Drupal/Component/Scaffold/PrestissimoFileFetcher.php b/core/lib/Drupal/Component/Scaffold/PrestissimoFileFetcher.php new file mode 100644 index 0000000000..cbc4e5bb88 --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/PrestissimoFileFetcher.php @@ -0,0 +1,100 @@ +config = $config; + } + + /** + * {@inheritdoc} + */ + public function fetch($version, $destination, $override) { + if (class_exists(CurlMulti::class)) { + $this->fetchWithPrestissimo($version, $destination, $override); + return; + } + parent::fetch($version, $destination, $override); + } + + /** + * Fetch files in parallel. + * + * @param string $version + * The version of the scaffold file to be retrieved. + * @param string $destination + * The location on the filesystem where we will place the file. + * @param bool $override + * Whether the file should be overridden or left in place. + */ + protected function fetchWithPrestissimo($version, $destination, $override) { + $requests = []; + + foreach ($this->filenames as $sourceFilename => $filename) { + $target = "$destination/$filename"; + if ($override || !file_exists($target)) { + $url = $this->getUri($sourceFilename, $version); + $this->fs->ensureDirectoryExists($destination . '/' . dirname($filename)); + $requests[] = new CopyRequest($url, $target, FALSE, $this->io, $this->config); + } + } + + $successCnt = $failureCnt = 0; + $totalCnt = count($requests); + if ($totalCnt == 0) { + return; + } + + $multi = new CurlMulti(); + $multi->setRequests($requests); + do { + $multi->setupEventLoop(); + $multi->wait(); + $result = $multi->getFinishedResults(); + $successCnt += $result['successCnt']; + $failureCnt += $result['failureCnt']; + if ($this->progress) { + foreach ($result['urls'] as $url) { + $this->io->writeError(" - Downloading $successCnt/$totalCnt: $url", TRUE); + } + } + } while ($multi->remain()); + } + +} diff --git a/core/lib/Drupal/Component/Scaffold/README.md b/core/lib/Drupal/Component/Scaffold/README.md new file mode 100644 index 0000000000..3ead55067f --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/README.md @@ -0,0 +1,126 @@ +# drupal-scaffold + +Composer plugin for automatically downloading Drupal scaffold files (like +`index.php`, `update.php`, etc) when requiring `drupal/core` via Composer. + +It is recommended that the vendor directory be placed in its standard location +at the project root, outside of the Drupal root; however, the location of the +vendor directory and the Drupal root may be placed in whatever +location suits the project. Drupal-scaffold will generate the autoload.php +file at the Drupal root to require the Composer-generated autoload file in the +vendor directory. + +## Usage + +Run `composer require drupal/drupal-scaffold` in your Composer +project before installing or updating `drupal/core`. + +Once drupal-scaffold is required by your project, it will automatically update +your scaffold files whenever `composer update` changes the version of +`drupal/core` installed. + +## Configuration + +You can configure the plugin by providing some settings in the `extra` section +of your root `composer.json`. + +```json +{ + "extra": { + "drupal-scaffold": { + "source": "https://cgit.drupalcode.org/drupal/plain/{path}?h={version}", + "excludes": [ + "google123.html", + "robots.txt" + ], + "includes": [ + "sites/default/example.settings.my.php" + ], + "initial": { + "sites/default/default.services.yml": "sites/default/services.yml", + "sites/default/default.settings.php": "sites/default/settings.php" + }, + "omit-defaults": false + } + } +} +``` +The `source` option may be used to specify the URL to download the +scaffold files from; the default source is cgit.drupalcode.org. The literal string +`{version}` in the `source` option is replaced with the current version of +Drupal core being updated prior to download. + +With the `drupal-scaffold` option `excludes`, you can provide additional paths +that should not be copied or overwritten. The plugin provides no excludes by +default. + +Default includes are provided by the plugin: +``` +.csslintrc +.editorconfig +.eslintignore +.eslintrc.json +.gitattributes +.ht.router.php +.htaccess +example.gitignore +index.php +INSTALL.txt +README.txt +robots.txt +modules/README.txt +profiles/README.txt +sites/README.txt +sites/default/default.settings.php +sites/default/default.services.yml +sites/development.services.yml +sites/example.settings.local.php +sites/example.sites.php +themes/README.txt +update.php +web.config +``` + +When setting `omit-defaults` to `true`, neither the default excludes nor the +default includes will be provided; in this instance, only those files explicitly +listed in the `excludes` and `includes` options will be considered. If +`omit-defaults` is `false` (the default), then any items listed in `excludes` +or `includes` will be in addition to the usual defaults. + +The `initial` hash lists files that should be copied over only if they do not +exist in the destination. The key specifies the path to the source file, and +the value indicates the path to the destination file. + +## Limitation + +When using Composer to install or update the Drupal development branch, the +scaffold files are always taken from the HEAD of the branch (or, more +specifically, from the most recent development .tar.gz archive). This might +not be what you want when using an old development version (e.g. when the +version is fixed via composer.lock). To avoid problems, always commit your +scaffold files to the repository any time that composer.lock is committed. +Note that the correct scaffold files are retrieved when using a tagged release +of `drupal/core` (recommended). + +## Custom command + +The plugin by default is only downloading the scaffold files when installing or +updating `drupal/core`. You can call it manually with "composer drupal:scaffold" +or "@composer drupal:scaffold" in Composer Scripts. + +```json +"scripts": { + "post-install-cmd": [ + "@composer drupal:scaffold", + "..." + ], + "post-update-cmd": [ + "@composer drupal:scaffold", + "..." + ] +}, +``` + +It is assumed that the scaffold files will be committed to the repository, to +ensure that the correct files are used on the CI server (see **Limitation**, +above). Commit the scaffold files to your repository after running `composer install` for the first time. diff --git a/core/lib/Drupal/Component/Scaffold/TESTING.txt b/core/lib/Drupal/Component/Scaffold/TESTING.txt new file mode 100644 index 0000000000..3186f0f901 --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/TESTING.txt @@ -0,0 +1,18 @@ +HOW-TO: Test this Drupal component + +In order to test this component, you'll need to get the entire Drupal repo and +run the tests there. + +You'll find the tests under core/tests/Drupal/Tests/Component. + +You can get the full Drupal repo here: +https://www.drupal.org/project/drupal/git-instructions + +You can find more information about running PHPUnit tests with Drupal here: +https://www.drupal.org/node/2116263 + +Each component in the Drupal\Component namespace has its own annotated test +group. You can use this group to run only the tests for this component. Like +this: + +$ ./vendor/bin/phpunit -c core --group Scaffold diff --git a/core/lib/Drupal/Component/Scaffold/composer.json b/core/lib/Drupal/Component/Scaffold/composer.json new file mode 100644 index 0000000000..6f77bf7ad0 --- /dev/null +++ b/core/lib/Drupal/Component/Scaffold/composer.json @@ -0,0 +1,27 @@ +{ + "name": "drupal/drupal-scaffold", + "description": "New Composer plugin for updating the Drupal scaffold files when using drupal/core", + "type": "composer-plugin", + "license": "GPL-2.0-or-later", + "require": { + "php": ">=5.5.9", + "composer-plugin-api": "^1.0.0" + }, + "require-dev": { + "composer/composer": "^1.8", + "symfony/process": "~3.4.0" + }, + "autoload": { + "classmap": [ + "CommandProvider.php", + "DrupalScaffoldCommand.php", + "FileFetcher.php", + "Handler.php", + "Plugin.php", + "PrestissimoFileFetcher.php" + ] + }, + "extra": { + "class": "Drupal\\Component\\Scaffold\\Plugin" + } +} diff --git a/core/lib/Drupal/Core/Composer/Composer.php b/core/lib/Drupal/Core/Composer/Composer.php index 9833b7ede1..d2f352eef8 100644 --- a/core/lib/Drupal/Core/Composer/Composer.php +++ b/core/lib/Drupal/Core/Composer/Composer.php @@ -15,60 +15,6 @@ */ class Composer { - protected static $packageToCleanup = [ - 'behat/mink' => ['tests', 'driver-testsuite'], - 'behat/mink-browserkit-driver' => ['tests'], - 'behat/mink-goutte-driver' => ['tests'], - 'drupal/coder' => ['coder_sniffer/Drupal/Test', 'coder_sniffer/DrupalPractice/Test'], - 'doctrine/cache' => ['tests'], - 'doctrine/collections' => ['tests'], - 'doctrine/common' => ['tests'], - 'doctrine/inflector' => ['tests'], - 'doctrine/instantiator' => ['tests'], - 'egulias/email-validator' => ['documentation', 'tests'], - 'fabpot/goutte' => ['Goutte/Tests'], - 'guzzlehttp/promises' => ['tests'], - 'guzzlehttp/psr7' => ['tests'], - 'jcalderonzumba/gastonjs' => ['docs', 'examples', 'tests'], - 'jcalderonzumba/mink-phantomjs-driver' => ['tests'], - 'masterminds/html5' => ['test'], - 'mikey179/vfsStream' => ['src/test'], - 'paragonie/random_compat' => ['tests'], - 'phpdocumentor/reflection-docblock' => ['tests'], - 'phpunit/php-code-coverage' => ['tests'], - 'phpunit/php-timer' => ['tests'], - 'phpunit/php-token-stream' => ['tests'], - 'phpunit/phpunit' => ['tests'], - 'phpunit/php-mock-objects' => ['tests'], - 'sebastian/comparator' => ['tests'], - 'sebastian/diff' => ['tests'], - 'sebastian/environment' => ['tests'], - 'sebastian/exporter' => ['tests'], - 'sebastian/global-state' => ['tests'], - 'sebastian/recursion-context' => ['tests'], - 'stack/builder' => ['tests'], - 'symfony/browser-kit' => ['Tests'], - 'symfony/class-loader' => ['Tests'], - 'symfony/console' => ['Tests'], - 'symfony/css-selector' => ['Tests'], - 'symfony/debug' => ['Tests'], - 'symfony/dependency-injection' => ['Tests'], - 'symfony/dom-crawler' => ['Tests'], - // @see \Drupal\Tests\Component\EventDispatcher\ContainerAwareEventDispatcherTest - // 'symfony/event-dispatcher' => ['Tests'], - 'symfony/http-foundation' => ['Tests'], - 'symfony/http-kernel' => ['Tests'], - 'symfony/process' => ['Tests'], - 'symfony/psr-http-message-bridge' => ['Tests'], - 'symfony/routing' => ['Tests'], - 'symfony/serializer' => ['Tests'], - 'symfony/translation' => ['Tests'], - 'symfony/validator' => ['Tests', 'Resources'], - 'symfony/yaml' => ['Tests'], - 'symfony-cmf/routing' => ['Test', 'Tests'], - 'twig/twig' => ['doc', 'ext', 'test'], - ]; - /** * Add vendor classes to Composer's static classmap. */ @@ -192,82 +138,16 @@ public static function upgradePHPUnitCheck($phpunit_version) { * @param \Composer\Installer\PackageEvent $event * A PackageEvent object to get the configured composer vendor directories * from. + * + * @deprecated in Drupal 8.7.0, will be removed before Drupal 9.0.0. */ public static function vendorTestCodeCleanup(PackageEvent $event) { - $vendor_dir = $event->getComposer()->getConfig()->get('vendor-dir'); $io = $event->getIO(); - $op = $event->getOperation(); - if ($op->getJobType() == 'update') { - $package = $op->getTargetPackage(); - } - else { - $package = $op->getPackage(); - } - $package_key = static::findPackageKey($package->getName()); - $message = sprintf(" Processing %s", $package->getPrettyName()); + // @todo: Add @see for change record. + $message = __METHOD__ . '() has been deprecated. Remove the script calls in your project composer.json'; if ($io->isVeryVerbose()) { - $io->write($message); - } - if ($package_key) { - foreach (static::$packageToCleanup[$package_key] as $path) { - $dir_to_remove = $vendor_dir . '/' . $package_key . '/' . $path; - $print_message = $io->isVeryVerbose(); - if (is_dir($dir_to_remove)) { - if (static::deleteRecursive($dir_to_remove)) { - $message = sprintf(" Removing directory '%s'", $path); - } - else { - // Always display a message if this fails as it means something has - // gone wrong. Therefore the message has to include the package name - // as the first informational message might not exist. - $print_message = TRUE; - $message = sprintf(" Failure removing directory '%s' in package %s.", $path, $package->getPrettyName()); - } - } - else { - // If the package has changed or the --prefer-dist version does not - // include the directory this is not an error. - $message = sprintf(" Directory '%s' does not exist", $path); - } - if ($print_message) { - $io->write($message); - } - } - - if ($io->isVeryVerbose()) { - // Add a new line to separate this output from the next package. - $io->write(""); - } - } - } - - /** - * Find the array key for a given package name with a case-insensitive search. - * - * @param string $package_name - * The package name from composer. This is always already lower case. - * - * @return string|null - * The string key, or NULL if none was found. - */ - protected static function findPackageKey($package_name) { - $package_key = NULL; - // In most cases the package name is already used as the array key. - if (isset(static::$packageToCleanup[$package_name])) { - $package_key = $package_name; - } - else { - // Handle any mismatch in case between the package name and array key. - // For example, the array key 'mikey179/vfsStream' needs to be found - // when composer returns a package name of 'mikey179/vfsstream'. - foreach (static::$packageToCleanup as $key => $dirs) { - if (strtolower($key) === $package_name) { - $package_key = $key; - break; - } - } + $io->write('' . $message . ''); } - return $package_key; } /** @@ -276,32 +156,4 @@ protected static function findPackageKey($package_name) { public static function removeTimeout() { ProcessExecutor::setTimeout(0); } - - /** - * Helper method to remove directories and the files they contain. - * - * @param string $path - * The directory or file to remove. It must exist. - * - * @return bool - * TRUE on success or FALSE on failure. - */ - protected static function deleteRecursive($path) { - if (is_file($path) || is_link($path)) { - return unlink($path); - } - $success = TRUE; - $dir = dir($path); - while (($entry = $dir->read()) !== FALSE) { - if ($entry == '.' || $entry == '..') { - continue; - } - $entry_path = $path . '/' . $entry; - $success = static::deleteRecursive($entry_path) && $success; - } - $dir->close(); - - return rmdir($path) && $success; - } - } diff --git a/core/tests/Drupal/Tests/Component/Scaffold/FetcherTest.php b/core/tests/Drupal/Tests/Component/Scaffold/FetcherTest.php new file mode 100644 index 0000000000..48f341a6c1 --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/FetcherTest.php @@ -0,0 +1,85 @@ +rootDir = realpath(realpath(__DIR__ . '/Scaffold')); + + // Prepare temp directory. + $this->fs = new Filesystem(); + $this->tmpDir = realpath(sys_get_temp_dir()) . DIRECTORY_SEPARATOR . 'drupal-scaffold'; + $this->ensureDirectoryExistsAndClear($this->tmpDir); + + chdir($this->tmpDir); + } + + /** + * Makes sure the given directory exists and has no content. + * + * @param string $directory + */ + protected function ensureDirectoryExistsAndClear($directory) { + if (is_dir($directory)) { + $this->fs->removeDirectory($directory); + } + mkdir($directory, 0777, TRUE); + } + + public function testFetch() { + $fetcher = new FileFetcher(new RemoteFilesystem(new NullIO()), 'https://cgit.drupalcode.org/drupal/plain/{path}?h={version}', new NullIO()); + $fetcher->setFilenames([ + '.htaccess' => '.htaccess', + 'sites/default/default.settings.php' => 'sites/default/default.settings.php', + ]); + $fetcher->fetch('8.1.1', $this->tmpDir, TRUE); + $this->assertFileExists($this->tmpDir . '/.htaccess'); + $this->assertFileExists($this->tmpDir . '/sites/default/default.settings.php'); + } + + public function testInitialFetch() { + $fetcher = new FileFetcher(new RemoteFilesystem(new NullIO()), 'https://cgit.drupalcode.org/drupal/plain/{path}?h={version}', new NullIO()); + $fetcher->setFilenames([ + 'sites/default/default.settings.php' => 'sites/default/settings.php', + ]); + $fetcher->fetch('8.1.1', $this->tmpDir, FALSE); + $this->assertFileExists($this->tmpDir . '/sites/default/settings.php'); + } + +} diff --git a/core/tests/Drupal/Tests/Component/Scaffold/PluginTest.php b/core/tests/Drupal/Tests/Component/Scaffold/PluginTest.php new file mode 100644 index 0000000000..92c4338edd --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Scaffold/PluginTest.php @@ -0,0 +1,174 @@ +drupalRootDir = dirname(dirname(dirname(dirname(dirname(dirname(__DIR__)))))); + // Get the root directory of the Scaffold component. + $this->componentRootDir = $this->drupalRootDir . '/core/lib/Drupal/Component/Scaffold'; + + // Prepare temp directory. + $this->fs = new Filesystem(); + $this->tmpDir = realpath(sys_get_temp_dir()) . DIRECTORY_SEPARATOR . 'drupal-scaffold'; + $this->ensureDirectoryExistsAndClear($this->tmpDir); + $this->writeComposerJSON(); + + chdir($this->tmpDir); + } + + /** + * TearDown. + * + * @return void + */ + public function tearDown() { + $this->fs->removeDirectory($this->tmpDir); + } + + /** + * Tests a simple composer install without core, but adding core later. + */ + public function testComposerInstallAndUpdate() { + $exampleScaffoldFile = $this->tmpDir . DIRECTORY_SEPARATOR . 'index.php'; + + $this->assertFileNotExists($exampleScaffoldFile, 'Scaffold file should not be exist.'); + $this->composer('install --no-dev --prefer-dist'); + $this->assertFileExists($this->tmpDir . DIRECTORY_SEPARATOR . 'core', 'Drupal core is installed.'); + $this->assertFileExists($exampleScaffoldFile, 'Scaffold file should be automatically installed.'); + + $this->fs->remove($exampleScaffoldFile); + $this->assertFileNotExists($exampleScaffoldFile, 'Scaffold file should not exist.'); + $this->composer('drupal:scaffold'); + $this->assertFileExists($exampleScaffoldFile, 'Scaffold file should be installed by "drupal:scaffold" command.'); + + // We touch a scaffold file, so we can check the file was modified after the + // scaffold update. + $version = '8.7.x-dev'; + touch($exampleScaffoldFile); + $mtime_touched = filemtime($exampleScaffoldFile); + // Requiring a newer version triggers "composer update". + $this->composer('require --update-with-dependencies --prefer-dist --update-no-dev drupal/core:"' . $version . '"'); + clearstatcache(); + $mtime_after = filemtime($exampleScaffoldFile); + $this->assertNotEquals($mtime_after, $mtime_touched, 'Scaffold file was modified by composer update. (' . $version . ')'); + + // We touch a scaffold file, so we can check the file was modified by the + // custom command. + file_put_contents($exampleScaffoldFile, 1); + $this->composer('drupal:scaffold'); + $this->assertNotEquals(file_get_contents($exampleScaffoldFile), 1, 'Scaffold file was modified by custom command.'); + } + + /** + * Writes the default composer json to the temp direcoty. + */ + protected function writeComposerJSON() { + $json = json_encode($this->composerJSONDefaults(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + // Write composer.json. + file_put_contents($this->tmpDir . '/composer.json', $json); + } + + /** + * Provides the default composer.json data. + * + * @return array + */ + protected function composerJSONDefaults() { + $package = json_decode(file_get_contents($this->componentRootDir . '/composer.json'), TRUE); + $package['dist'] = [ + 'type' => 'path', + 'url' => $this->componentRootDir, + ]; + $package['version'] = '999.0.' . time(); + return [ + 'repositories' => [ + [ + 'type' => 'package', + 'package' => $package, + ], + ], + 'require' => [ + 'composer/installers' => '^1.0.20', + 'drupal/drupal-scaffold' => $package['version'], + 'drupal/core' => '8.6.0', + ], + 'minimum-stability' => 'dev', + 'prefer-stable' => TRUE, + ]; + } + + /** + * Wrapper for the composer command. + * + * @param string $command + * Composer command name, arguments and/or options. + */ + protected function composer($command) { + $commands = [ + $this->drupalRootDir . '/vendor/bin/composer', + $command, + ]; + $ps = new Process(implode(' ', $commands), $this->tmpDir); + $ps->mustRun(); + } + + /** + * Makes sure the given directory exists and has no content. + * + * @param string $directory + */ + protected function ensureDirectoryExistsAndClear($directory) { + if (is_dir($directory)) { + $this->fs->removeDirectory($directory); + } + mkdir($directory, 0777, TRUE); + } + +}