Skip to content
Snippets Groups Projects
Verified Commit 3bb50b2b authored by Lee Rowlands's avatar Lee Rowlands
Browse files

Issue #2982684 by greg.1.anderson, Mile23, Mixologic, webflo, alexpott,...

Issue #2982684 by greg.1.anderson, Mile23, Mixologic, webflo, alexpott, yogeshmpawar, pingwin4eg, vijaycs85, larowlan, dww, borisson_, phenaproxima, kim.pepper, bojanz, grasmash, hctom, kmbremner, pingers, Jax, sherakama, derhasi, claudiu.cristea, jhedstrom, Xano, Grimreaper: Add a composer scaffolding plugin to core
parent c1734c61
Branches
Tags
2 merge requests!7452Issue #1797438. HTML5 validation is preventing form submit and not fully...,!789Issue #3210310: Adjust Database API to remove deprecated Drupal 9 code in Drupal 10
Showing
with 2142 additions and 0 deletions
......@@ -110,6 +110,7 @@
"drupal/core-proxy-builder": "self.version",
"drupal/core-render": "self.version",
"drupal/core-serialization": "self.version",
"drupal/core-composer-scaffold": "self.version",
"drupal/core-transliteration": "self.version",
"drupal/core-utility": "self.version",
"drupal/core-uuid": "self.version",
......@@ -201,6 +202,7 @@
"core/lib/Drupal/Component/ProxyBuilder/composer.json",
"core/lib/Drupal/Component/Render/composer.json",
"core/lib/Drupal/Component/Serialization/composer.json",
"core/lib/Drupal/Component/Scaffold/composer.json",
"core/lib/Drupal/Component/Transliteration/composer.json",
"core/lib/Drupal/Component/Utility/composer.json",
"core/lib/Drupal/Component/Uuid/composer.json",
......
<?php
namespace Drupal\Component\Scaffold;
use Composer\Composer;
use Composer\Installer\PackageEvent;
use Composer\IO\IOInterface;
use Composer\Package\PackageInterface;
/**
* Determine recursively which packages have been allowed to scaffold files.
*
* If the root-level composer.json allows drupal/core, and drupal/core allows
* drupal/assets, then the later package will also implicitly be allowed.
*/
class AllowedPackages implements PostPackageEventListenerInterface {
/**
* The Composer service.
*
* @var \Composer\Composer
*/
protected $composer;
/**
* Composer's I/O service.
*
* @var \Composer\IO\IOInterface
*/
protected $io;
/**
* Manager of the options in the top-level composer.json's 'extra' section.
*
* @var \Drupal\Component\Scaffold\ManageOptions
*/
protected $manageOptions;
/**
* The list of new packages added by this Composer command.
*
* @var array
*/
protected $newPackages = [];
/**
* AllowedPackages constructor.
*
* @param \Composer\Composer $composer
* The composer object.
* @param \Composer\IO\IOInterface $io
* IOInterface to write to.
* @param \Drupal\Component\Scaffold\ManageOptions $manage_options
* Manager of the options in the top-level composer.json's 'extra' section.
*/
public function __construct(Composer $composer, IOInterface $io, ManageOptions $manage_options) {
$this->composer = $composer;
$this->io = $io;
$this->manageOptions = $manage_options;
}
/**
* Gets a list of all packages that are allowed to copy scaffold files.
*
* Configuration for packages specified later will override configuration
* specified by packages listed earlier. In other words, the last listed
* package has the highest priority. The root package will always be returned
* at the end of the list.
*
* @return \Composer\Package\PackageInterface[]
* An array of allowed Composer packages.
*/
public function getAllowedPackages() {
$options = $this->manageOptions->getOptions();
$allowed_packages = $this->recursiveGetAllowedPackages($options->allowedPackages());
// If the root package defines any file mappings, then implicitly add it
// to the list of allowed packages. Add it at the end so that it overrides
// all the preceding packages.
if ($options->hasFileMapping()) {
$root_package = $this->composer->getPackage();
unset($allowed_packages[$root_package->getName()]);
$allowed_packages[$root_package->getName()] = $root_package;
}
// Handle any newly-added packages that are not already allowed.
return $this->evaluateNewPackages($allowed_packages);
}
/**
* {@inheritdoc}
*/
public function event(PackageEvent $event) {
$operation = $event->getOperation();
// Determine the package.
$package = $operation->getJobType() == 'update' ? $operation->getTargetPackage() : $operation->getPackage();
if (ScaffoldOptions::hasOptions($package->getExtra())) {
$this->newPackages[$package->getName()] = $package;
}
}
/**
* Builds a name-to-package mapping from a list of package names.
*
* @param string[] $packages_to_allow
* List of package names to allow.
* @param array $allowed_packages
* Mapping of package names to PackageInterface of packages already
* accumulated.
*
* @return \Composer\Package\PackageInterface[]
* Mapping of package names to PackageInterface in priority order.
*/
protected function recursiveGetAllowedPackages(array $packages_to_allow, array $allowed_packages = []) {
foreach ($packages_to_allow as $name) {
$package = $this->getPackage($name);
if ($package instanceof PackageInterface && !isset($allowed_packages[$name])) {
$allowed_packages[$name] = $package;
$package_options = $this->manageOptions->packageOptions($package);
$allowed_packages = $this->recursiveGetAllowedPackages($package_options->allowedPackages(), $allowed_packages);
}
}
return $allowed_packages;
}
/**
* Evaluates newly-added packages and see if they are already allowed.
*
* For now we will only emit warnings if they are not.
*
* @param array $allowed_packages
* Mapping of package names to PackageInterface of packages already
* accumulated.
*
* @return \Composer\Package\PackageInterface[]
* Mapping of package names to PackageInterface in priority order.
*/
protected function evaluateNewPackages(array $allowed_packages) {
foreach ($this->newPackages as $name => $newPackage) {
if (!array_key_exists($name, $allowed_packages)) {
$this->io->write("Not scaffolding files for <comment>{$name}</comment>, because it is not listed in the element 'extra.composer-scaffold.allowed-packages' in the root-level composer.json file.");
}
else {
$this->io->write("Package <comment>{$name}</comment> has scaffold operations, and is already allowed in the root-level composer.json file.");
}
}
// @todo We could prompt the user and ask if they wish to allow a
// newly-added package. This might be useful if, for example, the user
// might wish to require an installation profile that contains scaffolded
// assets. For more information, see:
// https://www.drupal.org/project/drupal/issues/3064990
return $allowed_packages;
}
/**
* Retrieves 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|null
* The Composer package.
*/
protected function getPackage($name) {
return $this->composer->getRepositoryManager()->getLocalRepository()->findPackage($name, '*');
}
}
<?php
namespace Drupal\Component\Scaffold;
use Composer\Plugin\Capability\CommandProvider as CommandProviderCapability;
/**
* List of all commands provided by this package.
*/
class CommandProvider implements CommandProviderCapability {
/**
* {@inheritdoc}
*/
public function getCommands() {
return [new ComposerScaffoldCommand()];
}
}
<?php
namespace Drupal\Component\Scaffold;
use Composer\Command\BaseCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* The "composer:scaffold" command class.
*
* Manually run the scaffold operation that normally happens after
* 'composer install'.
*/
class ComposerScaffoldCommand extends BaseCommand {
/**
* {@inheritdoc}
*/
protected function configure() {
$this
->setName('composer:scaffold')
->setDescription('Update the Composer scaffold files.')
->setHelp(
<<<EOT
The <info>composer:scaffold</info> command places the scaffold files in their
respective locations according to the layout stipulated in the composer.json
file.
<info>php composer.phar composer:scaffold</info>
It is usually not necessary to call <info>composer:scaffold</info> manually,
because it is called automatically as needed, e.g. after an <info>install</info>
or <info>update</info> command. Note, though, that only packages explicitly
allowed to scaffold in the top-level composer.json will be processed by this
command.
For more information, see https://www.drupal.org/docs/develop/using-composer/using-drupals-composer-scaffold.
EOT
);
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output) {
$handler = new Handler($this->getComposer(), $this->getIO());
$handler->scaffold();
}
}
<?php
namespace Drupal\Component\Scaffold;
use Composer\IO\IOInterface;
use Composer\Util\Filesystem;
use Drupal\Component\Scaffold\Operations\ScaffoldResult;
/**
* Generates an 'autoload.php' that includes the autoloader created by Composer.
*/
final class GenerateAutoloadReferenceFile {
/**
* This class provides only static methods.
*/
private function __construct() {
}
/**
* Generates the autoload file at the specified location.
*
* This only writes a bit of PHP that includes the autoload file that
* Composer generated. Drupal does this so that it can guarantee that there
* will always be an `autoload.php` file in a well-known location.
*
* @param \Composer\IO\IOInterface $io
* IOInterface to write to.
* @param string $package_name
* The name of the package defining the autoload file (the root package).
* @param string $web_root
* The path to the web root.
* @param string $vendor
* The path to the vendor directory.
*
* @return \Drupal\Component\Scaffold\Operations\ScaffoldResult
* The result of the autoload file generation.
*/
public static function generateAutoload(IOInterface $io, $package_name, $web_root, $vendor) {
$autoload_path = static::autoloadPath($package_name, $web_root);
$location = dirname($autoload_path->fullPath());
// Calculate the relative path from the webroot (location of the project
// autoload.php) to the vendor directory.
$fs = new Filesystem();
$relative_vendor_path = $fs->findShortestPath(realpath($location), $vendor);
file_put_contents($autoload_path->fullPath(), static::autoLoadContents($relative_vendor_path));
return new ScaffoldResult($autoload_path, TRUE);
}
/**
* Determines whether or not the autoload file has been committed.
*
* @param \Composer\IO\IOInterface $io
* IOInterface to write to.
* @param string $package_name
* The name of the package defining the autoload file (the root package).
* @param string $web_root
* The path to the web root.
*
* @return bool
* True if autoload.php file exists and has been committed to the repository
*/
public static function autoloadFileCommitted(IOInterface $io, $package_name, $web_root) {
$autoload_path = static::autoloadPath($package_name, $web_root);
$location = dirname($autoload_path->fullPath());
if (!file_exists($location)) {
return FALSE;
}
return Git::checkTracked($io, $location, $location);
}
/**
* Generates a scaffold file path object for the autoload file.
*
* @param string $package_name
* The name of the package defining the autoload file (the root package).
* @param string $web_root
* The path to the web root.
*
* @return \Drupal\Component\Scaffold\ScaffoldFilePath
* Object wrapping the relative and absolute path to the destination file.
*/
protected static function autoloadPath($package_name, $web_root) {
$rel_path = 'autoload.php';
$dest_rel_path = '[web-root]/' . $rel_path;
$dest_full_path = $web_root . '/' . $rel_path;
return new ScaffoldFilePath('autoload', $package_name, $dest_rel_path, $dest_full_path);
}
/**
* Builds the contents of the autoload file.
*
* @param string $vendor_path
* The relative path to vendor.
*
* @return string
* Return the contents for the autoload.php.
*/
protected static function autoLoadContents($vendor_path) {
$vendor_path = rtrim($vendor_path, '/');
return <<<EOF
<?php
/**
* @file
* Includes the autoloader created by Composer.
*
* This file was generated by composer-scaffold.
*.
* @see composer.json
* @see index.php
* @see core/install.php
* @see core/rebuild.php
* @see core/modules/statistics/statistics.php
*/
return require __DIR__ . '/{$vendor_path}/autoload.php';
EOF;
}
}
<?php
namespace Drupal\Component\Scaffold;
use Composer\IO\IOInterface;
use Composer\Util\ProcessExecutor;
/**
* Provide some Git utility operations
*/
class Git {
/**
* This class provides only static methods.
*/
private function __construct() {
}
/**
* Determines whether the specified scaffold file is already ignored.
*
* @param string $path
* Path to scaffold file to check.
* @param string $dir
* Base directory for git process.
*
* @return bool
* Whether the specified file is already ignored or not (TRUE if ignored).
*/
public static function checkIgnore(IOInterface $io, $path, $dir = NULL) {
$process = new ProcessExecutor($io);
$output = '';
$exitCode = $process->execute('git check-ignore ' . $process->escape($path), $output, $dir);
return $exitCode == 0;
}
/**
* Determines whether the specified scaffold file is tracked by git.
*
* @param string $path
* Path to scaffold file to check.
* @param string $dir
* Base directory for git process.
*
* @return bool
* Whether the specified file is already tracked or not (TRUE if tracked).
*/
public static function checkTracked(IOInterface $io, $path, $dir = NULL) {
$process = new ProcessExecutor($io);
$output = '';
$exitCode = $process->execute('git ls-files --error-unmatch ' . $process->escape($path), $output, $dir);
return $exitCode == 0;
}
/**
* Checks to see if the project root dir is in a git repository.
*
* @param string $dir
* Base directory for git process.
* @return bool
* True if this is a repository.
*/
public static function isRepository(IOInterface $io, $dir = NULL) {
$process = new ProcessExecutor($io);
$output = '';
$exitCode = $process->execute('git rev-parse --show-toplevel', $output, $dir);
return $exitCode == 0;
}
}
<?php
namespace Drupal\Component\Scaffold;
use Composer\Composer;
use Composer\EventDispatcher\EventDispatcher;
use Composer\Installer\PackageEvent;
use Composer\IO\IOInterface;
use Composer\Package\PackageInterface;
use Composer\Plugin\CommandEvent;
use Composer\Util\Filesystem;
use Drupal\Component\Scaffold\Operations\OperationData;
use Drupal\Component\Scaffold\Operations\OperationFactory;
use Drupal\Component\Scaffold\Operations\ScaffoldFileCollection;
/**
* Core class of the plugin.
*
* Contains the primary logic which determines the files to be fetched and
* processed.
*/
class Handler {
/**
* Composer hook called before scaffolding begins.
*/
const PRE_COMPOSER_SCAFFOLD_CMD = 'pre-composer-scaffold-cmd';
/**
* Composer hook called after scaffolding completes.
*/
const POST_COMPOSER_SCAFFOLD_CMD = 'post-composer-scaffold-cmd';
/**
* The Composer service.
*
* @var \Composer\Composer
*/
protected $composer;
/**
* Composer's I/O service.
*
* @var \Composer\IO\IOInterface
*/
protected $io;
/**
* The scaffold options in the top-level composer.json's 'extra' section.
*
* @var \Drupal\Component\Scaffold\ManageOptions
*/
protected $manageOptions;
/**
* The manager that keeps track of which packages are allowed to scaffold.
*
* @var \Drupal\Component\Scaffold\AllowedPackages
*/
protected $manageAllowedPackages;
/**
* The list of listeners that are notified after a package event.
*
* @var \Drupal\Component\Scaffold\PostPackageEventListenerInterface[]
*/
protected $postPackageListeners = [];
/**
* Handler constructor.
*
* @param \Composer\Composer $composer
* The Composer service.
* @param \Composer\IO\IOInterface $io
* The Composer I/O service.
*/
public function __construct(Composer $composer, IOInterface $io) {
$this->composer = $composer;
$this->io = $io;
$this->manageOptions = new ManageOptions($composer);
$this->manageAllowedPackages = new AllowedPackages($composer, $io, $this->manageOptions);
}
/**
* Registers post-package events before any 'require' event runs.
*
* This method is called by composer prior to doing a 'require' command.
*
* @param \Composer\Plugin\CommandEvent $event
* The Composer Command event.
*/
public function beforeRequire(CommandEvent $event) {
// In order to differentiate between post-package events called after
// 'composer require' vs. the same events called at other times, we will
// only install our handler when a 'require' event is detected.
$this->postPackageListeners[] = $this->manageAllowedPackages;
}
/**
* Posts package command event.
*
* We want to detect packages 'require'd that have scaffold files, but are not
* yet allowed in the top-level composer.json file.
*
* @param \Composer\Installer\PackageEvent $event
* Composer package event sent on install/update/remove.
*/
public function onPostPackageEvent(PackageEvent $event) {
foreach ($this->postPackageListeners as $listener) {
$listener->event($event);
}
}
/**
* Creates scaffold operation objects for all items in the file mappings.
*
* @param \Composer\Package\PackageInterface $package
* The package that relative paths will be relative from.
* @param array $package_file_mappings
* The package file mappings array keyed by destination path and the values
* are operation metadata arrays.
*
* @return \Drupal\Component\Scaffold\Operations\OperationInterface[]
* A list of scaffolding operation objects
*/
protected function createScaffoldOperations(PackageInterface $package, array $package_file_mappings) {
$scaffold_op_factory = new OperationFactory($this->composer);
$scaffold_ops = [];
foreach ($package_file_mappings as $dest_rel_path => $data) {
$operation_data = new OperationData($dest_rel_path, $data);
$scaffold_ops[$dest_rel_path] = $scaffold_op_factory->create($package, $operation_data);
}
return $scaffold_ops;
}
/**
* Copies all scaffold files from source to destination.
*/
public function scaffold() {
// Recursively get the list of allowed packages. Only allowed packages
// may declare scaffold files. Note that the top-level composer.json file
// is implicitly allowed.
$allowed_packages = $this->manageAllowedPackages->getAllowedPackages();
if (empty($allowed_packages)) {
$this->io->write("Nothing scaffolded because no packages are allowed in the top-level composer.json file.");
return;
}
// Call any pre-scaffold scripts that may be defined.
$dispatcher = new EventDispatcher($this->composer, $this->io);
$dispatcher->dispatch(self::PRE_COMPOSER_SCAFFOLD_CMD);
// Fetch the list of file mappings from each allowed package and normalize
// them.
$file_mappings = $this->getFileMappingsFromPackages($allowed_packages);
$location_replacements = $this->manageOptions->getLocationReplacements();
$scaffold_options = $this->manageOptions->getOptions();
// Create a collection of scaffolded files to process. This determines which
// take priority and which are conjoined.
$scaffold_files = new ScaffoldFileCollection($file_mappings, $location_replacements);
// Process the list of scaffolded files.
$scaffold_results = ScaffoldFileCollection::process($scaffold_files, $this->io, $scaffold_options);
// Generate an autoload file in the document root that includes the
// autoload.php file in the vendor directory, wherever that is. Drupal
// requires this in order to easily locate relocated vendor dirs.
$web_root = $this->manageOptions->getOptions()->getLocation('web-root');
if (!GenerateAutoloadReferenceFile::autoloadFileCommitted($this->io, $this->rootPackageName(), $web_root)) {
$scaffold_results[] = GenerateAutoloadReferenceFile::generateAutoload($this->io, $this->rootPackageName(), $web_root, $this->getVendorPath());
}
// Add the managed scaffold files to .gitignore if applicable.
$gitIgnoreManager = new ManageGitIgnore($this->io, getcwd());
$gitIgnoreManager->manageIgnored($scaffold_results, $scaffold_options);
// Call post-scaffold scripts.
$dispatcher->dispatch(self::POST_COMPOSER_SCAFFOLD_CMD);
}
/**
* Gets the path to the 'vendor' directory.
*
* @return string
* The file path of the vendor directory.
*/
protected function getVendorPath() {
$vendor_dir = $this->composer->getConfig()->get('vendor-dir');
$filesystem = new Filesystem();
return $filesystem->normalizePath(realpath($vendor_dir));
}
/**
* Gets a consolidated list of file mappings from all allowed packages.
*
* @param \Composer\Package\Package[] $allowed_packages
* A multidimensional array of file mappings, as returned by
* self::getAllowedPackages().
*
* @return \Drupal\Component\Scaffold\Operations\OperationInterface[]
* An array of destination paths => scaffold operation objects.
*/
protected function getFileMappingsFromPackages(array $allowed_packages) {
$file_mappings = [];
foreach ($allowed_packages as $package_name => $package) {
$file_mappings[$package_name] = $this->getPackageFileMappings($package);
}
return $file_mappings;
}
/**
* Gets the array of file mappings provided by a given package.
*
* @param \Composer\Package\PackageInterface $package
* The Composer package from which to get the file mappings.
*
* @return \Drupal\Component\Scaffold\Operations\OperationInterface[]
* An array of destination paths => scaffold operation objects.
*/
protected function getPackageFileMappings(PackageInterface $package) {
$options = $this->manageOptions->packageOptions($package);
if ($options->hasFileMapping()) {
return $this->createScaffoldOperations($package, $options->fileMapping());
}
if (!$options->hasAllowedPackages()) {
$this->io->writeError("The allowed package {$package->getName()} does not provide a file mapping for Composer Scaffold.");
}
return [];
}
/**
* Gets the root package name.
*
* @return string
* The package name of the root project
*/
protected function rootPackageName() {
$root_package = $this->composer->getPackage();
return $root_package->getName();
}
}
<?php
namespace Drupal\Component\Scaffold;
/**
* Injects config values from an associative array into a string.
*/
class Interpolator {
/**
* The character sequence that identifies the start of a token.
*
* @var string
*/
protected $startToken;
/**
* The character sequence that identifies the end of a token.
*
* @var string
*/
protected $endToken;
/**
* The associative array of replacements.
*
* @var array
*/
protected $data = [];
/**
* Interpolator constructor.
*
* @param string $start_token
* The start marker for a token, e.g. '['.
* @param string $end_token
* The end marker for a token, e.g. ']'.
*/
public function __construct($start_token = '\\[', $end_token = '\\]') {
$this->startToken = $start_token;
$this->endToken = $end_token;
}
/**
* Sets the data set to use when interpolating.
*
* @param array $data
* The key:value pairs to use when interpolating.
*
* @return $this
*/
public function setData(array $data) {
$this->data = $data;
return $this;
}
/**
* Adds to the data set to use when interpolating.
*
* @param array $data
* The key:value pairs to use when interpolating.
*
* @return $this
*/
public function addData(array $data) {
$this->data = array_merge($this->data, $data);
return $this;
}
/**
* Replaces tokens in a string with values from an associative array.
*
* Tokens are surrounded by delimiters, e.g. square brackets "[key]". The
* characters that surround the key may be defined when the Interpolator is
* constructed.
*
* Example:
* If the message is 'Hello, [user.name]', then the value of the user.name
* item is fetched from the array, and the token [user.name] is replaced with
* the result.
*
* @param string $message
* Message containing tokens to be replaced.
* @param array $extra
* Data to use for interpolation in addition to whatever was provided to
* self::setData().
* @param string|bool $default
* (optional) The value to substitute for tokens that are not found in the
* data. If FALSE, then missing tokens are not replaced. Defaults to an
* empty string.
*
* @return string
* The message after replacements have been made.
*/
public function interpolate($message, array $extra = [], $default = '') {
$data = $extra + $this->data;
$replacements = $this->replacements($message, $data, $default);
return strtr($message, $replacements);
}
/**
* Finds the tokens that exist in a message and builds a replacement array.
*
* All of the replacements in the data array are looked up given the token
* keys from the provided message. Keys that do not exist in the configuration
* are replaced with the default value.
*
* @param string $message
* String with tokens.
* @param array $data
* Data to use for interpolation.
* @param string $default
* (optional) The value to substitute for tokens that are not found in the
* data. If FALSE, then missing tokens are not replaced. Defaults to an
* empty string.
*
* @return string[]
* An array of replacements to make. Keyed by tokens and the replacements
* are the values.
*/
protected function replacements($message, array $data, $default = '') {
$tokens = $this->findTokens($message);
$replacements = [];
foreach ($tokens as $sourceText => $key) {
$replacement_text = array_key_exists($key, $data) ? $data[$key] : $default;
if ($replacement_text !== FALSE) {
$replacements[$sourceText] = $replacement_text;
}
}
return $replacements;
}
/**
* Finds all of the tokens in the provided message.
*
* @param string $message
* String with tokens.
*
* @return string[]
* map of token to key, e.g. {{key}} => key
*/
protected function findTokens($message) {
$reg_ex = '#' . $this->startToken . '([a-zA-Z0-9._-]+)' . $this->endToken . '#';
if (!preg_match_all($reg_ex, $message, $matches, PREG_SET_ORDER)) {
return [];
}
$tokens = [];
foreach ($matches as $matchSet) {
list($sourceText, $key) = $matchSet;
$tokens[$sourceText] = $key;
}
return $tokens;
}
}
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.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
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.
<signature of Ty Coon>, 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.
<?php
namespace Drupal\Component\Scaffold;
use Composer\IO\IOInterface;
/**
* Manage the .gitignore file.
*/
class ManageGitIgnore {
/**
* Composer's I/O service.
*
* @var \Composer\IO\IOInterface
*/
protected $io;
/**
* The directory where the project is located.
*
* @var string
*/
protected $dir;
/**
* ManageGitIgnore constructor.
*
* @param string $dir
* The directory where the project is located.
*/
public function __construct(IOInterface $io, $dir) {
$this->io = $io;
$this->dir = $dir;
}
/**
* Manages gitignore files.
*
* @param \Drupal\Component\Scaffold\Operations\ScaffoldResult[] $files
* A list of scaffold results, each of which holds a path and whether
* or not that file is managed.
* @param \Drupal\Component\Scaffold\ScaffoldOptions $options
* Configuration options from the composer.json extras section.
*/
public function manageIgnored(array $files, ScaffoldOptions $options) {
if (!$this->managementOfGitIgnoreEnabled($options)) {
return;
}
// Accumulate entries to add to .gitignore, sorted into buckets based on the
// location of the .gitignore file the entry should be added to.
$add_to_git_ignore = [];
foreach ($files as $scaffoldResult) {
$path = $scaffoldResult->destination()->fullPath();
$is_ignored = Git::checkIgnore($this->io, $path, $this->dir);
if (!$is_ignored) {
$is_tracked = Git::checkTracked($this->io, $path, $this->dir);
if (!$is_tracked && $scaffoldResult->isManaged()) {
$dir = realpath(dirname($path));
$name = basename($path);
$add_to_git_ignore[$dir][] = $name;
}
}
}
// Write out the .gitignore files one at a time.
foreach ($add_to_git_ignore as $dir => $entries) {
$this->addToGitIgnore($dir, $entries);
}
}
/**
* Determines whether we should manage gitignore files.
*
* @param \Drupal\Component\Scaffold\ScaffoldOptions $options
* Configuration options from the composer.json extras section.
*
* @return bool
* Whether or not gitignore files should be managed.
*/
protected function managementOfGitIgnoreEnabled(ScaffoldOptions $options) {
// If the composer.json stipulates whether gitignore is managed or not, then
// follow its recommendation.
if ($options->hasGitIgnore()) {
return $options->gitIgnore();
}
// Do not manage .gitignore if there is no repository here.
if (!Git::isRepository($this->io, $this->dir)) {
return FALSE;
}
// If the composer.json did not specify whether or not .gitignore files
// should be managed, then manage them if the vendor directory is not
// committed.
return !Git::checkTracked($this->io, 'vendor', $this->dir);
}
/**
* Adds a set of entries to the specified .gitignore file.
*
* @param string $dir
* Path to directory where gitignore should be written.
* @param string[] $entries
* Entries to write to .gitignore file.
*/
protected function addToGitIgnore($dir, array $entries) {
sort($entries);
$git_ignore_path = $dir . '/.gitignore';
$contents = '';
// Appending to existing .gitignore files.
if (file_exists($git_ignore_path)) {
$contents = file_get_contents($git_ignore_path);
if (!empty($contents) && substr($contents, -1) != "\n") {
$contents .= "\n";
}
}
$contents .= implode("\n", $entries);
file_put_contents($git_ignore_path, $contents);
}
}
<?php
namespace Drupal\Component\Scaffold;
use Composer\Composer;
use Composer\Package\PackageInterface;
use Composer\Util\Filesystem;
/**
* Per-project options from the 'extras' section of the composer.json file.
*
* Projects that describe scaffold files do so via their scaffold options.
* This data is pulled from the 'composer-scaffold' portion of the extras
* section of the project data.
*/
class ManageOptions {
/**
* The Composer service.
*
* @var \Composer\Composer
*/
protected $composer;
/**
* ManageOptions constructor.
*
* @param \Composer\Composer $composer
* The Composer service.
*/
public function __construct(Composer $composer) {
$this->composer = $composer;
}
/**
* Gets the root-level scaffold options for this project.
*
* @return \Drupal\Component\Scaffold\ScaffoldOptions
* The scaffold options object.
*/
public function getOptions() {
return $this->packageOptions($this->composer->getPackage());
}
/**
* Gets the scaffold options for the stipulated project.
*
* @param \Composer\Package\PackageInterface $package
* The package to fetch the scaffold options from.
*
* @return \Drupal\Component\Scaffold\ScaffoldOptions
* The scaffold options object.
*/
public function packageOptions(PackageInterface $package) {
return ScaffoldOptions::create($package->getExtra());
}
/**
* Creates an interpolator for the 'locations' element.
*
* The interpolator returned will replace a path string with the tokens
* defined in the 'locations' element.
*
* Note that only the root package may define locations.
*
* @return \Drupal\Component\Scaffold\Interpolator
* Interpolator that will do replacements in a string using tokens in
* 'locations' element.
*/
public function getLocationReplacements() {
return (new Interpolator())->setData($this->ensureLocations());
}
/**
* Ensures that all of the locations defined in the scaffold files exist.
*
* Create them on the filesystem if they do not.
*/
protected function ensureLocations() {
$fs = new Filesystem();
$locations = $this->getOptions()->locations() + ['web_root' => './'];
$locations = array_map(function ($location) use ($fs) {
$fs->ensureDirectoryExists($location);
$location = realpath($location);
return $location;
}, $locations);
return $locations;
}
}
<?php
namespace Drupal\Component\Scaffold\Operations;
use Composer\IO\IOInterface;
use Drupal\Component\Scaffold\ScaffoldFilePath;
use Drupal\Component\Scaffold\ScaffoldOptions;
/**
* Scaffold operation to add to the beginning and/or end of a scaffold file.
*/
class AppendOp implements OperationInterface, ConjoinableInterface {
/**
* Identifies Append operations.
*/
const ID = 'append';
/**
* Path to the source file to prepend, if any.
*
* @var \Drupal\Component\Scaffold\ScaffoldFilePath
*/
protected $prepend;
/**
* Path to the source file to append, if any.
*
* @var \Drupal\Component\Scaffold\ScaffoldFilePath
*/
protected $append;
/**
* Constructs an AppendOp.
*
* @param \Drupal\Component\Scaffold\ScaffoldFilePath $prepend_path
* The relative path to the prepend file.
* @param \Drupal\Component\Scaffold\ScaffoldFilePath $append_path
* The relative path to the append file.
*/
public function __construct(ScaffoldFilePath $prepend_path = NULL, ScaffoldFilePath $append_path = NULL) {
$this->prepend = $prepend_path;
$this->append = $append_path;
}
/**
* {@inheritdoc}
*/
public function process(ScaffoldFilePath $destination, IOInterface $io, ScaffoldOptions $options) {
$destination_path = $destination->fullPath();
if (!file_exists($destination_path)) {
throw new \RuntimeException($destination->getInterpolator()->interpolate("Cannot append/prepend because no prior package provided a scaffold file at that [dest-rel-path]."));
}
$interpolator = $destination->getInterpolator();
// Fetch the prepend contents, if provided.
$prepend_contents = '';
if (!empty($this->prepend)) {
$this->prepend->addInterpolationData($interpolator, 'prepend');
$prepend_contents = file_get_contents($this->prepend->fullPath()) . "\n";
$io->write($interpolator->interpolate(" - Prepend to <info>[dest-rel-path]</info> from <info>[prepend-rel-path]</info>"));
}
// Fetch the append contents, if provided.
$append_contents = '';
if (!empty($this->append)) {
$this->append->addInterpolationData($interpolator, 'append');
$append_contents = "\n" . file_get_contents($this->append->fullPath());
$io->write($interpolator->interpolate(" - Append to <info>[dest-rel-path]</info> from <info>[append-rel-path]</info>"));
}
if (!empty(trim($prepend_contents)) || !empty(trim($append_contents))) {
// None of our asset files are very large, so we will load each one into
// memory for processing.
$original_contents = file_get_contents($destination_path);
// Write the appended and prepended contents back to the file.
$altered_contents = $prepend_contents . $original_contents . $append_contents;
file_put_contents($destination_path, $altered_contents);
}
else {
$io->write($interpolator->interpolate(" - Keep <info>[dest-rel-path]</info> unchanged: no content to prepend / append was provided."));
}
return new ScaffoldResult($destination, TRUE);
}
}
<?php
namespace Drupal\Component\Scaffold\Operations;
/**
* Marker interface indicating that operation is conjoinable.
*
* A conjoinable operation is one that runs in addition to any previous
* operation defined at the same destination path. Operations that are
* not conjoinable simply replace anything at the same destination path.
*/
interface ConjoinableInterface {
}
<?php
namespace Drupal\Component\Scaffold\Operations;
use Composer\IO\IOInterface;
use Drupal\Component\Scaffold\ScaffoldFilePath;
use Drupal\Component\Scaffold\ScaffoldOptions;
/**
* Joins two operations on the same file into a single operation.
*/
class ConjunctionOp implements OperationInterface {
/**
* The first operation.
*
* @var \Drupal\Component\Scaffold\Operations\OperationInterface
*/
protected $firstOperation;
/**
* The second operation.
*
* @var \Drupal\Component\Scaffold\Operations\OperationInterface
*/
protected $secondOperation;
/**
* ConjunctionOp constructor.
*
* @param \Drupal\Component\Scaffold\Operations\OperationInterface $first_operation
* @param \Drupal\Component\Scaffold\Operations\OperationInterface $second_operation
*/
public function __construct(OperationInterface $first_operation, OperationInterface $second_operation) {
$this->firstOperation = $first_operation;
$this->secondOperation = $second_operation;
}
/**
* {@inheritdoc}
*/
public function process(ScaffoldFilePath $destination, IOInterface $io, ScaffoldOptions $options) {
$destination_path = $destination->fullPath();
// First, scaffold the original file. Disable symlinking, because we
// need a copy of the file if we're going to append / prepend to it.
@unlink($destination_path);
$this->firstOperation->process($destination, $io, $options->overrideSymlink(FALSE));
return $this->secondOperation->process($destination, $io, $options);
}
}
<?php
namespace Drupal\Component\Scaffold\Operations;
/**
* Holds parameter data for operation objects during operation creation only.
*/
class OperationData {
const MODE = 'mode';
const PATH = 'path';
const OVERWRITE = 'overwrite';
const PREPEND = 'prepend';
const APPEND = 'append';
/**
* The parameter data.
*
* @var array
*/
protected $data;
/**
* The destination path
*
* @var string
*/
protected $destination;
/**
* OperationData constructor.
*
* @param mixed $data
* The raw data array to wrap.
*/
public function __construct($destination, $data) {
$this->destination = $destination;
$this->data = $this->normalizeScaffoldMetadata($destination, $data);
}
/**
* Gets the destination path that this operation data is associated with.
*
* @return string
* The destination path for the scaffold result.
*/
public function destination() {
return $this->destination;
}
/**
* Gets operation mode
*
* @return string
* Operation mode.
*/
public function mode() {
return $this->data[self::MODE];
}
/**
* Checks if path exists
*
* @return bool
* Returns true if path exists
*/
public function hasPath() {
return isset($this->data[self::PATH]);
}
/**
* Gets path
*
* @return string
* The path.
*/
public function path() {
return $this->data[self::PATH];
}
/**
* Determines overwrite.
*
* @return bool
* Returns true if overwrite mode was selected.
*/
public function overwrite() {
return isset($this->data[self::OVERWRITE]) ? $this->data[self::OVERWRITE] : TRUE;
}
/**
* Checks if prepend path exists.
*
* @return bool
* Returns true if prepend exists.
*/
public function hasPrepend() {
return isset($this->data[self::PREPEND]);
}
/**
* Gets prepend path.
*
* @return string
* Path to prepend data
*/
public function prepend() {
return $this->data[self::PREPEND];
}
/**
* Checks if append path exists.
*
* @return bool
* Returns true if prepend exists.
*/
public function hasAppend() {
return isset($this->data[self::APPEND]);
}
/**
* Gets append path.
*
* @return string
* Path to append data
*/
public function append() {
return $this->data[self::APPEND];
}
/**
* Normalizes metadata by converting literal values into arrays.
*
* Conversions performed include:
* - Boolean 'false' means "skip".
* - A string means "replace", with the string value becoming the path.
*
* @param string $destination
* The destination path for the scaffold file.
* @param mixed $value
* The metadata for this operation object, which varies by operation type.
*
* @return array
* Normalized scaffold metadata.
*/
protected function normalizeScaffoldMetadata($destination, $value) {
if (is_bool($value)) {
if (!$value) {
return [self::MODE => SkipOp::ID];
}
throw new \RuntimeException("File mapping {$destination} cannot be given the value 'true'.");
}
if (empty($value)) {
throw new \RuntimeException("File mapping {$destination} cannot be empty.");
}
if (is_string($value)) {
$value = [self::PATH => $value];
}
// If there is no 'mode', but there is an 'append' or a 'prepend' path,
// then the mode is 'append' (append + prepend).
if (!isset($value[self::MODE]) && (isset($value[self::APPEND]) || isset($value[self::PREPEND]))) {
$value[self::MODE] = AppendOp::ID;
}
// If there is no 'mode', then the default is 'replace'.
if (!isset($value[self::MODE])) {
$value[self::MODE] = ReplaceOp::ID;
}
return $value;
}
}
<?php
namespace Drupal\Component\Scaffold\Operations;
use Composer\Composer;
use Composer\Package\PackageInterface;
use Drupal\Component\Scaffold\ScaffoldFilePath;
/**
* Create Scaffold operation objects based on provided metadata.
*/
class OperationFactory {
/**
* The Composer service.
*
* @var \Composer\Composer
*/
protected $composer;
/**
* OperationFactory constructor.
*
* @param \Composer\Composer $composer
* Reference to the 'Composer' object, since the Scaffold Operation Factory
* is also responsible for evaluating relative package paths as it creates
* scaffold operations.
*/
public function __construct(Composer $composer) {
$this->composer = $composer;
}
/**
* Creates a scaffolding operation object as determined by the metadata.
*
* @param \Composer\Package\PackageInterface $package
* The package that relative paths will be relative from.
* @param OperationData $operation_data
* The parameter data for this operation object; varies by operation type.
*
* @return \Drupal\Component\Scaffold\Operations\OperationInterface
* The scaffolding operation object (skip, replace, etc.)
*
* @throws \RuntimeException
* Exception thrown when parameter data does not identify a known scaffol
* operation.
*/
public function create(PackageInterface $package, OperationData $operation_data) {
switch ($operation_data->mode()) {
case SkipOp::ID:
return new SkipOp();
case ReplaceOp::ID:
return $this->createReplaceOp($package, $operation_data);
case AppendOp::ID:
return $this->createAppendOp($package, $operation_data);
}
throw new \RuntimeException("Unknown scaffold operation mode <comment>{$operation_data->mode()}</comment>.");
}
/**
* Creates a 'replace' scaffold op.
*
* Replace ops may copy or symlink, depending on settings.
*
* @param \Composer\Package\PackageInterface $package
* The package that relative paths will be relative from.
* @param OperationData $operation_data
* The parameter data for this operation object, i.e. the relative 'path'.
*
* @return \Drupal\Component\Scaffold\Operations\OperationInterface
* A scaffold replace operation object.
*/
protected function createReplaceOp(PackageInterface $package, OperationData $operation_data) {
if (!$operation_data->hasPath()) {
throw new \RuntimeException("'path' component required for 'replace' operations.");
}
$package_name = $package->getName();
$package_path = $this->getPackagePath($package);
$source = ScaffoldFilePath::sourcePath($package_name, $package_path, $operation_data->destination(), $operation_data->path());
$op = new ReplaceOp($source, $operation_data->overwrite());
return $op;
}
/**
* Creates an 'append' (or 'prepend') scaffold op.
*
* @param \Composer\Package\PackageInterface $package
* The package that relative paths will be relative from.
* @param OperationData $operation_data
* The parameter data for this operation object, i.e. the relative 'path'.
*
* @return \Drupal\Component\Scaffold\Operations\OperationInterface
* A scaffold replace operation object.
*/
protected function createAppendOp(PackageInterface $package, OperationData $operation_data) {
$package_name = $package->getName();
$package_path = $this->getPackagePath($package);
$prepend_source_file = NULL;
$append_source_file = NULL;
if ($operation_data->hasPrepend()) {
$prepend_source_file = ScaffoldFilePath::sourcePath($package_name, $package_path, $operation_data->destination(), $operation_data->prepend());
}
if ($operation_data->hasAppend()) {
$append_source_file = ScaffoldFilePath::sourcePath($package_name, $package_path, $operation_data->destination(), $operation_data->append());
}
$op = new AppendOp($prepend_source_file, $append_source_file);
return $op;
}
/**
* Gets the file path of a package.
*
* Note that if we call getInstallPath on the root package, we get the
* wrong answer (the installation manager thinks our package is in
* vendor). We therefore add special checking for this case.
*
* @param \Composer\Package\PackageInterface $package
* The package.
*
* @return string
* The file path.
*/
protected function getPackagePath(PackageInterface $package) {
if ($package->getName() == $this->composer->getPackage()->getName()) {
// This will respect the --working-dir option if Composer is invoked with
// it. There is no API or method to determine the filesystem path of
// a package's composer.json file.
return getcwd();
}
return $this->composer->getInstallationManager()->getInstallPath($package);
}
}
<?php
namespace Drupal\Component\Scaffold\Operations;
use Composer\IO\IOInterface;
use Drupal\Component\Scaffold\ScaffoldFilePath;
use Drupal\Component\Scaffold\ScaffoldOptions;
/**
* Interface for scaffold operation objects.
*/
interface OperationInterface {
/**
* Process this scaffold operation.
*
* @param \Drupal\Component\Scaffold\ScaffoldFilePath $destination
* Scaffold file's destination path.
* @param \Composer\IO\IOInterface $io
* IOInterface to write to.
* @param \Drupal\Component\Scaffold\ScaffoldOptions $options
* Various options that may alter the behavior of the operation.
*
* @return \Drupal\Component\Scaffold\Operations\ScaffoldResult
* Result of the scaffolding operation.
*/
public function process(ScaffoldFilePath $destination, IOInterface $io, ScaffoldOptions $options);
}
<?php
namespace Drupal\Component\Scaffold\Operations;
use Composer\IO\IOInterface;
use Composer\Util\Filesystem;
use Drupal\Component\Scaffold\ScaffoldFilePath;
use Drupal\Component\Scaffold\ScaffoldOptions;
/**
* Scaffold operation to copy or symlink from source to destination.
*/
class ReplaceOp implements OperationInterface {
/**
* Identifies Replace operations.
*/
const ID = 'replace';
/**
* The relative path to the source file.
*
* @var \Drupal\Component\Scaffold\ScaffoldFilePath
*/
protected $source;
/**
* Whether to overwrite existing files.
*
* @var bool
*/
protected $overwrite;
/**
* Constructs a ReplaceOp.
*
* @param \Drupal\Component\Scaffold\ScaffoldFilePath $sourcePath
* The relative path to the source file.
* @param bool $overwrite
* Whether to allow this scaffold file to overwrite files already at
* the destination. Defaults to TRUE.
*/
public function __construct(ScaffoldFilePath $sourcePath, $overwrite = TRUE) {
$this->source = $sourcePath;
$this->overwrite = $overwrite;
}
/**
* {@inheritdoc}
*/
public function process(ScaffoldFilePath $destination, IOInterface $io, ScaffoldOptions $options) {
$fs = new Filesystem();
$destination_path = $destination->fullPath();
// Do nothing if overwrite is 'false' and a file already exists at the
// destination.
if ($this->overwrite === FALSE && file_exists($destination_path)) {
$interpolator = $destination->getInterpolator();
$io->write($interpolator->interpolate(" - Skip <info>[dest-rel-path]</info> because it already exists and overwrite is <comment>false</comment>."));
return new ScaffoldResult($destination, FALSE);
}
// Get rid of the destination if it exists, and make sure that
// the directory where it's going to be placed exists.
$fs->remove($destination_path);
$fs->ensureDirectoryExists(dirname($destination_path));
if ($options->symlink()) {
return $this->symlinkScaffold($destination, $io);
}
return $this->copyScaffold($destination, $io);
}
/**
* Copies the scaffold file.
*
* @param \Drupal\Component\Scaffold\ScaffoldFilePath $destination
* Scaffold file to process.
* @param \Composer\IO\IOInterface $io
* IOInterface to writing to.
*
* @return \Drupal\Component\Scaffold\Operations\ScaffoldResult
* The scaffold result.
*/
protected function copyScaffold(ScaffoldFilePath $destination, IOInterface $io) {
$interpolator = $destination->getInterpolator();
$this->source->addInterpolationData($interpolator);
$fs = new Filesystem();
$success = $fs->copy($this->source->fullPath(), $destination->fullPath());
if (!$success) {
throw new \RuntimeException($interpolator->interpolate("Could not copy source file <info>[src-rel-path]</info> to <info>[dest-rel-path]</info>!"));
}
$io->write($interpolator->interpolate(" - Copy <info>[dest-rel-path]</info> from <info>[src-rel-path]</info>"));
return new ScaffoldResult($destination, $this->overwrite);
}
/**
* Symlinks the scaffold file.
*
* @param \Drupal\Component\Scaffold\ScaffoldFilePath $destination
* Scaffold file to process.
* @param \Composer\IO\IOInterface $io
* IOInterface to writing to.
*
* @return \Drupal\Component\Scaffold\Operations\ScaffoldResult
* The scaffold result.
*/
protected function symlinkScaffold(ScaffoldFilePath $destination, IOInterface $io) {
$interpolator = $destination->getInterpolator();
try {
$fs = new Filesystem();
$fs->relativeSymlink($this->source->fullPath(), $destination->fullPath());
}
catch (\Exception $e) {
throw new \RuntimeException($interpolator->interpolate("Could not symlink source file <info>[src-rel-path]</info> to <info>[dest-rel-path]</info>!"), [], $e);
}
$io->write($interpolator->interpolate(" - Link <info>[dest-rel-path]</info> from <info>[src-rel-path]</info>"));
return new ScaffoldResult($destination, $this->overwrite);
}
}
<?php
namespace Drupal\Component\Scaffold\Operations;
use Composer\IO\IOInterface;
use Drupal\Component\Scaffold\Interpolator;
use Drupal\Component\Scaffold\ScaffoldFileInfo;
use Drupal\Component\Scaffold\ScaffoldFilePath;
use Drupal\Component\Scaffold\ScaffoldOptions;
/**
* Collection of scaffold files.
*/
class ScaffoldFileCollection implements \IteratorAggregate {
/**
* Nested list of all scaffold files.
*
* The top level array maps from the package name to the collection of
* scaffold files provided by that package. Each collection of scaffold files
* is keyed by destination path.
*
* @var \Drupal\Component\Scaffold\ScaffoldFileInfo[][]
*/
protected $scaffoldFilesByProject = [];
/**
* ScaffoldFileCollection constructor.
*
* @param array $file_mappings
* A multidimensional array of file mappings.
* @param \Drupal\Component\Scaffold\Interpolator $location_replacements
* An object with the location mappings (e.g. [web-root]).
*/
public function __construct(array $file_mappings, Interpolator $location_replacements) {
// Collection of all destination paths to be scaffolded. Used to determine
// when two project scaffold the same file and we have to skip or use a
// ConjunctionOp.
$scaffoldFiles = [];
// Build the list of ScaffoldFileInfo objects by project.
foreach ($file_mappings as $package_name => $package_file_mappings) {
foreach ($package_file_mappings as $destination_rel_path => $op) {
$destination = ScaffoldFilePath::destinationPath($package_name, $destination_rel_path, $location_replacements);
// If there was already a scaffolding operation happening at this path,
// and the new operation is Conjoinable, then use a ConjunctionOp to
// join together both operations. This will cause both operations to
// run, one after the other. At the moment, only AppendOp is
// conjoinable; all other operations simply replace anything at the same
// path.
if (isset($scaffoldFiles[$destination_rel_path])) {
$previous_scaffold_file = $scaffoldFiles[$destination_rel_path];
if ($op instanceof ConjoinableInterface) {
$op = new ConjunctionOp($previous_scaffold_file->op(), $op);
}
// Remove the previous op so we only touch the destination once.
$message = " - Skip <info>[dest-rel-path]</info>: overridden in <comment>{$package_name}</comment>";
$this->scaffoldFilesByProject[$previous_scaffold_file->packageName()][$destination_rel_path] = new ScaffoldFileInfo($destination, new SkipOp($message));
}
$scaffold_file = new ScaffoldFileInfo($destination, $op);
$scaffoldFiles[$destination_rel_path] = $scaffold_file;
$this->scaffoldFilesByProject[$package_name][$destination_rel_path] = $scaffold_file;
}
}
}
/**
* {@inheritdoc}
*/
public function getIterator() {
return new \RecursiveArrayIterator($this->scaffoldFilesByProject, \RecursiveArrayIterator::CHILD_ARRAYS_ONLY);
}
/**
* Processes the iterator created by ScaffoldFileCollection::create().
*
* @param \Drupal\Component\Scaffold\Operations\ScaffoldFileCollection $collection
* The iterator to process.
* @param \Composer\IO\IOInterface $io
* The Composer IO object.
* @param \Drupal\Component\Scaffold\ScaffoldOptions $scaffold_options
* The scaffold options.
*
* @return \Drupal\Component\Scaffold\Operations\ScaffoldResult[]
* The results array.
*/
public static function process(ScaffoldFileCollection $collection, IOInterface $io, ScaffoldOptions $scaffold_options) {
$results = [];
foreach ($collection as $project_name => $scaffold_files) {
$io->write("Scaffolding files for <comment>{$project_name}</comment>:");
foreach ($scaffold_files as $scaffold_file) {
$results[$scaffold_file->destination()->relativePath()] = $scaffold_file->process($io, $scaffold_options);
}
}
return $results;
}
}
<?php
namespace Drupal\Component\Scaffold\Operations;
use Drupal\Component\Scaffold\ScaffoldFilePath;
/**
* Record the result of a scaffold operation.
*/
class ScaffoldResult {
/**
* The path to the scaffold file that was processed.
*
* @var \Drupal\Component\Scaffold\ScaffoldFilePath
*/
protected $destination;
/**
* Indicates if this scaffold file is managed by the scaffold command.
*
* @var bool
*/
protected $managed;
/**
* ScaffoldResult constructor.
*
* @param \Drupal\Component\Scaffold\ScaffoldFilePath $destination
* The path to the scaffold file that was processed.
* @param bool $isManaged
* (optional) Whether this result is managed. Defaults to FALSE.
*/
public function __construct(ScaffoldFilePath $destination, $isManaged = FALSE) {
$this->destination = $destination;
$this->managed = $isManaged;
}
/**
* Determines whether this scaffold file is managed.
*
* @return bool
* TRUE if this scaffold file is managed, FALSE if not.
*/
public function isManaged() {
return $this->managed;
}
/**
* Gets the destination scaffold file that this result refers to.
*
* @return \Drupal\Component\Scaffold\ScaffoldFilePath
* The destination path for the scaffold result.
*/
public function destination() {
return $this->destination;
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment