diff --git a/src/RecipeCommandBehaviour.php b/src/RecipeCommandBehaviour.php
index aeabd09..6dd51bd 100644
--- a/src/RecipeCommandBehaviour.php
+++ b/src/RecipeCommandBehaviour.php
@@ -2,11 +2,12 @@
namespace SilverStripe\RecipePlugin;
-use BadMethodCallException;
use Composer\Command\RequireCommand;
use Composer\Command\UpdateCommand;
use Composer\Composer;
-use MongoDB\Driver\Exception\InvalidArgumentException;
+use Composer\Factory;
+use Composer\IO\IOInterface;
+use Composer\Json\JsonFile;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\OutputInterface;
@@ -34,50 +35,9 @@ trait RecipeCommandBehaviour
public abstract function resetComposer();
/**
- * Load composer data from the given directory
- *
- * @param string $path
- * @param array|null $default If file doesn't exist use this default. If null, file is mandatory and there is
- * no default
- * @return array
+ * @return IOInterface
*/
- protected function loadComposer($path, $default = null)
- {
- if (!file_exists($path)) {
- if (isset($default)) {
- return $default;
- }
- throw new BadMethodCallException("Could not find " . basename($path));
- }
- $data = json_decode(file_get_contents($path), true);
- if (json_last_error() !== JSON_ERROR_NONE) {
- throw new \LogicException("Invalid composer.json with error: " . json_last_error_msg());
- }
- return $data;
- }
-
- /**
- * Save the given data to the composer file in the given directory
- *
- * @param string $directory
- * @param array $data
- */
- protected function saveComposer($directory, $data)
- {
- $path = $directory.'/composer.json';
- if (!file_exists($path)) {
- throw new BadMethodCallException("Could not find composer.json");
- }
- $content = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
- // Make sure errors are reported
- if (json_last_error()) {
- throw new InvalidArgumentException("Invalid composer.json data with error: " . json_last_error_msg());
- }
- file_put_contents($path, $content);
-
- // Reset composer object
- $this->resetComposer();
- }
+ abstract public function getIO();
/**
* @param OutputInterface $output
@@ -224,36 +184,109 @@ trait RecipeCommandBehaviour
return $returnCode;
}
- // Begin modification of composer.json
- $composerData = $this->loadComposer(getcwd() . '/composer.json');
+ // inline all dependencies inline into composer.json
+ $this->modifyComposer(function ($composerData) use ($output, $recipe, $installedVersion) {
+ // Check previously installed, and currently installed modules
+ $require = isset($composerData['require']) ? $composerData['require'] : [];
+ $previouslyInstalled = isset($composerData['extra'][RecipePlugin::PROJECT_DEPENDENCIES_INSTALLED])
+ ? $composerData['extra'][RecipePlugin::PROJECT_DEPENDENCIES_INSTALLED]
+ : [];
- // Get composer data for both root and newly installed recipe
- $installedRecipe = $this
- ->getComposer()
- ->getRepositoryManager()
- ->getLocalRepository()
- ->findPackage($recipe, '*');
- if ($installedRecipe) {
- $output->writeln("Inlining all dependencies for recipe {$recipe}:");
- foreach ($installedRecipe->getRequires() as $requireName => $require) {
- $requireVersion = $require->getPrettyConstraint();
- $output->writeln(
- " - Inlining {$requireName} ({$requireVersion})"
- );
- $composerData['require'][$requireName] = $requireVersion;
+ // Get composer data for both root and newly installed recipe
+ $installedRecipe = $this
+ ->getComposer()
+ ->getRepositoryManager()
+ ->getLocalRepository()
+ ->findPackage($recipe, '*');
+ if ($installedRecipe) {
+ $output->writeln("Inlining all dependencies for recipe {$recipe}:");
+ foreach ($installedRecipe->getRequires() as $requireName => $requireConstraint) {
+ $requireVersion = $requireConstraint->getPrettyConstraint();
+
+ // If already installed, upgrade
+ if (isset($require[$requireName])) {
+ // Check if upgrade or not
+ $requireInstalledVersion = $require[$requireName];
+ if ($requireInstalledVersion === $requireVersion) {
+ // No need to upgrade
+ $output->writeln(
+ " - Skipping {$requireName} "
+ . "(Already installed as {$requireVersion})"
+ );
+ } else {
+ // Upgrade obsolete version
+ $output->writeln(
+ " - Inlining {$requireName} "
+ . "(Updated to {$requireVersion} from "
+ . "{$requireInstalledVersion})"
+ );
+ $require[$requireName] = $requireVersion;
+ }
+ } elseif (isset($previouslyInstalled[$requireName])) {
+ // Old module, manually removed
+ $output->writeln(
+ " - Skipping {$requireName} (Manually removed from recipe)"
+ );
+ } else {
+ // New module
+ $output->writeln(
+ " - Inlining {$requireName} ({$requireVersion})"
+ );
+ $require[$requireName] = $requireVersion;
+ }
+
+ // note dependency as previously installed
+ $previouslyInstalled[$requireName] = $requireVersion;
+ }
}
- }
- // Move recipe from 'require' to 'provide'
- $installedVersion = $this->findInstalledVersion($recipe) ?: $installedVersion;
- unset($composerData['require'][$recipe]);
- if (!isset($composerData['provide'])) {
- $composerData['provide'] = [];
- }
- $composerData['provide'][$recipe] = $installedVersion;
+ // Add new require / extra-installed
+ $composerData['require'] = $require;
+ if ($previouslyInstalled){
+ if (!isset($composerData['extra'])) {
+ $composerData['extra'] = [];
+ }
+ ksort($previouslyInstalled);
+ $composerData['extra'][RecipePlugin::PROJECT_DEPENDENCIES_INSTALLED] = $previouslyInstalled;
+ }
- // Update composer.json and synchronise composer.lock
- $this->saveComposer(getcwd(), $composerData);
+ // Move recipe from 'require' to 'provide'
+ $installedVersion = $this->findInstalledVersion($recipe) ?: $installedVersion;
+ unset($composerData['require'][$recipe]);
+ if (!isset($composerData['provide'])) {
+ $composerData['provide'] = [];
+ }
+ $composerData['provide'][$recipe] = $installedVersion;
+ return $composerData;
+ });
+
+ // Update synchronise composer.lock
return $this->updateProject($output);
}
+
+ /**
+ * callback to safely modify composer.json data
+ *
+ * @param callable $callable Callable which will safely take and return the composer data.
+ * This should return false if no content changed, or the updated data
+ */
+ protected function modifyComposer($callable)
+ {
+ // Begin modification of composer.json
+ $composerFile = new JsonFile(Factory::getComposerFile(), null, $this->getIO());
+ $composerData = $composerFile->read();
+
+ // Note: Respect call by ref $composerData
+ $result = $callable($composerData);
+ if ($result === false) {
+ return;
+ }
+ if ($result) {
+ $composerData = $result;
+ }
+
+ // Update composer.json and refresh local composer instance
+ $composerFile->write($composerData);
+ $this->resetComposer();
+ }
}
diff --git a/src/RecipeInstaller.php b/src/RecipeInstaller.php
index fefa069..e991767 100644
--- a/src/RecipeInstaller.php
+++ b/src/RecipeInstaller.php
@@ -2,9 +2,11 @@
namespace SilverStripe\RecipePlugin;
+use Composer\Factory;
use Composer\Installer\LibraryInstaller;
use Composer\Composer;
use Composer\IO\IOInterface;
+use Composer\Json\JsonFile;
use Composer\Package\PackageInterface;
use FilesystemIterator;
use Iterator;
@@ -28,11 +30,19 @@ class RecipeInstaller extends LibraryInstaller {
*/
protected function installProjectFiles($recipe, $sourceRoot, $destinationRoot, $filePatterns)
{
+ // load composer json data
+ $composerFile = new JsonFile(Factory::getComposerFile(), null, $this->io);
+ $composerData = $composerFile->read();
+ $installedFiles = isset($composerData['extra'][RecipePlugin::PROJECT_FILES_INSTALLED])
+ ? $composerData['extra'][RecipePlugin::PROJECT_FILES_INSTALLED]
+ : [];
+
+ // Load all project files
$fileIterator = $this->getFileIterator($sourceRoot, $filePatterns);
$any = false;
foreach($fileIterator as $path => $info) {
- $relativePath = substr($path, strlen($sourceRoot));
- $destination = $destinationRoot . $relativePath;
+ $destination = $destinationRoot . substr($path, strlen($sourceRoot));
+ $relativePath = substr($path, strlen($sourceRoot) + 1); // Name path without leading '/'
// Write header
if (!$any) {
@@ -51,11 +61,32 @@ class RecipeInstaller extends LibraryInstaller {
" - Skipping $relativePath (existing and modified in project)"
);
}
+ } elseif (in_array($relativePath, $installedFiles)) {
+ // Don't re-install previously installed files that have been deleted
+ $this->io->write(
+ " - Skipping $relativePath (previously installed)"
+ );
} else {
+ $any++;
$this->io->write(" - Copying $relativePath");
$this->filesystem->ensureDirectoryExists(dirname($destination));
copy($path, $destination);
}
+
+ // 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 ($installedFiles) {
+ sort($installedFiles);
+ if (!isset($composerData['extra'])) {
+ $composerData['extra'] = [];
+ }
+ $composerData['extra'][RecipePlugin::PROJECT_FILES_INSTALLED] = $installedFiles;
+ $composerFile->write($composerData);
}
}
@@ -108,20 +139,22 @@ class RecipeInstaller extends LibraryInstaller {
public function installLibrary(PackageInterface $package)
{
// Check if silverstripe-recipe type
- if ($package->getType() !== 'silverstripe-recipe') {
+ if ($package->getType() !== RecipePlugin::RECIPE_TYPE) {
return;
}
+ // Find project path
+ $destinationPath = dirname(realpath(Factory::getComposerFile()));
+
// Copy project files to root
- $destinationPath = getcwd();
$name = $package->getName();
$extra = $package->getExtra();
- if (isset($extra['project-files'])) {
+ if (isset($extra[RecipePlugin::PROJECT_FILES])) {
$this->installProjectFiles(
$name,
$this->getInstallPath($package),
$destinationPath,
- $extra['project-files']
+ $extra[RecipePlugin::PROJECT_FILES]
);
}
}
diff --git a/src/RecipePlugin.php b/src/RecipePlugin.php
index acee08a..262b1c6 100644
--- a/src/RecipePlugin.php
+++ b/src/RecipePlugin.php
@@ -7,14 +7,15 @@ use Composer\Composer;
use Composer\DependencyResolver\Operation\InstallOperation;
use Composer\DependencyResolver\Operation\UpdateOperation;
use Composer\EventDispatcher\EventSubscriberInterface;
+use Composer\Factory;
use Composer\Installer\PackageEvent;
use Composer\IO\IOInterface;
+use Composer\Json\JsonFile;
use Composer\Package\PackageInterface;
use Composer\Plugin\Capability\CommandProvider;
use Composer\Plugin\Capable;
use Composer\Plugin\PluginInterface;
use Composer\Script\Event;
-use LogicException;
/**
* Register the RecipeInstaller
@@ -23,6 +24,26 @@ use LogicException;
*/
class RecipePlugin implements PluginInterface, EventSubscriberInterface, Capable
{
+ /**
+ * Type of recipe to check for
+ */
+ const RECIPE_TYPE = 'silverstripe-recipe';
+
+ /**
+ * 'extra' key for project files
+ */
+ const PROJECT_FILES = 'project-files';
+
+ /**
+ * 'extra' key for list of project files installed
+ */
+ const PROJECT_FILES_INSTALLED = 'project-files-installed';
+
+ /**
+ * 'extra' key for project dependencies installed
+ */
+ const PROJECT_DEPENDENCIES_INSTALLED = 'project-dependencies-installed';
+
public function activate(Composer $composer, IOInterface $io)
{
}
@@ -57,30 +78,17 @@ class RecipePlugin implements PluginInterface, EventSubscriberInterface, Capable
*/
public function cleanupProject(Event $event)
{
- $path = getcwd() . '/composer.json';
+ $file = new JsonFile(Factory::getComposerFile());
+ $data = $file->read();
- // Load composer data
- $data = json_decode(file_get_contents($path), true);
- if (json_last_error() !== JSON_ERROR_NONE) {
- throw new LogicException("Invalid composer.json with error: " . json_last_error_msg());
- }
-
- // Remove project-files from project
- if (isset($data['extra']['project-files'])) {
- unset($data['extra']['project-files']);
- }
-
- // Clean empty extra key
+ // Remove project-files from project, and any empty extra
+ unset($data['extra']['project-files']);
if (empty($data['extra'])) {
unset($data['extra']);
}
// Save back to composer.json
- $content = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
- if (json_last_error()) {
- throw new LogicException("Invalid composer.json data with error: " . json_last_error_msg());
- }
- file_put_contents($path, $content);
+ $file->write($data);
}
/**