2016-09-12 05:35:13 +02:00
|
|
|
<?php
|
|
|
|
|
2016-09-12 05:43:34 +02:00
|
|
|
namespace SilverStripe\RecipePlugin;
|
2016-09-12 05:35:13 +02:00
|
|
|
|
2017-07-17 07:36:08 +02:00
|
|
|
use Composer\Factory;
|
2016-09-12 05:35:13 +02:00
|
|
|
use Composer\Installer\LibraryInstaller;
|
|
|
|
use Composer\Composer;
|
|
|
|
use Composer\IO\IOInterface;
|
2017-07-17 07:36:08 +02:00
|
|
|
use Composer\Json\JsonFile;
|
2016-09-12 05:35:13 +02:00
|
|
|
use Composer\Package\PackageInterface;
|
2016-09-12 07:37:50 +02:00
|
|
|
use FilesystemIterator;
|
|
|
|
use Iterator;
|
|
|
|
use RecursiveDirectoryIterator;
|
|
|
|
use RecursiveIteratorIterator;
|
|
|
|
use RegexIterator;
|
2016-09-12 05:35:13 +02:00
|
|
|
|
2018-04-05 06:35:24 +02:00
|
|
|
class RecipeInstaller extends LibraryInstaller
|
|
|
|
{
|
2022-05-09 23:40:14 +02:00
|
|
|
/**
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
private $hasWrittenFiles = false;
|
2016-09-12 05:35:13 +02:00
|
|
|
|
2018-04-05 06:35:24 +02:00
|
|
|
public function __construct(IOInterface $io, Composer $composer)
|
|
|
|
{
|
2016-09-20 03:40:10 +02:00
|
|
|
parent::__construct($io, $composer, null);
|
2016-09-12 07:37:50 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Install project files in the specified directory
|
|
|
|
*
|
|
|
|
* @param string $recipe Recipe name
|
|
|
|
* @param string $sourceRoot Base of source files (no trailing slash)
|
|
|
|
* @param string $destinationRoot Base of destination directory (no trailing slash)
|
|
|
|
* @param array $filePatterns List of file patterns in wildcard format (e.g. `code/My*.php`)
|
2017-12-19 02:03:21 +01:00
|
|
|
* @param string $registrationKey Registration key for installed files
|
|
|
|
* @param string $name Name of project file type being installed
|
2016-09-12 07:37:50 +02:00
|
|
|
*/
|
2018-04-05 06:35:24 +02:00
|
|
|
protected function installProjectFiles(
|
|
|
|
$recipe,
|
|
|
|
$sourceRoot,
|
|
|
|
$destinationRoot,
|
|
|
|
$filePatterns,
|
|
|
|
$registrationKey,
|
|
|
|
$name = 'project'
|
|
|
|
) {
|
2017-07-17 07:36:08 +02:00
|
|
|
// load composer json data
|
|
|
|
$composerFile = new JsonFile(Factory::getComposerFile(), null, $this->io);
|
|
|
|
$composerData = $composerFile->read();
|
2017-12-19 02:03:21 +01:00
|
|
|
$installedFiles = isset($composerData['extra'][$registrationKey])
|
|
|
|
? $composerData['extra'][$registrationKey]
|
2017-07-17 07:36:08 +02:00
|
|
|
: [];
|
|
|
|
|
|
|
|
// Load all project files
|
2016-09-12 07:37:50 +02:00
|
|
|
$fileIterator = $this->getFileIterator($sourceRoot, $filePatterns);
|
2017-07-10 03:59:00 +02:00
|
|
|
$any = false;
|
2018-04-05 06:35:24 +02:00
|
|
|
foreach ($fileIterator as $path => $info) {
|
|
|
|
// Write header on first file
|
2017-07-10 03:59:00 +02:00
|
|
|
if (!$any) {
|
2017-12-19 02:03:21 +01:00
|
|
|
$this->io->write("Installing {$name} files for recipe <info>{$recipe}</info>:");
|
2017-07-10 03:59:00 +02:00
|
|
|
$any = true;
|
|
|
|
}
|
|
|
|
|
2018-04-05 06:35:24 +02:00
|
|
|
// Install this file
|
|
|
|
$relativePath = $this->installProjectFile($sourceRoot, $destinationRoot, $path, $installedFiles);
|
2017-07-17 07:36:08 +02:00
|
|
|
|
|
|
|
// Add file to installed (even if already exists)
|
2022-04-13 07:43:58 +02:00
|
|
|
if (!in_array($relativePath, $installedFiles ?? [])) {
|
2017-07-17 07:36:08 +02:00
|
|
|
$installedFiles[] = $relativePath;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If any files are written, modify composer.json with newly installed files
|
2022-05-09 23:40:14 +02:00
|
|
|
if ($this->hasWrittenFiles) {
|
2017-07-17 07:36:08 +02:00
|
|
|
sort($installedFiles);
|
|
|
|
if (!isset($composerData['extra'])) {
|
|
|
|
$composerData['extra'] = [];
|
|
|
|
}
|
2017-12-19 02:03:21 +01:00
|
|
|
$composerData['extra'][$registrationKey] = $installedFiles;
|
2017-07-17 07:36:08 +02:00
|
|
|
$composerFile->write($composerData);
|
2022-05-09 23:40:14 +02:00
|
|
|
// Reset the variable so that we can try this trick again later
|
|
|
|
$this->hasWrittenFiles = false;
|
2016-09-12 07:37:50 +02:00
|
|
|
}
|
|
|
|
}
|
2016-09-12 06:04:00 +02:00
|
|
|
|
2018-04-05 06:35:24 +02:00
|
|
|
/**
|
|
|
|
* @param string $sourceRoot Base of source files (no trailing slash)
|
|
|
|
* @param string $destinationRoot Base of destination directory (no trailing slash)
|
|
|
|
* @param string $sourcePath Full filesystem path to the file to copy
|
|
|
|
* @param array $installedFiles List of installed files
|
|
|
|
* @return bool|string
|
|
|
|
*/
|
|
|
|
protected function installProjectFile($sourceRoot, $destinationRoot, $sourcePath, $installedFiles)
|
|
|
|
{
|
|
|
|
// Relative path
|
2022-04-13 07:43:58 +02:00
|
|
|
$relativePath = substr($sourcePath ?? '', strlen($sourceRoot ?? '') + 1); // Name path without leading '/'
|
2018-04-05 06:35:24 +02:00
|
|
|
|
|
|
|
// Get destination path
|
|
|
|
$relativeDestination = $this->rewriteFilePath($destinationRoot, $relativePath);
|
|
|
|
$destination = $destinationRoot . DIRECTORY_SEPARATOR . $relativeDestination;
|
|
|
|
|
|
|
|
// Check if file exists
|
2022-04-13 07:43:58 +02:00
|
|
|
if (file_exists($destination ?? '')) {
|
|
|
|
if (file_get_contents($destination ?? '') === file_get_contents($sourcePath ?? '')) {
|
2018-04-05 06:35:24 +02:00
|
|
|
$this->io->write(
|
|
|
|
" - Skipping <info>$relativePath</info> (<comment>existing, but unchanged</comment>)"
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
$this->io->write(
|
|
|
|
" - Skipping <info>$relativePath</info> (<comment>existing and modified in project</comment>)"
|
|
|
|
);
|
|
|
|
}
|
2022-07-06 04:02:51 +02:00
|
|
|
} elseif (
|
|
|
|
in_array($relativePath, $installedFiles ?? []) ||
|
2022-04-13 07:43:58 +02:00
|
|
|
in_array($relativeDestination, $installedFiles ?? [])
|
|
|
|
) {
|
2018-04-05 06:35:24 +02:00
|
|
|
// Don't re-install previously installed files that have been deleted
|
|
|
|
$this->io->write(
|
|
|
|
" - Skipping <info>$relativePath</info> (<comment>previously installed</comment>)"
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
$this->io->write(" - Copying <info>$relativePath</info>");
|
2022-04-13 07:43:58 +02:00
|
|
|
$this->filesystem->ensureDirectoryExists(dirname($destination ?? ''));
|
|
|
|
copy($sourcePath ?? '', $destination ?? '');
|
2022-05-09 23:40:14 +02:00
|
|
|
$this->hasWrittenFiles = true;
|
2018-04-05 06:35:24 +02:00
|
|
|
}
|
|
|
|
return $relativePath;
|
|
|
|
}
|
|
|
|
|
2016-09-12 07:37:50 +02:00
|
|
|
/**
|
|
|
|
* Get iterator of matching source files to copy
|
|
|
|
*
|
|
|
|
* @param string $sourceRoot Root directory of sources (no trailing slash)
|
|
|
|
* @param array $patterns List of wildcard patterns to match
|
|
|
|
* @return Iterator File iterator, where key is path and value is file info object
|
|
|
|
*/
|
2018-04-05 06:35:24 +02:00
|
|
|
protected function getFileIterator($sourceRoot, $patterns)
|
|
|
|
{
|
2016-09-12 07:37:50 +02:00
|
|
|
// Build regexp pattern
|
|
|
|
$expressions = [];
|
2018-04-05 06:35:24 +02:00
|
|
|
foreach ($patterns as $pattern) {
|
2016-09-12 07:37:50 +02:00
|
|
|
$expressions[] = $this->globToRegexp($pattern);
|
2016-09-12 06:04:00 +02:00
|
|
|
}
|
2022-07-06 04:02:51 +02:00
|
|
|
$regExp = '#^' . $this->globToRegexp($sourceRoot . '/') . '((' . implode(')|(', $expressions) . '))$#';
|
2016-09-12 07:37:50 +02:00
|
|
|
|
|
|
|
// Build directory iterator
|
|
|
|
$directoryIterator = new RecursiveDirectoryIterator(
|
|
|
|
$sourceRoot,
|
|
|
|
FilesystemIterator::SKIP_DOTS
|
|
|
|
| FilesystemIterator::UNIX_PATHS
|
|
|
|
| FilesystemIterator::KEY_AS_PATHNAME
|
|
|
|
| FilesystemIterator::CURRENT_AS_FILEINFO
|
|
|
|
);
|
|
|
|
|
|
|
|
// Return filtered iterator
|
|
|
|
$iterator = new RecursiveIteratorIterator($directoryIterator);
|
|
|
|
return new RegexIterator($iterator, $regExp);
|
|
|
|
}
|
2016-09-12 06:04:00 +02:00
|
|
|
|
2016-09-12 07:37:50 +02:00
|
|
|
/**
|
|
|
|
* Convert glob pattern to regexp
|
|
|
|
*
|
|
|
|
* @param string $glob
|
|
|
|
* @return string
|
|
|
|
*/
|
2018-04-05 06:35:24 +02:00
|
|
|
protected function globToRegexp($glob)
|
|
|
|
{
|
2022-04-13 07:43:58 +02:00
|
|
|
$sourceParts = explode('*', $glob ?? '');
|
2018-04-05 06:35:24 +02:00
|
|
|
$regexParts = array_map(function ($part) {
|
2022-04-13 07:43:58 +02:00
|
|
|
return preg_quote($part ?? '', '#');
|
|
|
|
}, $sourceParts ?? []);
|
2016-09-12 07:37:50 +02:00
|
|
|
return implode('(.+)', $regexParts);
|
2016-09-12 05:35:13 +02:00
|
|
|
}
|
2016-09-20 03:40:10 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @param PackageInterface $package
|
|
|
|
*/
|
|
|
|
public function installLibrary(PackageInterface $package)
|
|
|
|
{
|
2017-07-07 05:47:49 +02:00
|
|
|
// Check if silverstripe-recipe type
|
2017-07-17 07:36:08 +02:00
|
|
|
if ($package->getType() !== RecipePlugin::RECIPE_TYPE) {
|
2017-07-07 05:47:49 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-12-19 02:03:21 +01:00
|
|
|
// Find recipe base dir
|
|
|
|
$recipePath = $this->getInstallPath($package);
|
|
|
|
|
2017-07-17 07:36:08 +02:00
|
|
|
// Find project path
|
2022-04-13 07:43:58 +02:00
|
|
|
$projectPath = dirname(realpath(Factory::getComposerFile() ?? '') ?? '');
|
2017-12-19 02:03:21 +01:00
|
|
|
|
|
|
|
// Find public path
|
|
|
|
$candidatePublicPath = $projectPath . DIRECTORY_SEPARATOR . RecipePlugin::PUBLIC_PATH;
|
2022-04-13 07:43:58 +02:00
|
|
|
$publicPath = is_dir($candidatePublicPath ?? '') ? $candidatePublicPath : $projectPath;
|
2017-07-17 07:36:08 +02:00
|
|
|
|
2016-09-20 03:40:10 +02:00
|
|
|
// Copy project files to root
|
|
|
|
$name = $package->getName();
|
|
|
|
$extra = $package->getExtra();
|
2017-12-19 02:03:21 +01:00
|
|
|
|
|
|
|
// Install project-files
|
2017-07-17 07:36:08 +02:00
|
|
|
if (isset($extra[RecipePlugin::PROJECT_FILES])) {
|
2016-09-20 03:40:10 +02:00
|
|
|
$this->installProjectFiles(
|
|
|
|
$name,
|
2017-12-19 02:03:21 +01:00
|
|
|
$recipePath,
|
|
|
|
$projectPath,
|
|
|
|
$extra[RecipePlugin::PROJECT_FILES],
|
|
|
|
RecipePlugin::PROJECT_FILES_INSTALLED,
|
|
|
|
'project'
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Install public-files
|
|
|
|
if (isset($extra[RecipePlugin::PUBLIC_FILES])) {
|
|
|
|
$this->installProjectFiles(
|
|
|
|
$name,
|
|
|
|
$recipePath . '/' . RecipePlugin::PUBLIC_PATH,
|
|
|
|
$publicPath,
|
|
|
|
$extra[RecipePlugin::PUBLIC_FILES],
|
|
|
|
RecipePlugin::PUBLIC_FILES_INSTALLED,
|
|
|
|
'public'
|
2016-09-20 03:40:10 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2018-04-05 06:35:24 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Perform any file rewrites necessary to a relative path of a file being installed.
|
|
|
|
* E.g. if 'mysite' folder exists, rewrite 'mysite' to 'app' and 'mysite/code' to 'app/src'
|
|
|
|
*
|
2022-10-25 06:23:26 +02:00
|
|
|
* This will be removed in 2.0 as the app folder will be hard coded and no rewrites supported.
|
|
|
|
*
|
|
|
|
* @deprecated 1.2.0 Will be removed without equivalent functionality to replace it
|
2018-04-05 06:35:24 +02:00
|
|
|
* @param string $destinationRoot Project root
|
|
|
|
* @param string $relativePath Relative path to the resource being installed
|
|
|
|
* @return string Relative path we should write to
|
|
|
|
*/
|
|
|
|
protected function rewriteFilePath($destinationRoot, $relativePath)
|
|
|
|
{
|
|
|
|
// If app folder exists, no rewrite
|
|
|
|
if (is_dir($destinationRoot . DIRECTORY_SEPARATOR . 'app')) {
|
|
|
|
return $relativePath;
|
|
|
|
}
|
|
|
|
// if mysite folder does NOT exist, no rewrite
|
|
|
|
if (!is_dir($destinationRoot . DIRECTORY_SEPARATOR . 'mysite')) {
|
|
|
|
return $relativePath;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Return first rewrite
|
|
|
|
$rewrites = [
|
|
|
|
'app/src' => 'mysite/code',
|
|
|
|
'app' => 'mysite',
|
|
|
|
];
|
|
|
|
foreach ($rewrites as $from => $to) {
|
2022-04-13 07:43:58 +02:00
|
|
|
if (stripos($relativePath ?? '', $from ?? '') === 0) {
|
|
|
|
return $to . substr($relativePath ?? '', strlen($from ?? ''));
|
2018-04-05 06:35:24 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return $relativePath;
|
|
|
|
}
|
2016-09-12 05:35:13 +02:00
|
|
|
}
|