Skip to content

Commit

Permalink
Change update strategy (use diff of local repos)
Browse files Browse the repository at this point in the history
  • Loading branch information
stefk committed Feb 16, 2016
1 parent 3eca200 commit 41bffc8
Show file tree
Hide file tree
Showing 14 changed files with 561 additions and 41 deletions.
2 changes: 1 addition & 1 deletion Command/PlatformUpdateCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ protected function execute(InputInterface $input, OutputInterface $output)
$installer = $this->getContainer()->get('claroline.installation.platform_installer');
$installer->setOutput($output);
$installer->setLogger($consoleLogger);
$installer->installFromOperationFile();
$installer->updateFromComposerInfo();

/** @var \Claroline\CoreBundle\Library\Installation\Refresher $refresher */
$refresher = $this->getContainer()->get('claroline.installation.refresher');
Expand Down
19 changes: 19 additions & 0 deletions Library/Installation/ExecutorException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

/*
* This file is part of the Claroline Connect package.
*
* (c) Claroline Consortium <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Claroline\CoreBundle\Library\Installation;

class ExecutorException extends \Exception
{
const REPO_NOT_FOUND = 11;
const REPO_NOT_JSON = 12;
const REPO_NOT_ARRAY = 13;
}
86 changes: 86 additions & 0 deletions Library/Installation/Operation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

/*
* This file is part of the Claroline Connect package.
*
* (c) Claroline Consortium <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Claroline\CoreBundle\Library\Installation;

/**
* Holds the details of an install/update operation, such as the type
* of the operation, the package name, the target version, etc.
*/
class Operation
{
const INSTALL = 'install';
const UPDATE = 'update';

private $type;
private $package;
private $bundleFqcn;
private $fromVersion;
private $toVersion;

public function __construct($type, \stdClass $package, $bundleFqcn)
{
if (!in_array($type, [self::INSTALL, self::UPDATE])) {
throw new \InvalidArgumentException(
'Operation type must be an Operation::* class constant'
);
}

$this->type = $type;
$this->package = $package;
$this->bundleFqcn = $bundleFqcn;
}

public function getType()
{
return $this->type;
}

public function getPackageName()
{
return $this->package->name;
}

public function getPackageType()
{
return $this->package->type;
}

public function getBundleFqcn()
{
return $this->bundleFqcn;
}

public function setFromVersion($version)
{
$this->fromVersion = $version;
}

public function getFromVersion()
{
return $this->fromVersion;
}

public function setToVersion($version)
{
$this->toVersion = $version;
}

public function getToVersion()
{
return $this->toVersion;
}

public function getRawPackage()
{
return $this->package;
}
}
193 changes: 164 additions & 29 deletions Library/Installation/OperationExecutor.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,21 @@

namespace Claroline\CoreBundle\Library\Installation;

use Claroline\BundleRecorder\Detector\Detector;
use Claroline\BundleRecorder\Log\LoggableTrait;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpKernel\KernelInterface;
use Claroline\CoreBundle\Library\Installation\Plugin\Installer;
use Claroline\InstallationBundle\Manager\InstallationManager;
use Claroline\BundleRecorder\Handler\OperationHandler;
use Claroline\BundleRecorder\Operation;
use JMS\DiExtraBundle\Annotation as DI;
use Psr\Log\LoggerInterface;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpKernel\KernelInterface;

