io); $composerData = $composerFile->read(); $installedFiles = isset($composerData['extra'][$registrationKey]) ? $composerData['extra'][$registrationKey] : []; // Load all project files $fileIterator = $this->getFileIterator($sourceRoot, $filePatterns); $any = false; foreach ($fileIterator as $path => $info) { // Write header on first file if (!$any) { $this->io->write("Installing {$name} files for recipe {$recipe}:"); $any = true; } // Install this file $relativePath = $this->installProjectFile($sourceRoot, $destinationRoot, $path, $installedFiles); // Add file to installed (even if already exists) if (!in_array($relativePath, $installedFiles ?? [])) { $installedFiles[] = $relativePath; } } // If any files are written, modify composer.json with newly installed files if ($this->hasWrittenFiles) { sort($installedFiles); if (!isset($composerData['extra'])) { $composerData['extra'] = []; } $composerData['extra'][$registrationKey] = $installedFiles; $composerFile->write($composerData); // Reset the variable so that we can try this trick again later $this->hasWrittenFiles = false; } } /** * @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 $relativePath = substr($sourcePath ?? '', strlen($sourceRoot ?? '') + 1); // Name path without leading '/' // Get destination path $relativeDestination = $this->rewriteFilePath($destinationRoot, $relativePath); $destination = $destinationRoot . DIRECTORY_SEPARATOR . $relativeDestination; // Check if file exists if (file_exists($destination ?? '')) { if (file_get_contents($destination ?? '') === file_get_contents($sourcePath ?? '')) { $this->io->write( " - Skipping $relativePath (existing, but unchanged)" ); } else { $this->io->write( " - Skipping $relativePath (existing and modified in project)" ); } } elseif ( in_array($relativePath, $installedFiles ?? []) || in_array($relativeDestination, $installedFiles ?? []) ) { // Don't re-install previously installed files that have been deleted $this->io->write( " - Skipping $relativePath (previously installed)" ); } else { $this->io->write(" - Copying $relativePath"); $this->filesystem->ensureDirectoryExists(dirname($destination ?? '')); copy($sourcePath ?? '', $destination ?? ''); $this->hasWrittenFiles = true; } return $relativePath; } /** * 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 */ protected function getFileIterator($sourceRoot, $patterns) { // Build regexp pattern $expressions = []; foreach ($patterns as $pattern) { $expressions[] = $this->globToRegexp($pattern); } $regExp = '#^' . $this->globToRegexp($sourceRoot . '/') . '((' . implode(')|(', $expressions) . '))$#'; // 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); } /** * Convert glob pattern to regexp * * @param string $glob * @return string */ protected function globToRegexp($glob) { $sourceParts = explode('*', $glob ?? ''); $regexParts = array_map(function ($part) { return preg_quote($part ?? '', '#'); }, $sourceParts ?? []); return implode('(.+)', $regexParts); } /** * @param PackageInterface $package */ public function installLibrary(PackageInterface $package) { // Check if silverstripe-recipe type if ($package->getType() !== RecipePlugin::RECIPE_TYPE) { return; } // Find recipe base dir $recipePath = $this->getInstallPath($package); // Find project path $projectPath = dirname(realpath(Factory::getComposerFile() ?? '') ?? ''); // Find public path $candidatePublicPath = $projectPath . DIRECTORY_SEPARATOR . RecipePlugin::PUBLIC_PATH; $publicPath = is_dir($candidatePublicPath ?? '') ? $candidatePublicPath : $projectPath; // Copy project files to root $name = $package->getName(); $extra = $package->getExtra(); // Install project-files if (isset($extra[RecipePlugin::PROJECT_FILES])) { $this->installProjectFiles( $name, $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' ); } } /** * 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' * * 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 * @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) { if (stripos($relativePath ?? '', $from ?? '') === 0) { return $to . substr($relativePath ?? '', strlen($from ?? '')); } } return $relativePath; } }