/**
* @DI\Service("claroline.installation.operation_executor")
*
* Installs/updates platform bundles as mentioned in the operation
* file (operations.xml) generated during composer execution.
* Installs/updates platform bundles based on the comparison of
* previous and current local composer repositories (i.e. the file
* "vendor/composer/installed.json" and its backup in "app/config").
*/
class OperationExecutor
{
Expand All @@ -33,7 +34,9 @@ class OperationExecutor
private $kernel;
private $baseInstaller;
private $pluginInstaller;
private $operationFile;
private $installedRepoFile;
private $previousRepoFile;
private $detector;

/**
* @DI\InjectParams({
Expand All @@ -51,14 +54,31 @@ public function __construct(
$this->kernel = $kernel;
$this->baseInstaller = $baseInstaller;
$this->pluginInstaller = $pluginInstaller;
$this->previousRepoFile = $this->kernel->getRootDir() . '/config/previous-installed.json';
$this->installedRepoFile = $this->kernel->getRootDir() . '/../vendor/composer/installed.json';
$this->detector = new Detector();
}

/**
* Overrides default local repository files (test purposes).
*
* @param string $previousRepoFile
* @param string $installedRepoFile
*/
public function setRepositoryFiles($previousRepoFile, $installedRepoFile)
{
$this->previousRepoFile = $previousRepoFile;
$this->installedRepoFile = $installedRepoFile;
}

/**
* @param string $operationFile
* Overrides the default bundle detector (test purposes).
*
* @param Detector $detector
*/
public function setOperationFile($operationFile)
public function setBundleDetector(Detector $detector)
{
$this->operationFile = $operationFile;
$this->detector = $detector;
}

/**
Expand All @@ -71,44 +91,143 @@ public function setLogger(LoggerInterface $logger)
$this->pluginInstaller->setLogger($logger);
}

public function execute()
/**
* Builds the list of operations to be executed based on the comparison
* of previous and current installed dependencies.
*
* @return array
*/
public function buildOperationList()
{
$this->operationFile = $this->operationFile ?
$this->operationFile :
$this->kernel->getRootDir() . '/config/operations.xml';
$operationsHandler = new OperationHandler($this->operationFile, $this->logger);
$bundles = $this->getBundlesByFqcn();
$operations = $operationsHandler->getOperations();
$this->log('Building install/update operations list...');

/** @var \Claroline\BundleRecorder\Operation[] $orderedOperations */
$orderedOperations = [];
foreach ($operations as $operation) {
if ($operation->getBundleType() === Operation::BUNDLE_CORE) {
array_unshift($orderedOperations, $operation);
} else {
array_push($orderedOperations, $operation);
$current = $this->openRepository($this->previousRepoFile);
$target = $this->openRepository($this->installedRepoFile);
$operations = [];

foreach ($target as $targetName => $targetPackage) {
if (!isset($current[$targetName])) {
$this->log(" - Installation of {$targetName} required");
$operation = $this->buildOperation(Operation::INSTALL, $targetPackage);
$operations[$operation->getBundleFqcn()] = $operation;
} elseif ($targetPackage->{'version-normalized'}
!== $current[$targetName]->{'version-normalized'}) {
$this->log(" - Update of {$targetName} required");
$operation =$this->buildOperation(Operation::UPDATE, $targetPackage);
$operation->setFromVersion($current[$targetName]->{'version-normalized'});
$operation->setToVersion($targetPackage->{'version-normalized'});
$operations[$operation->getBundleFqcn()] = $operation;
}
}

foreach ($orderedOperations as $operation) {
$installer = $operation->getBundleType() === Operation::BUNDLE_CORE ?
$this->log("Sorting operations...");
$bundles = $this->kernel->getBundles();
$sortedOperations = [];

foreach ($bundles as $bundle) {
$bundleClass = $bundle->getNamespace() ?
$bundle->getNamespace() . '\\' . $bundle->getName() :
$bundle->getName();

if (isset($operations[$bundleClass])) {
$sortedOperations[] = $operations[$bundleClass];
}
}

return $sortedOperations;
}

/**
* Executes a list of install/update operations. Each successful operation
* is followed by an update of the previous local repository, so that the
* process can be resumed after an error (e.g. an error) without triggering
* again already executed operations. When there's no more operation to
* execute, the snapshot of the previous local repository is deleted.
*
* @param Operation[] $operations
* @throws ExecutorException if the the previous repository file is not writable
*/
public function execute(array $operations)
{
$this->log("Executing install/update operations...");

$previousRepo = $this->openRepository($this->previousRepoFile, false);

if (!is_writable($this->previousRepoFile)) {
throw new ExecutorException("'{$this->previousRepoFile}' must be writable");
}

$bundles = $this->getBundlesByFqcn();

foreach ($operations as $operation) {
$installer = $operation->getPackageType() === 'claroline-core' ?
$this->baseInstaller :
$this->pluginInstaller;

if ($operation->getType() === Operation::INSTALL) {
$installer->install($bundles[$operation->getBundleFqcn()]);
$this->updatePreviousRepo($previousRepo, $operation->getRawPackage(), true);
} elseif ($operation->getType() === Operation::UPDATE) {
$installer->update(
$bundles[$operation->getBundleFqcn()],
$operation->getFromVersion(),
$operation->getToVersion()
);
} else {
// remove or disable package
$this->updatePreviousRepo($previousRepo, $operation->getRawPackage());
}
}

rename($this->operationFile, $this->operationFile . '.bup');
$this->log("Removing previous local repository snapshot...");
$filesystem = new Filesystem();
$filesystem->remove($this->previousRepoFile);
}

private function openRepository($repoFile, $filter = true)
{
if (!file_exists($repoFile)) {
throw new ExecutorException(
"Repository file '{$repoFile}' doesn't exist",
ExecutorException::REPO_NOT_FOUND
);
}

$repo = json_decode(file_get_contents($repoFile));

if (json_last_error() !== JSON_ERROR_NONE) {
throw new ExecutorException(
"Repository file '{$repoFile}' isn't valid JSON",
ExecutorException::REPO_NOT_JSON
);
}

if (!is_array($repo)) {
throw new ExecutorException(
"Repository file '{$repoFile}' doesn't contain an array of packages",
ExecutorException::REPO_NOT_ARRAY
);
}

$packages = !$filter ? $repo : array_filter($repo, function ($package) {
return $package->type === 'claroline-core' || $package->type === 'claroline-plugin';
});

$packagesByName = [];

foreach ($packages as $package) {
$packagesByName[$package->name] = $package;
}

return $packagesByName;
}

private function buildOperation($type, \stdClass $package)
{
$vendorDir = $this->kernel->getRootDir() . '/../vendor';
$targetDir = property_exists($package, 'targetDir') ? $package->targetDir : '';
$packageDir = empty($targetDir) ? $package->name : "{$targetDir}/{$package->name}";
$fqcn = $this->detector->detectBundle("{$vendorDir}/{$packageDir}");

return new Operation($type, $package, $fqcn);
}

private function getBundlesByFqcn()
Expand All @@ -121,4 +240,20 @@ private function getBundlesByFqcn()

return $byFqcn;
}

private function updatePreviousRepo(array $repo, \stdClass $package, $add = true)
{
if ($add) {
$repo[] = $package;
} else {
foreach ($repo as $index => $previousPackage) {
if ($previousPackage->name === $package->name) {
$repo[$index] = $package;
}
}
}

$options = JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE;
file_put_contents($this->previousRepoFile, json_encode($repo, $options));
}
}
Loading

0 comments on commit 41bffc8

Please sign in to comment.