diff --git a/bin/composer b/bin/composer index c9af1edbd..94c5c19b2 100755 --- a/bin/composer +++ b/bin/composer @@ -4,21 +4,43 @@ require __DIR__.'/../tests/bootstrap.php'; use Composer\Composer; -use Composer\Downloader\GitDownloader; -use Composer\Downloader\PearDownloader; -use Composer\Downloader\ZipDownloader; -use Composer\Installer\LibraryInstaller; -use Composer\Console\Application; +use Composer\Installer; +use Composer\Downloader; +use Composer\Repository; +use Composer\Package; +use Composer\Console\Application as ComposerApplication; -setlocale(LC_ALL, 'C'); +// initialize repository manager +$rm = new Repository\RepositoryManager(); +$localRepository = new Repository\WrapperRepository(array( + new Repository\ArrayRepository('.composer/installed.json'), + new Repository\PlatformRepository(), +)); +$rm->setLocalRepository($localRepository); +$rm->setRepository('Packagist', new Repository\ComposerRepository('http://packagist.org')); + +// initialize download manager +$dm = new Downloader\DownloadManager($preferSource = false); +$dm->setDownloader('git', new Downloader\GitDownloader()); +//$dm->setDownloader('pear', new Downloader\PearDownloader()); +//$dm->setDownloader('zip', new Downloader\ZipDownloader()); + +// initialize installation manager +$im = new Installer\InstallationManager(); +$im->setInstaller('library', new Installer\LibraryInstaller('vendor', $dm, $rm->getLocalRepository())); + +// load package +$loader = new Package\Loader\JsonLoader(); +$package = $loader->load('composer.json'); // initialize composer $composer = new Composer(); -$composer->addDownloader('git', new GitDownloader); -$composer->addDownloader('pear', new PearDownloader); -$composer->addDownloader('zip', new ZipDownloader); -$composer->addInstaller('library', new LibraryInstaller); +$composer->setPackage($package); +$composer->setPackageLock(new Package\PackageLock('composer.lock')); +$composer->setRepositoryManager($rm); +$composer->setDownloadManager($dm); +$composer->setInstallationManager($im); // run the command application -$application = new Application($composer); +$application = new ComposerApplication($composer); $application->run(); diff --git a/doc/composer-schema.json b/doc/composer-schema.json index 122de80cd..8d80ccf53 100644 --- a/doc/composer-schema.json +++ b/doc/composer-schema.json @@ -9,7 +9,7 @@ "required": true }, "type": { - "description": "Package type, either 'Library', or the parent project it applies to if it's a plugin for a framework or application (e.g. 'Symfony2', 'Typo3', 'Drupal', ..).", + "description": "Package type, either 'Library', or the parent project it applies to if it's a plugin for a framework or application (e.g. 'Symfony2', 'Typo3', 'Drupal', ..), note that this has to be defined and communicated by any project implementing a custom composer installer, those are just unreliable examples.", "type": "string", "optional": true }, diff --git a/src/Composer/Command/Command.php b/src/Composer/Command/Command.php index 2fb47285c..946d775b8 100644 --- a/src/Composer/Command/Command.php +++ b/src/Composer/Command/Command.php @@ -13,11 +13,17 @@ namespace Composer\Command; use Symfony\Component\Console\Command\Command as BaseCommand; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Composer\DependencyResolver\Request; +use Composer\DependencyResolver\Solver; +use Composer\Installer\Operation; /** * Base class for Composer commands * * @author Ryan Weaver + * @authro Konstantin Kudryashov */ abstract class Command extends BaseCommand { @@ -28,4 +34,4 @@ abstract class Command extends BaseCommand { return $this->getApplication()->getComposer(); } -} \ No newline at end of file +} diff --git a/src/Composer/Command/InstallCommand.php b/src/Composer/Command/InstallCommand.php index 07b85f163..3e63318df 100644 --- a/src/Composer/Command/InstallCommand.php +++ b/src/Composer/Command/InstallCommand.php @@ -12,20 +12,17 @@ namespace Composer\Command; +use Composer\DependencyResolver; use Composer\DependencyResolver\Pool; use Composer\DependencyResolver\Request; -use Composer\DependencyResolver\DefaultPolicy; -use Composer\DependencyResolver\Solver; -use Composer\Repository\PlatformRepository; -use Composer\Package\MemoryPackage; -use Composer\Package\LinkConstraint\VersionConstraint; +use Composer\DependencyResolver\Operation; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; - /** * @author Jordi Boggiano * @author Ryan Weaver + * @author Konstantin Kudryashov */ class InstallCommand extends Command { @@ -48,130 +45,53 @@ EOT protected function execute(InputInterface $input, OutputInterface $output) { - // TODO this needs a parameter to enable installing from source (i.e. git clone, instead of downloading archives) - $sourceInstall = false; + $composer = $this->getComposer(); - $config = $this->loadConfig(); + if ($composer->getPackageLock()->isLocked()) { + $output->writeln('Found lockfile. Reading'); - $output->writeln('Loading repositories'); - - if (isset($config['repositories'])) { - foreach ($config['repositories'] as $name => $spec) { - $this->getComposer()->addRepository($name, $spec); + $installationManager = $composer->getInstallationManager(); + foreach ($composer->getPackageLock()->getLockedPackages() as $package) { + if (!$installationManager->isPackageInstalled($package)) { + $operation = new Operation\InstallOperation($package, 'lock resolving'); + $installationManager->execute($operation); + } } + + return 0; } + // creating repository pool $pool = new Pool; - - $repoInstalled = new PlatformRepository; - $pool->addRepository($repoInstalled); - - // TODO check the lock file to see what's currently installed - // $repoInstalled->addPackage(new MemoryPackage('$Package', '$Version')); - - $output->writeln('Loading package list'); - - foreach ($this->getComposer()->getRepositories() as $repository) { + $pool->addRepository($composer->getRepositoryManager()->getLocalRepository()); + foreach ($composer->getRepositoryManager()->getRepositories() as $repository) { $pool->addRepository($repository); } + // creating requirements request $request = new Request($pool); - - $output->writeln('Building up request'); - - // TODO there should be an update flag or dedicated update command - // TODO check lock file to remove packages that disappeared from the requirements - foreach ($config['require'] as $name => $version) { - if ('latest' === $version) { - $request->install($name); - } else { - preg_match('#^([>=<~]*)([\d.]+.*)$#', $version, $match); - if (!$match[1]) { - $match[1] = '='; - } - $constraint = new VersionConstraint($match[1], $match[2]); - $request->install($name, $constraint); - } + foreach ($composer->getPackage()->getRequires() as $link) { + $request->install($link->getTarget(), $link->getConstraint()); } - $output->writeln('Solving dependencies'); + // prepare solver + $installationManager = $composer->getInstallationManager(); + $localRepo = $composer->getRepositoryManager()->getLocalRepository(); + $policy = new DependencyResolver\DefaultPolicy(); + $solver = new DependencyResolver\Solver($policy, $pool, $localRepo); - $policy = new DefaultPolicy; - $solver = new Solver($policy, $pool, $repoInstalled); - $transaction = $solver->solve($request); - - $lock = array(); - - foreach ($transaction as $task) { - switch ($task['job']) { - case 'install': - $package = $task['package']; - $output->writeln('> Installing '.$package->getPrettyName()); - if ($sourceInstall) { - // TODO - } else { - if ($package->getDistType()) { - $downloaderType = $package->getDistType(); - $type = 'dist'; - } elseif ($package->getSourceType()) { - $output->writeln('Package '.$package->getPrettyName().' has no dist url, installing from source instead.'); - $downloaderType = $package->getSourceType(); - $type = 'source'; - } else { - throw new \UnexpectedValueException('Package '.$package->getPrettyName().' has no source or dist URL.'); - } - $downloader = $this->getComposer()->getDownloader($downloaderType); - $installer = $this->getComposer()->getInstaller($package->getType()); - if (!$installer->install($package, $downloader, $type)) { - throw new \LogicException($package->getPrettyName().' could not be installed.'); - } - } - $lock[$package->getName()] = array('version' => $package->getVersion()); - break; - default: - throw new \UnexpectedValueException('Unhandled job type : '.$task['job']); - } + // solve dependencies and execute operations + foreach ($solver->solve($request) as $operation) { + $installationManager->execute($operation); } + + if (false) { + $composer->getPackageLock()->lock($localRepo->getPackages()); + $output->writeln('> Locked'); + } + + $localRepo->write(); + $output->writeln('> Done'); - - $this->storeLockFile($lock, $output); } - - protected function loadConfig() - { - if (!file_exists('composer.json')) { - throw new \UnexpectedValueException('composer.json config file not found in '.getcwd()); - } - $config = json_decode(file_get_contents('composer.json'), true); - if (!$config) { - switch (json_last_error()) { - case JSON_ERROR_NONE: - $msg = 'No error has occurred, is your composer.json file empty?'; - break; - case JSON_ERROR_DEPTH: - $msg = 'The maximum stack depth has been exceeded'; - break; - case JSON_ERROR_STATE_MISMATCH: - $msg = 'Invalid or malformed JSON'; - break; - case JSON_ERROR_CTRL_CHAR: - $msg = 'Control character error, possibly incorrectly encoded'; - break; - case JSON_ERROR_SYNTAX: - $msg = 'Syntax error'; - break; - case JSON_ERROR_UTF8: - $msg = 'Malformed UTF-8 characters, possibly incorrectly encoded'; - break; - } - throw new \UnexpectedValueException('Incorrect composer.json file: '.$msg); - } - return $config; - } - - protected function storeLockFile(array $content, OutputInterface $output) - { - file_put_contents('composer.lock', json_encode($content, JSON_FORCE_OBJECT)."\n"); - $output->writeln('> composer.lock dumped'); - } -} \ No newline at end of file +} diff --git a/src/Composer/Composer.php b/src/Composer/Composer.php index 4da5de68d..421b4be9f 100644 --- a/src/Composer/Composer.php +++ b/src/Composer/Composer.php @@ -12,125 +12,74 @@ namespace Composer; -use Composer\Downloader\DownloaderInterface; -use Composer\Installer\InstallerInterface; -use Composer\Repository\ComposerRepository; -use Composer\Repository\PlatformRepository; -use Composer\Repository\GitRepository; -use Composer\Repository\PearRepository; +use Composer\Package\PackageInterface; +use Composer\Package\PackageLock; +use Composer\Repository\RepositoryManager; +use Composer\Installer\InstallationManager; +use Composer\Downloader\DownloadManager; /** * @author Jordi Boggiano + * @author Konstantin Kudryashiv */ class Composer { const VERSION = '1.0.0-DEV'; - protected $repositories = array(); - protected $downloaders = array(); - protected $installers = array(); + private $package; + private $lock; - public function __construct() + private $rm; + private $dm; + private $im; + + public function setPackage(PackageInterface $package) { - $this->addRepository('Packagist', array('composer' => 'http://packagist.org')); + $this->package = $package; } - /** - * Add downloader for type - * - * @param string $type - * @param DownloaderInterface $downloader - */ - public function addDownloader($type, DownloaderInterface $downloader) + public function getPackage() { - $type = strtolower($type); - $this->downloaders[$type] = $downloader; + return $this->package; } - /** - * Get type downloader - * - * @param string $type - * - * @return DownloaderInterface - */ - public function getDownloader($type) + public function setPackageLock($lock) { - $type = strtolower($type); - if (!isset($this->downloaders[$type])) { - throw new \UnexpectedValueException('Unknown source type: '.$type); - } - return $this->downloaders[$type]; + $this->lock = $lock; } - /** - * Add installer for type - * - * @param string $type - * @param InstallerInterface $installer - */ - public function addInstaller($type, InstallerInterface $installer) + public function getPackageLock() { - $type = strtolower($type); - $this->installers[$type] = $installer; + return $this->lock; } - /** - * Get type installer - * - * @param string $type - * - * @return InstallerInterface - */ - public function getInstaller($type) + public function setRepositoryManager(RepositoryManager $manager) { - $type = strtolower($type); - if (!isset($this->installers[$type])) { - throw new \UnexpectedValueException('Unknown dependency type: '.$type); - } - return $this->installers[$type]; + $this->rm = $manager; } - public function addRepository($name, $spec) + public function getRepositoryManager() { - if (null === $spec) { - unset($this->repositories[$name]); - } - if (is_array($spec) && count($spec) === 1) { - return $this->repositories[$name] = $this->createRepository($name, key($spec), current($spec)); - } - throw new \UnexpectedValueException('Invalid repositories specification '.json_encode($spec).', should be: {"type": "url"}'); + return $this->rm; } - public function getRepositories() + public function setDownloadManager(DownloadManager $manager) { - return $this->repositories; + $this->dm = $manager; } - public function createRepository($name, $type, $spec) + public function getDownloadManager() { - if (is_string($spec)) { - $spec = array('url' => $spec); - } - $spec['url'] = rtrim($spec['url'], '/'); + return $this->dm; + } - switch ($type) { - case 'git-bare': - case 'git-multi': - throw new \Exception($type.' repositories not supported yet'); - break; + public function setInstallationManager(InstallationManager $manager) + { + $this->im = $manager; + } - case 'git': - return new GitRepository($spec['url']); - - case 'composer': - return new ComposerRepository($spec['url']); - - case 'pear': - return new PearRepository($spec['url'], $name); - - default: - throw new \UnexpectedValueException('Unknown repository type: '.$type.', could not create repository '.$name); - } + public function getInstallationManager() + { + return $this->im; } } diff --git a/src/Composer/Console/Application.php b/src/Composer/Console/Application.php index 84f307143..c792cdd5a 100644 --- a/src/Composer/Console/Application.php +++ b/src/Composer/Console/Application.php @@ -13,11 +13,13 @@ namespace Composer\Console; use Symfony\Component\Console\Application as BaseApplication; -use Composer\Composer; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Finder\Finder; use Composer\Command\InstallCommand; +use Composer\Composer; +use Composer\Package\PackageInterface; +use Composer\Package\PackageLock; /** * The console application that handles the commands @@ -59,10 +61,10 @@ class Application extends BaseApplication } /** - * Initializes all the composer commands + * Looks for all *Command files in Composer's Command directory */ protected function registerCommands() { $this->add(new InstallCommand()); } -} \ No newline at end of file +} diff --git a/src/Composer/DependencyResolver/Operation/InstallOperation.php b/src/Composer/DependencyResolver/Operation/InstallOperation.php new file mode 100644 index 000000000..89ab5bd85 --- /dev/null +++ b/src/Composer/DependencyResolver/Operation/InstallOperation.php @@ -0,0 +1,58 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\DependencyResolver\Operation; + +use Composer\Package\PackageInterface; + +/** + * Solver install operation. + * + * @author Konstantin Kudryashov + */ +class InstallOperation extends SolverOperation +{ + protected $package; + + /** + * Initializes operation. + * + * @param PackageInterface $package package instance + * @param string $reason operation reason + */ + public function __construct(PackageInterface $package, $reason = null) + { + parent::__construct($reason); + + $this->package = $package; + } + + /** + * Returns package instance. + * + * @return PackageInterface + */ + public function getPackage() + { + return $this->package; + } + + /** + * Returns job type. + * + * @return string + */ + public function getJobType() + { + return 'install'; + } +} diff --git a/src/Composer/DependencyResolver/Operation/OperationInterface.php b/src/Composer/DependencyResolver/Operation/OperationInterface.php new file mode 100644 index 000000000..704de2d5d --- /dev/null +++ b/src/Composer/DependencyResolver/Operation/OperationInterface.php @@ -0,0 +1,37 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\DependencyResolver\Operation; + +use Composer\Package\PackageInterface; + +/** + * Solver operation interface. + * + * @author Konstantin Kudryashov + */ +interface OperationInterface +{ + /** + * Returns job type. + * + * @return string + */ + function getJobType(); + + /** + * Returns operation reason. + * + * @return string + */ + function getReason(); +} diff --git a/src/Composer/DependencyResolver/Operation/SolverOperation.php b/src/Composer/DependencyResolver/Operation/SolverOperation.php new file mode 100644 index 000000000..a0071641e --- /dev/null +++ b/src/Composer/DependencyResolver/Operation/SolverOperation.php @@ -0,0 +1,45 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\DependencyResolver\Operation; + +use Composer\Package\PackageInterface; + +/** + * Abstract solver operation class. + * + * @author Konstantin Kudryashov + */ +abstract class SolverOperation implements OperationInterface +{ + protected $reason; + + /** + * Initializes operation. + * + * @param string $reason operation reason + */ + public function __construct($reason = null) + { + $this->reason = $reason; + } + + /** + * Returns operation reason. + * + * @return string + */ + public function getReason() + { + return $this->reason; + } +} diff --git a/src/Composer/DependencyResolver/Operation/UninstallOperation.php b/src/Composer/DependencyResolver/Operation/UninstallOperation.php new file mode 100644 index 000000000..3731f3181 --- /dev/null +++ b/src/Composer/DependencyResolver/Operation/UninstallOperation.php @@ -0,0 +1,58 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\DependencyResolver\Operation; + +use Composer\Package\PackageInterface; + +/** + * Solver uninstall operation. + * + * @author Konstantin Kudryashov + */ +class UninstallOperation extends SolverOperation +{ + protected $package; + + /** + * Initializes operation. + * + * @param PackageInterface $package package instance + * @param string $reason operation reason + */ + public function __construct(PackageInterface $package, $reason = null) + { + parent::__construct($reason); + + $this->package = $package; + } + + /** + * Returns package instance. + * + * @return PackageInterface + */ + public function getPackage() + { + return $this->package; + } + + /** + * Returns job type. + * + * @return string + */ + public function getJobType() + { + return 'uninstall'; + } +} diff --git a/src/Composer/DependencyResolver/Operation/UpdateOperation.php b/src/Composer/DependencyResolver/Operation/UpdateOperation.php new file mode 100644 index 000000000..c9d75c7b4 --- /dev/null +++ b/src/Composer/DependencyResolver/Operation/UpdateOperation.php @@ -0,0 +1,71 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\DependencyResolver\Operation; + +use Composer\Package\PackageInterface; + +/** + * Solver update operation. + * + * @author Konstantin Kudryashov + */ +class UpdateOperation extends SolverOperation +{ + protected $initialPackage; + protected $targetPackage; + + /** + * Initializes update operation. + * + * @param PackageInterface $initial initial package + * @param PackageInterface $target target package (updated) + * @param string $reason update reason + */ + public function __construct(PackageInterface $initial, PackageInterface $target, $reason = null) + { + parent::__construct($reason); + + $this->initialPackage = $initial; + $this->targetPackage = $target; + } + + /** + * Returns initial package. + * + * @return PackageInterface + */ + public function getInitialPackage() + { + return $this->initialPackage; + } + + /** + * Returns target package. + * + * @return PackageInterface + */ + public function getTargetPackage() + { + return $this->targetPackage; + } + + /** + * Returns job type. + * + * @return string + */ + public function getJobType() + { + return 'update'; + } +} diff --git a/src/Composer/DependencyResolver/Solver.php b/src/Composer/DependencyResolver/Solver.php index b06538d40..c21918262 100644 --- a/src/Composer/DependencyResolver/Solver.php +++ b/src/Composer/DependencyResolver/Solver.php @@ -14,6 +14,7 @@ namespace Composer\DependencyResolver; use Composer\Repository\RepositoryInterface; use Composer\Package\PackageInterface; +use Composer\DependencyResolver\Operation; /** * @author Nils Adermann @@ -49,6 +50,7 @@ class Solver protected $watches = array(); protected $removeWatches = array(); protected $decisionMap; + protected $installedPackageMap; protected $packageToUpdateRule = array(); protected $packageToFeatureRule = array(); @@ -251,7 +253,7 @@ class Solver $this->addedMap[$package->getId()] = true; $dontFix = 0; - if ($this->installed === $package->getRepository() && !isset($this->fixMap[$package->getId()])) { + if (isset($this->installedPackageMap[$package->getId()]) && !isset($this->fixMap[$package->getId()])) { $dontFix = 1; } @@ -270,7 +272,7 @@ class Solver if ($dontFix) { $foundInstalled = false; foreach ($possibleRequires as $require) { - if ($this->installed === $require->getRepository()) { + if (isset($this->installedPackageMap[$require->getId()])) { $foundInstalled = true; break; } @@ -293,7 +295,7 @@ class Solver $possibleConflicts = $this->pool->whatProvides($link->getTarget(), $link->getConstraint()); foreach ($possibleConflicts as $conflict) { - if ($dontFix && $this->installed === $conflict->getRepository()) { + if ($dontFix && isset($this->installedPackageMap[$conflict->getId()])) { continue; } @@ -308,7 +310,7 @@ class Solver /** @TODO: if ($this->noInstalledObsoletes) */ if (true) { $noObsoletes = isset($this->noObsoletes[$package->getId()]); - $isInstalled = ($this->installed === $package->getRepository()); + $isInstalled = (isset($this->installedPackageMap[$package->getId()])); foreach ($package->getReplaces() as $link) { $obsoleteProviders = $this->pool->whatProvides($link->getTarget(), $link->getConstraint()); @@ -757,7 +759,7 @@ class Solver switch ($job['cmd']) { case 'install': foreach ($job['packages'] as $package) { - if ($this->installed === $package->getRepository()) { + if (isset($this->installedPackageMap[$package->getId()])) { $disableQueue[] = array('type' => 'update', 'package' => $package); } @@ -870,7 +872,7 @@ class Solver case 'remove': foreach ($job['packages'] as $package) { - if ($this->installed === $package->getRepository()) { + if (isset($this->installedPackageMap[$package->getId()])) { $disableQueue[] = array('type' => 'update', 'package' => $package); } } @@ -932,6 +934,10 @@ class Solver { $this->jobs = $request->getJobs(); $installedPackages = $this->installed->getPackages(); + $this->installedPackageMap = array(); + foreach ($installedPackages as $package) { + $this->installedPackageMap[$package->getId()] = $package; + } $this->decisionMap = new \SplFixedArray($this->pool->getMaxId() + 1); @@ -953,12 +959,12 @@ class Solver foreach ($job['packages'] as $package) { switch ($job['cmd']) { case 'fix': - if ($this->installed === $package->getRepository()) { + if (isset($this->installedPackageMap[$package->getId()])) { $this->fixMap[$package->getId()] = true; } break; case 'update': - if ($this->installed === $package->getRepository()) { + if (isset($this->installedPackageMap[$package->getId()])) { $this->updateMap[$package->getId()] = true; } break; @@ -1038,7 +1044,7 @@ class Solver break; case 'lock': foreach ($job['packages'] as $package) { - if ($this->installed === $package->getRepository()) { + if (isset($this->installedPackageMap[$package->getId()])) { $rule = $this->createInstallRule($package, self::RULE_JOB_LOCK); } else { $rule = $this->createRemoveRule($package, self::RULE_JOB_LOCK); @@ -1082,7 +1088,7 @@ class Solver $package = $literal->getPackage(); // !wanted & installed - if (!$literal->isWanted() && $this->installed === $package->getRepository()) { + if (!$literal->isWanted() && isset($this->installedPackageMap[$package->getId()])) { $updateRule = $this->packageToUpdateRule[$package->getId()]; foreach ($updateRule->getLiterals() as $updateLiteral) { @@ -1097,7 +1103,7 @@ class Solver $package = $literal->getPackage(); // wanted & installed || !wanted & !installed - if ($literal->isWanted() == ($this->installed === $package->getRepository())) { + if ($literal->isWanted() == (isset($this->installedPackageMap[$package->getId()]))) { continue; } @@ -1105,28 +1111,21 @@ class Solver if (isset($installMeansUpdateMap[$literal->getPackageId()])) { $source = $installMeansUpdateMap[$literal->getPackageId()]; - $transaction[] = array( - 'job' => 'update', - 'from' => $source, - 'to' => $package, - 'why' => $this->decisionQueueWhy[$i], + $transaction[] = new Operation\UpdateOperation( + $source, $package, $this->decisionQueueWhy[$i] ); // avoid updates to one package from multiple origins unset($installMeansUpdateMap[$literal->getPackageId()]); $ignoreRemove[$source->getId()] = true; } else { - $transaction[] = array( - 'job' => 'install', - 'package' => $package, - 'why' => $this->decisionQueueWhy[$i], + $transaction[] = new Operation\InstallOperation( + $package, $this->decisionQueueWhy[$i] ); } } else if (!isset($ignoreRemove[$package->getId()])) { - $transaction[] = array( - 'job' => 'remove', - 'package' => $package, - 'why' => $this->decisionQueueWhy[$i], + $transaction[] = new Operation\UninstallOperation( + $package, $this->decisionQueueWhy[$i] ); } } @@ -2060,4 +2059,4 @@ class Solver } echo "\n"; } -} \ No newline at end of file +} diff --git a/src/Composer/Downloader/DownloadManager.php b/src/Composer/Downloader/DownloadManager.php new file mode 100644 index 000000000..8f40fdff0 --- /dev/null +++ b/src/Composer/Downloader/DownloadManager.php @@ -0,0 +1,171 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Downloader; + +use Composer\Package\PackageInterface; +use Composer\Downloader\DownloaderInterface; + +/** + * Downloaders manager. + * + * @author Konstantin Kudryashov + */ +class DownloadManager +{ + private $preferSource = false; + private $downloaders = array(); + + /** + * Initializes download manager. + * + * @param Boolean $preferSource prefer downloading from source + */ + public function __construct($preferSource = false) + { + $this->preferSource = $preferSource; + } + + /** + * Makes downloader prefer source installation over the dist. + * + * @param Boolean $preferSource prefer downloading from source + */ + public function preferSource($preferSource = true) + { + $this->preferSource = $preferSource; + } + + /** + * Sets installer downloader for a specific installation type. + * + * @param string $type installation type + * @param DownloaderInterface $downloader downloader instance + */ + public function setDownloader($type, DownloaderInterface $downloader) + { + $this->downloaders[$type] = $downloader; + } + + /** + * Returns downloader for a specific installation type. + * + * @param string $type installation type + * + * @return DownloaderInterface + * + * @throws UnexpectedValueException if downloader for provided type is not registeterd + */ + public function getDownloader($type) + { + if (!isset($this->downloaders[$type])) { + throw new \UnexpectedValueException('Unknown source type: '.$type); + } + + return $this->downloaders[$type]; + } + + /** + * Downloads package into target dir. + * + * @param PackageInterface $package package instance + * @param string $targetDir target dir + * @param Boolean $preferSource prefer installation from source + * + * @return string downloader type (source/dist) + * + * @throws InvalidArgumentException if package have no urls to download from + */ + public function download(PackageInterface $package, $targetDir, $preferSource = null) + { + $preferSource = null !== $preferSource ? $preferSource : $this->preferSource; + $sourceType = $package->getSourceType(); + $distType = $package->getDistType(); + + if (!($preferSource && $sourceType) && $distType) { + $downloader = $this->getDownloader($distType); + $downloader->download( + $package, $targetDir, + $package->getDistUrl(), $package->getDistSha1Checksum(), + $preferSource + ); + $package->setInstallationSource('dist'); + } elseif ($sourceType) { + $downloader = $this->getDownloader($sourceType); + $downloader->download($package, $targetDir, $package->getSourceUrl(), $preferSource); + $package->setInstallationSource('source'); + } else { + throw new \InvalidArgumentException('Package should have dist or source specified'); + } + } + + /** + * Updates package from initial to target version. + * + * @param PackageInterface $initial initial package version + * @param PackageInterface $target target package version + * @param string $targetDir target dir + * + * @throws InvalidArgumentException if initial package is not installed + */ + public function update(PackageInterface $initial, PackageInterface $target, $targetDir) + { + if (null === $installationType = $initial->getInstallationSource()) { + throw new \InvalidArgumentException( + 'Package '.$initial.' was not been installed propertly and can not be updated' + ); + } + $useSource = 'source' === $installationType; + + if (!$useSource) { + $initialType = $initial->getDistType(); + $targetType = $target->getDistType(); + } else { + $initialType = $initial->getSourceType(); + $targetType = $target->getSourceType(); + } + + $downloader = $this->getDownloader($initialType); + + if ($initialType === $targetType) { + $downloader->update($initial, $target, $targetDir, $useSource); + } else { + $downloader->remove($initial, $targetDir, $useSource); + $this->download($target, $targetDir, $useSource); + } + } + + /** + * Removes package from target dir. + * + * @param PackageInterface $package package instance + * @param string $targetDir target dir + */ + public function remove(PackageInterface $package, $targetDir) + { + if (null === $installationType = $package->getInstallationSource()) { + throw new \InvalidArgumentException( + 'Package '.$package.' was not been installed propertly and can not be removed' + ); + } + $useSource = 'source' === $installationType; + + // get proper downloader + if (!$useSource) { + $downloader = $this->getDownloader($package->getDistType()); + } else { + $downloader = $this->getDownloader($package->getSourceType()); + } + + $downloader->remove($package, $targetDir, $useSource); + } +} diff --git a/src/Composer/Downloader/DownloaderInterface.php b/src/Composer/Downloader/DownloaderInterface.php index b424bba3e..863d6bd30 100644 --- a/src/Composer/Downloader/DownloaderInterface.php +++ b/src/Composer/Downloader/DownloaderInterface.php @@ -15,21 +15,39 @@ namespace Composer\Downloader; use Composer\Package\PackageInterface; /** - * Package Downloader - * - * @author Kirill chEbba Chebunin - */ -interface DownloaderInterface + * Downloader interface. + * + * @author Konstantin Kudryashov + */ +interface DownloaderInterface { /** - * Download package + * Downloads specific package into specific folder. * - * @param PackageInterface $package Downloaded package - * @param string $path Download to - * @param string $url Download from - * @param string|null $checksum Package checksum - * - * @throws \UnexpectedValueException + * @param PackageInterface $package package instance + * @param string $path download path + * @param string $url download url + * @param string $checksum package checksum (for dists) + * @param Boolean $useSource download as source */ - function download(PackageInterface $package, $path, $url, $checksum = null); + function download(PackageInterface $package, $path, $url, $checksum = null, $useSource = false); + + /** + * Updates specific package in specific folder from initial to target version. + * + * @param PackageInterface $initial initial package + * @param PackageInterface $target updated package + * @param string $path download path + * @param Boolean $useSource download as source + */ + function update(PackageInterface $initial, PackageInterface $target, $path, $useSource = false); + + /** + * Removes specific package from specific folder. + * + * @param PackageInterface $package package instance + * @param string $path download path + * @param Boolean $useSource download as source + */ + function remove(PackageInterface $package, $path, $useSource = false); } diff --git a/src/Composer/Downloader/GitDownloader.php b/src/Composer/Downloader/GitDownloader.php index d7a98bed7..cf732bafd 100644 --- a/src/Composer/Downloader/GitDownloader.php +++ b/src/Composer/Downloader/GitDownloader.php @@ -19,27 +19,33 @@ use Composer\Package\PackageInterface; */ class GitDownloader implements DownloaderInterface { - protected $clone; - - public function __construct($clone = true) + /** + * {@inheritDoc} + */ + public function download(PackageInterface $package, $path, $url, $checksum = null, $useSource = false) { - $this->clone = $clone; + system('git clone '.escapeshellarg($url).' -b master '.escapeshellarg($path)); + + // TODO non-source installs: + // system('git archive --format=tar --prefix='.escapeshellarg($package->getName()).' --remote='.escapeshellarg($url).' master | tar -xf -'); } - public function download(PackageInterface $package, $path, $url, $checksum = null) + /** + * {@inheritDoc} + */ + public function update(PackageInterface $initial, PackageInterface $target, $path, $useSource = false) { - if (!is_dir($path)) { - if (file_exists($path)) { - throw new \UnexpectedValueException($path.' exists and is not a directory.'); - } - if (!mkdir($path, 0777, true)) { - throw new \UnexpectedValueException($path.' does not exist and could not be created.'); - } - } - if ($this->clone) { - system('git clone '.escapeshellarg($url).' -b master '.escapeshellarg($path.'/'.$package->getName())); - } else { - system('git archive --format=tar --prefix='.escapeshellarg($package->getName()).' --remote='.escapeshellarg($url).' master | tar -xf -'); - } + $cwd = getcwd(); + chdir($path); + system('git pull'); + chdir($cwd); } -} \ No newline at end of file + + /** + * {@inheritDoc} + */ + public function remove(PackageInterface $package, $path, $useSource = false) + { + echo 'rm -rf '.$path; // TODO + } +} diff --git a/src/Composer/Downloader/PearDownloader.php b/src/Composer/Downloader/PearDownloader.php index 56e45c677..0f4c86289 100644 --- a/src/Composer/Downloader/PearDownloader.php +++ b/src/Composer/Downloader/PearDownloader.php @@ -66,4 +66,4 @@ class PearDownloader implements DownloaderInterface } chdir($cwd); } -} \ No newline at end of file +} diff --git a/src/Composer/Downloader/ZipDownloader.php b/src/Composer/Downloader/ZipDownloader.php index 1c5fa547a..94a7eac0e 100644 --- a/src/Composer/Downloader/ZipDownloader.php +++ b/src/Composer/Downloader/ZipDownloader.php @@ -73,4 +73,4 @@ class ZipDownloader implements DownloaderInterface throw new \UnexpectedValueException($zipName.' is not a valid zip archive, got error code '.$retval); } } -} \ No newline at end of file +} diff --git a/src/Composer/Installer/InstallationManager.php b/src/Composer/Installer/InstallationManager.php new file mode 100644 index 000000000..e27f1c52e --- /dev/null +++ b/src/Composer/Installer/InstallationManager.php @@ -0,0 +1,131 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Installer; + +use Composer\Package\PackageInterface; +use Composer\DependencyResolver\Operation\OperationInterface; +use Composer\DependencyResolver\Operation\InstallOperation; +use Composer\DependencyResolver\Operation\UpdateOperation; +use Composer\DependencyResolver\Operation\UninstallOperation; + +/** + * Package operation manager. + * + * @author Konstantin Kudryashov + */ +class InstallationManager +{ + private $installers = array(); + + /** + * Sets installer for a specific package type. + * + * @param string $type package type (library f.e.) + * @param InstallerInterface $installer installer instance + */ + public function setInstaller($type, InstallerInterface $installer) + { + $this->installers[$type] = $installer; + } + + /** + * Returns installer for a specific package type. + * + * @param string $type package type + * + * @return InstallerInterface + * + * @throws InvalidArgumentException if installer for provided type is not registered + */ + public function getInstaller($type) + { + if (!isset($this->installers[$type])) { + throw new \InvalidArgumentException('Unknown installer type: '.$type); + } + + return $this->installers[$type]; + } + + /** + * Checks whether provided package is installed in one of the registered installers. + * + * @param PackageInterface $package package instance + * + * @return Boolean + */ + public function isPackageInstalled(PackageInterface $package) + { + foreach ($this->installers as $installer) { + if ($installer->isInstalled($package)) { + return true; + } + } + + return false; + } + + /** + * Executes solver operation. + * + * @param OperationInterface $operation operation instance + */ + public function execute(OperationInterface $operation) + { + $method = $operation->getJobType(); + $this->$method($operation); + } + + /** + * Executes install operation. + * + * @param InstallOperation $operation operation instance + */ + public function install(InstallOperation $operation) + { + $installer = $this->getInstaller($operation->getPackage()->getType()); + $installer->install($operation->getPackage()); + } + + /** + * Executes update operation. + * + * @param InstallOperation $operation operation instance + */ + public function update(UpdateOperation $operation) + { + $initial = $operation->getInitialPackage(); + $target = $operation->getTargetPackage(); + + $initialType = $initial->getType(); + $targetType = $target->getType(); + + if ($initialType === $targetType) { + $installer = $this->getInstaller($initialType); + $installer->update($initial, $target); + } else { + $this->getInstaller($initialType)->uninstall($initial); + $this->getInstaller($targetType)->install($target); + } + } + + /** + * Uninstalls package. + * + * @param UninstallOperation $operation operation instance + */ + public function uninstall(UninstallOperation $operation) + { + $installer = $this->getInstaller($operation->getPackage()->getType()); + $installer->uninstall($operation->getPackage()); + } +} diff --git a/src/Composer/Installer/InstallerInterface.php b/src/Composer/Installer/InstallerInterface.php index 6a3ac6fbd..9fbacbc36 100644 --- a/src/Composer/Installer/InstallerInterface.php +++ b/src/Composer/Installer/InstallerInterface.php @@ -12,22 +12,46 @@ namespace Composer\Installer; -use Composer\Downloader\DownloaderInterface; +use Composer\DependencyResolver\Operation\OperationInterface; use Composer\Package\PackageInterface; /** - * Package Installer - * - * @author Kirill chEbba Chebunin - */ + * Interface for the package installation manager. + * + * @author Konstantin Kudryashov + */ interface InstallerInterface { /** - * Install package + * Checks that provided package is installed. * - * @param PackageInterface $package - * @param DownloaderInterface $downloader - * @param string $type + * @param PackageInterface $package package instance + * + * @return Boolean */ - function install(PackageInterface $package, DownloaderInterface $downloader, $type); + function isInstalled(PackageInterface $package); + + /** + * Installs specific package. + * + * @param PackageInterface $package package instance + */ + function install(PackageInterface $package); + + /** + * Updates specific package. + * + * @param PackageInterface $initial already installed package version + * @param PackageInterface $target updated version + * + * @throws InvalidArgumentException if $from package is not installed + */ + function update(PackageInterface $initial, PackageInterface $target); + + /** + * Uninstalls specific package. + * + * @param PackageInterface $package package instance + */ + function uninstall(PackageInterface $package); } diff --git a/src/Composer/Installer/LibraryInstaller.php b/src/Composer/Installer/LibraryInstaller.php index ad3fd451e..59d546da2 100644 --- a/src/Composer/Installer/LibraryInstaller.php +++ b/src/Composer/Installer/LibraryInstaller.php @@ -12,30 +12,115 @@ namespace Composer\Installer; -use Composer\Downloader\DownloaderInterface; +use Composer\Downloader\DownloadManager; +use Composer\Repository\WritableRepositoryInterface; +use Composer\DependencyResolver\Operation\OperationInterface; use Composer\Package\PackageInterface; /** + * Package installation manager. + * * @author Jordi Boggiano + * @author Konstantin Kudryashov */ class LibraryInstaller implements InstallerInterface { - protected $dir; + private $dir; + private $dm; + private $repository; - public function __construct($dir = 'vendor') + /** + * Initializes library installer. + * + * @param string $dir relative path for packages home + * @param DownloadManager $dm download manager + * @param WritableRepositoryInterface $repository repository controller + */ + public function __construct($dir, DownloadManager $dm, WritableRepositoryInterface $repository) { $this->dir = $dir; + $this->dm = $dm; + + if (!is_dir($this->dir)) { + if (file_exists($this->dir)) { + throw new \UnexpectedValueException( + $this->dir.' exists and is not a directory.' + ); + } + if (!mkdir($this->dir, 0777, true)) { + throw new \UnexpectedValueException( + $this->dir.' does not exist and could not be created.' + ); + } + } + + $this->repository = $repository; } - public function install(PackageInterface $package, DownloaderInterface $downloader, $type) + /** + * Checks that specific package is installed. + * + * @param PackageInterface $package package instance + * + * @return Boolean + */ + public function isInstalled(PackageInterface $package) { - if ($type === 'dist') { - $downloader->download($package, $this->dir, $package->getDistUrl(), $package->getDistSha1Checksum()); - } elseif ($type === 'source') { - $downloader->download($package, $this->dir, $package->getSourceUrl()); - } else { - throw new \InvalidArgumentException('Type must be one of (dist, source), '.$type.' given.'); - } - return true; + return $this->repository->hasPackage($package); } -} \ No newline at end of file + + /** + * Installs specific package. + * + * @param PackageInterface $package package instance + * + * @throws InvalidArgumentException if provided package have no urls to download from + */ + public function install(PackageInterface $package) + { + $downloadPath = $this->dir.DIRECTORY_SEPARATOR.$package->getName(); + + $this->dm->download($package, $downloadPath); + $this->repository->addPackage($package); + } + + /** + * Updates specific package. + * + * @param PackageInterface $initial already installed package version + * @param PackageInterface $target updated version + * + * @throws InvalidArgumentException if $from package is not installed + */ + public function update(PackageInterface $initial, PackageInterface $target) + { + if (!$this->repository->hasPackage($initial)) { + throw new \InvalidArgumentException('Package is not installed: '.$initial); + } + + $downloadPath = $this->dir.DIRECTORY_SEPARATOR.$initial->getName(); + + $this->dm->update($initial, $target, $downloadPath); + $this->repository->removePackage($initial); + $this->repository->addPackage($target); + } + + /** + * Uninstalls specific package. + * + * @param PackageInterface $package package instance + * + * @throws InvalidArgumentException if package is not installed + */ + public function uninstall(PackageInterface $package) + { + if (!$this->repository->hasPackage($package)) { + throw new \InvalidArgumentException('Package is not installed: '.$package); + } + + $downloadPath = $this->dir.DIRECTORY_SEPARATOR.$package->getName(); + + $this->dm->remove($package, $downloadPath); + $this->repository->removePackage($package); + } +} diff --git a/src/Composer/Package/BasePackage.php b/src/Composer/Package/BasePackage.php index 73afc1a79..1caaf3ba8 100644 --- a/src/Composer/Package/BasePackage.php +++ b/src/Composer/Package/BasePackage.php @@ -134,6 +134,16 @@ abstract class BasePackage implements PackageInterface $this->repository = $repository; } + /** + * Returns package unique name, constructed from name, version and release type. + * + * @return string + */ + public function getUniqueName() + { + return $this->getName().'-'.$this->getVersion().'-'.$this->getReleaseType(); + } + /** * Converts the package into a readable and unique string * @@ -141,27 +151,6 @@ abstract class BasePackage implements PackageInterface */ public function __toString() { - return $this->getName().'-'.$this->getVersion().'-'.$this->getReleaseType(); - } - - /** - * Parses a version string and returns an array with the version, its type (alpha, beta, RC, stable) and a dev flag (for development branches tracking) - * - * @param string $version - * @return array - */ - public static function parseVersion($version) - { - if (!preg_match('#^v?(\d+)(\.\d+)?(\.\d+)?-?((?:beta|RC|alpha)\d*)?-?(dev)?$#i', $version, $matches)) { - throw new \UnexpectedValueException('Invalid version string '.$version); - } - - return array( - 'version' => $matches[1] - .(!empty($matches[2]) ? $matches[2] : '.0') - .(!empty($matches[3]) ? $matches[3] : '.0'), - 'type' => !empty($matches[4]) ? strtolower($matches[4]) : 'stable', - 'dev' => !empty($matches[5]), - ); + return $this->getUniqueName(); } } diff --git a/src/Composer/Package/Dumper/ArrayDumper.php b/src/Composer/Package/Dumper/ArrayDumper.php new file mode 100644 index 000000000..d13482264 --- /dev/null +++ b/src/Composer/Package/Dumper/ArrayDumper.php @@ -0,0 +1,58 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Package\Dumper; + +use Composer\Package\PackageInterface; + +/** + * @author Konstantin Kudryashiv + */ +class ArrayDumper +{ + public function dump(PackageInterface $package) + { + $keys = array( + 'type', + 'names', + 'extra', + 'installationSource', + 'sourceType', + 'sourceUrl', + 'distType', + 'distUrl', + 'distSha1Checksum', + 'releaseType', + 'version', + 'license', + 'requires', + 'conflicts', + 'provides', + 'replaces', + 'recommends', + 'suggests' + ); + + $data = array(); + $data['name'] = $package->getPrettyName(); + foreach ($keys as $key) { + $getter = 'get'.ucfirst($key); + $value = $package->$getter(); + + if (null !== $value && !(is_array($value) && 0 === count($value))) { + $data[$key] = $value; + } + } + + return $data; + } +} diff --git a/src/Composer/Package/Loader/ArrayLoader.php b/src/Composer/Package/Loader/ArrayLoader.php new file mode 100644 index 000000000..83a6e2051 --- /dev/null +++ b/src/Composer/Package/Loader/ArrayLoader.php @@ -0,0 +1,114 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Package\Loader; + +use Composer\Package; + +/** + * @author Konstantin Kudryashiv + */ +class ArrayLoader +{ + protected $supportedLinkTypes = array( + 'require' => 'requires', + 'conflict' => 'conflicts', + 'provide' => 'provides', + 'replace' => 'replaces', + 'recommend' => 'recommends', + 'suggest' => 'suggests', + ); + + public function load($config) + { + $this->validateConfig($config); + + $versionParser = new Package\Version\VersionParser(); + $version = $versionParser->parse($config['version']); + $package = new Package\MemoryPackage($config['name'], $version['version'], $version['type']); + + $package->setType(isset($config['type']) ? $config['type'] : 'library'); + + if (isset($config['extra'])) { + $package->setExtra($config['extra']); + } + + if (isset($config['license'])) { + $package->setLicense($config['license']); + } + + if (isset($config['source'])) { + if (!isset($config['source']['type']) || !isset($config['source']['url'])) { + throw new \UnexpectedValueException(sprintf( + "package source should be specified as {\"type\": ..., \"url\": ...},\n%s given", + json_encode($config['source']) + )); + } + $package->setSourceType($config['source']['type']); + $package->setSourceUrl($config['source']['url']); + } + + if (isset($config['dist'])) { + if (!isset($config['dist']['type']) + || !isset($config['dist']['url']) + || !isset($config['dist']['shasum'])) { + throw new \UnexpectedValueException(sprintf( + "package dist should be specified as ". + "{\"type\": ..., \"url\": ..., \"shasum\": ...},\n%s given", + json_encode($config['source']) + )); + } + $package->setDistType($config['dist']['type']); + $package->setDistUrl($config['dist']['url']); + $package->setDistSha1Checksum($config['dist']['shasum']); + } + + foreach ($this->supportedLinkTypes as $type => $description) { + if (isset($config[$type])) { + $method = 'set'.ucfirst($description); + $package->{$method}( + $this->loadLinksFromConfig($package->getName(), $description, $config['require']) + ); + } + } + + return $package; + } + + private function loadLinksFromConfig($srcPackageName, $description, array $linksSpecs) + { + $links = array(); + foreach ($linksSpecs as $packageName => $version) { + $name = strtolower($packageName); + + preg_match('#^([>=<~]*)([\d.]+.*)$#', $version, $match); + if (!$match[1]) { + $match[1] = '='; + } + + $constraint = new Package\LinkConstraint\VersionConstraint($match[1], $match[2]); + $links[] = new Package\Link($srcPackageName, $packageName, $constraint, $description); + } + + return $links; + } + + private function validateConfig(array $config) + { + if (!isset($config['name'])) { + throw new \UnexpectedValueException('name is required for package'); + } + if (!isset($config['version'])) { + throw new \UnexpectedValueException('version is required for package'); + } + } +} diff --git a/src/Composer/Package/Loader/JsonLoader.php b/src/Composer/Package/Loader/JsonLoader.php new file mode 100644 index 000000000..b0fcbd52a --- /dev/null +++ b/src/Composer/Package/Loader/JsonLoader.php @@ -0,0 +1,60 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Package\Loader; + +/** + * @author Konstantin Kudryashiv + */ +class JsonLoader extends ArrayLoader +{ + public function load($json) + { + $config = $this->loadJsonConfig($json); + + return parent::load($config); + } + + private function loadJsonConfig($json) + { + if (is_file($json)) { + $json = file_get_contents($json); + } + + $config = json_decode($json, true); + if (!$config) { + switch (json_last_error()) { + case JSON_ERROR_NONE: + $msg = 'No error has occurred, is your composer.json file empty?'; + break; + case JSON_ERROR_DEPTH: + $msg = 'The maximum stack depth has been exceeded'; + break; + case JSON_ERROR_STATE_MISMATCH: + $msg = 'Invalid or malformed JSON'; + break; + case JSON_ERROR_CTRL_CHAR: + $msg = 'Control character error, possibly incorrectly encoded'; + break; + case JSON_ERROR_SYNTAX: + $msg = 'Syntax error'; + break; + case JSON_ERROR_UTF8: + $msg = 'Malformed UTF-8 characters, possibly incorrectly encoded'; + break; + } + throw new \UnexpectedValueException('Incorrect composer.json file: '.$msg); + } + + return $config; + } +} diff --git a/src/Composer/Package/MemoryPackage.php b/src/Composer/Package/MemoryPackage.php index 0476f86e6..535b73eb1 100644 --- a/src/Composer/Package/MemoryPackage.php +++ b/src/Composer/Package/MemoryPackage.php @@ -20,6 +20,7 @@ namespace Composer\Package; class MemoryPackage extends BasePackage { protected $type; + protected $installationSource; protected $sourceType; protected $sourceUrl; protected $distType; @@ -84,6 +85,22 @@ class MemoryPackage extends BasePackage return $this->extra; } + /** + * {@inheritDoc} + */ + public function setInstallationSource($type) + { + $this-> installationSource = $type; + } + + /** + * {@inheritDoc} + */ + public function getInstallationSource() + { + return $this->installationSource; + } + /** * @param string $type */ diff --git a/src/Composer/Package/PackageInterface.php b/src/Composer/Package/PackageInterface.php index 5b1cbd712..e6e1fc19e 100644 --- a/src/Composer/Package/PackageInterface.php +++ b/src/Composer/Package/PackageInterface.php @@ -82,6 +82,20 @@ interface PackageInterface */ function getExtra(); + /** + * Sets source from which this package was installed (source/dist). + * + * @param string $type source/dist + */ + function setInstallationSource($type); + + /** + * Returns source from which this package was installed (source/dist). + * + * @param string $type source/dist + */ + function getInstallationSource(); + /** * Returns the repository type of this package, e.g. git, svn * @@ -202,6 +216,13 @@ interface PackageInterface */ function getRepository(); + /** + * Returns package unique name, constructed from name, version and release type. + * + * @return string + */ + function getUniqueName(); + /** * Converts the package into a readable and unique string * diff --git a/src/Composer/Package/PackageLock.php b/src/Composer/Package/PackageLock.php new file mode 100644 index 000000000..5e79cccce --- /dev/null +++ b/src/Composer/Package/PackageLock.php @@ -0,0 +1,91 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Package; + +use Composer\Package\MemoryPackage; +use Composer\Package\Version\VersionParser; + +/** + * @author Konstantin Kudryashiv + */ +class PackageLock +{ + private $file; + private $isLocked = false; + + public function __construct($file = 'composer.lock') + { + if (file_exists($file)) { + $this->file = $file; + $this->isLocked = true; + } + } + + public function isLocked() + { + return $this->isLocked; + } + + public function getLockedPackages() + { + $lockList = $this->loadJsonConfig($this->file); + + $versionParser = new VersionParser(); + $packages = array(); + foreach ($lockList as $info) { + $version = $versionParser->parse($info['version']); + $packages[] = new MemoryPackage($info['package'], $version['version'], $version['type']); + } + + return $packages; + } + + public function lock(array $packages) + { + // TODO: write installed packages info into $this->file + } + + private function loadJsonConfig($json) + { + if (is_file($json)) { + $json = file_get_contents($json); + } + + $config = json_decode($json, true); + if (!$config) { + switch (json_last_error()) { + case JSON_ERROR_NONE: + $msg = 'No error has occurred, is your composer.json file empty?'; + break; + case JSON_ERROR_DEPTH: + $msg = 'The maximum stack depth has been exceeded'; + break; + case JSON_ERROR_STATE_MISMATCH: + $msg = 'Invalid or malformed JSON'; + break; + case JSON_ERROR_CTRL_CHAR: + $msg = 'Control character error, possibly incorrectly encoded'; + break; + case JSON_ERROR_SYNTAX: + $msg = 'Syntax error'; + break; + case JSON_ERROR_UTF8: + $msg = 'Malformed UTF-8 characters, possibly incorrectly encoded'; + break; + } + throw new \UnexpectedValueException('Incorrect composer.json file: '.$msg); + } + + return $config; + } +} diff --git a/src/Composer/Package/Version/VersionParser.php b/src/Composer/Package/Version/VersionParser.php new file mode 100644 index 000000000..39c0d1215 --- /dev/null +++ b/src/Composer/Package/Version/VersionParser.php @@ -0,0 +1,43 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Package\Version; + +/** + * Version parser + * + * @author Konstantin Kudryashov + * @author Nils Adermann + */ +class VersionParser +{ + /** + * Parses a version string and returns an array with the version, its type (alpha, beta, RC, stable) and a dev flag (for development branches tracking) + * + * @param string $version + * @return array + */ + public function parse($version) + { + if (!preg_match('#^v?(\d+)(\.\d+)?(\.\d+)?-?((?:beta|RC|alpha)\d*)?-?(dev)?$#i', $version, $matches)) { + throw new \UnexpectedValueException('Invalid version string '.$version); + } + + return array( + 'version' => $matches[1] + .(!empty($matches[2]) ? $matches[2] : '.0') + .(!empty($matches[3]) ? $matches[3] : '.0'), + 'type' => strtolower(!empty($matches[4]) ? $matches[4] : 'stable'), + 'dev' => !empty($matches[5]), + ); + } +} diff --git a/src/Composer/Repository/ArrayRepository.php b/src/Composer/Repository/ArrayRepository.php index fb7025e9f..fcffc72af 100644 --- a/src/Composer/Repository/ArrayRepository.php +++ b/src/Composer/Repository/ArrayRepository.php @@ -23,6 +23,26 @@ class ArrayRepository implements RepositoryInterface { protected $packages; + /** + * Checks if specified package in this repository. + * + * @param PackageInterface $package package instance + * + * @return Boolean + */ + public function hasPackage(PackageInterface $package) + { + $packageId = $package->getUniqueName(); + + foreach ($this->getPackages() as $repoPackage) { + if ($packageId === $repoPackage->getUniqueName()) { + return true; + } + } + + return false; + } + /** * Adds a new package to the repository * @@ -37,6 +57,24 @@ class ArrayRepository implements RepositoryInterface $this->packages[] = $package; } + /** + * Removes package from repository. + * + * @param PackageInterface $package package instance + */ + public function removePackage(PackageInterface $package) + { + $packageId = $package->getUniqueName(); + + foreach ($this->getPackages() as $key => $repoPackage) { + if ($packageId === $repoPackage->getUniqueName()) { + array_splice($this->packages, $key, 1); + + return; + } + } + } + /** * Returns all contained packages * diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index eddd2b3a3..36f2e0a18 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -12,9 +12,7 @@ namespace Composer\Repository; -use Composer\Package\MemoryPackage; -use Composer\Package\BasePackage; -use Composer\Package\Link; +use Composer\Package\Loader\ArrayLoader; use Composer\Package\LinkConstraint\VersionConstraint; /** @@ -22,10 +20,16 @@ use Composer\Package\LinkConstraint\VersionConstraint; */ class ComposerRepository extends ArrayRepository { + protected $url; protected $packages; public function __construct($url) { + $url = rtrim($url, '/'); + if (!filter_var($url, FILTER_VALIDATE_URL)) { + throw new \UnexpectedValueException('Invalid url given for Composer repository: '.$url); + } + $this->url = $url; } @@ -42,60 +46,11 @@ class ComposerRepository extends ArrayRepository } } - protected function createPackages($data) + private function createPackages($data) { foreach ($data['versions'] as $rev) { - $version = BasePackage::parseVersion($rev['version']); - - $package = new MemoryPackage($rev['name'], $version['version'], $version['type']); - $package->setSourceType($rev['source']['type']); - $package->setSourceUrl($rev['source']['url']); - - $package->setDistType($rev['dist']['type']); - $package->setDistUrl($rev['dist']['url']); - $package->setDistSha1Checksum($rev['dist']['shasum']); - - if (isset($rev['type'])) { - $package->setType($rev['type']); - } - - if (isset($rev['extra'])) { - $package->setExtra($rev['extra']); - } - - if (isset($rev['license'])) { - $package->setLicense($rev['license']); - } - - $links = array( - 'require', - 'conflict', - 'provide', - 'replace', - 'recommend', - 'suggest', - ); - foreach ($links as $link) { - if (isset($rev[$link])) { - $method = 'set'.$link.'s'; - $package->{$method}($this->createLinks($rev['name'], $link.'s', $rev[$link])); - } - } - $this->addPackage($package); + $loader = new ArrayLoader(); + $this->addPackage($loader->load($rev)); } } - - protected function createLinks($name, $description, $linkSpecs) - { - $links = array(); - foreach ($linkSpecs as $dep => $ver) { - preg_match('#^([>=<~]*)([\d.]+.*)$#', $ver, $match); - if (!$match[1]) { - $match[1] = '='; - } - $constraint = new VersionConstraint($match[1], $match[2]); - $links[] = new Link($name, $dep, $constraint, $description); - } - return $links; - } -} \ No newline at end of file +} diff --git a/src/Composer/Repository/FilesystemRepository.php b/src/Composer/Repository/FilesystemRepository.php new file mode 100644 index 000000000..e5de4e5cb --- /dev/null +++ b/src/Composer/Repository/FilesystemRepository.php @@ -0,0 +1,82 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Repository; + +use Composer\Package\PackageInterface; +use Composer\Package\Loader\ArrayLoader; +use Composer\Package\Dumper\ArrayDumper; + +/** + * Filesystem repository. + * + * @author Konstantin Kudryashov + */ +class FilesystemRepository extends ArrayRepository implements WritableRepositoryInterface +{ + private $file; + + /** + * Initializes filesystem repository. + * + * @param string $group registry (installer) group + */ + public function __construct($repositoryFile) + { + $this->file = $repositoryFile; + $path = dirname($this->file); + + if (!is_dir($path)) { + if (file_exists($path)) { + throw new \UnexpectedValueException( + $path.' exists and is not a directory.' + ); + } + if (!mkdir($path, 0777, true)) { + throw new \UnexpectedValueException( + $path.' does not exist and could not be created.' + ); + } + } + } + + /** + * Initializes repository (reads file, or remote address). + */ + protected function initialize() + { + parent::initialize(); + + $packages = @json_decode(file_get_contents($this->file), true); + + if (is_array($packages)) { + $loader = new ArrayLoader(); + foreach ($packages as $package) { + $this->addPackage($loader->load($package)); + } + } + } + + /** + * Writes writable repository. + */ + public function write() + { + $packages = array(); + $dumper = new ArrayDumper(); + foreach ($this->getPackages() as $package) { + $packages[] = $dumper->dump($package); + } + + file_put_contents($this->file, json_encode($packages)); + } +} diff --git a/src/Composer/Repository/GitRepository.php b/src/Composer/Repository/GitRepository.php index 8eb0f95da..ed382538e 100644 --- a/src/Composer/Repository/GitRepository.php +++ b/src/Composer/Repository/GitRepository.php @@ -24,11 +24,13 @@ use Composer\Package\LinkConstraint\VersionConstraint; */ class GitRepository extends ArrayRepository { - protected $packages; + protected $url; + protected $cacheDir; - public function __construct($url) + public function __construct($url, $cacheDir) { $this->url = $url; + $this->cacheDir = $cacheDir; } protected function initialize() @@ -98,4 +100,4 @@ class GitRepository extends ArrayRepository } return $links; } -} \ No newline at end of file +} diff --git a/src/Composer/Repository/PearRepository.php b/src/Composer/Repository/PearRepository.php index 6fc4e94f4..928dbe06c 100644 --- a/src/Composer/Repository/PearRepository.php +++ b/src/Composer/Repository/PearRepository.php @@ -23,16 +23,17 @@ use Composer\Package\LinkConstraint\VersionConstraint; */ class PearRepository extends ArrayRepository { - private $name; - private $url; + protected $url; + protected $cacheDir; - public function __construct($url, $name = '') + public function __construct($url, $cacheDir) { if (!filter_var($url, FILTER_VALIDATE_URL)) { - throw new \UnexpectedValueException('Invalid url given for PEAR repository "'.$name.'": '.$url); + throw new \UnexpectedValueException('Invalid url given for PEAR repository: '.$url); } $this->url = $url; + $this->cacheDir = $cacheDir; } protected function initialize() diff --git a/src/Composer/Repository/PlatformRepository.php b/src/Composer/Repository/PlatformRepository.php index ebb9b81ed..5a94eee55 100644 --- a/src/Composer/Repository/PlatformRepository.php +++ b/src/Composer/Repository/PlatformRepository.php @@ -14,22 +14,23 @@ namespace Composer\Repository; use Composer\Package\MemoryPackage; use Composer\Package\BasePackage; +use Composer\Package\Version\VersionParser; /** * @author Jordi Boggiano */ class PlatformRepository extends ArrayRepository { - protected $packages; - protected function initialize() { parent::initialize(); + $versionParser = new VersionParser(); + try { - $version = BasePackage::parseVersion(PHP_VERSION); + $version = $versionParser->parse(PHP_VERSION); } catch (\UnexpectedValueException $e) { - $version = BasePackage::parseVersion(preg_replace('#^(.+?)(-.+)?$#', '$1', PHP_VERSION)); + $version = $versionParser->parse(preg_replace('#^(.+?)(-.+)?$#', '$1', PHP_VERSION)); } $php = new MemoryPackage('php', $version['version'], $version['type']); @@ -42,7 +43,7 @@ class PlatformRepository extends ArrayRepository $reflExt = new \ReflectionExtension($ext); try { - $version = BasePackage::parseVersion($reflExt->getVersion()); + $version = $versionParser->parse($reflExt->getVersion()); } catch (\UnexpectedValueException $e) { $version = array('version' => '0', 'type' => 'stable'); } @@ -51,4 +52,4 @@ class PlatformRepository extends ArrayRepository $this->addPackage($ext); } } -} \ No newline at end of file +} diff --git a/src/Composer/Repository/RepositoryInterface.php b/src/Composer/Repository/RepositoryInterface.php index 49e16690e..96c06656f 100644 --- a/src/Composer/Repository/RepositoryInterface.php +++ b/src/Composer/Repository/RepositoryInterface.php @@ -12,10 +12,29 @@ namespace Composer\Repository; +use Composer\Package\PackageInterface; + /** + * Repository interface. + * * @author Nils Adermann + * @author Konstantin Kudryashov */ interface RepositoryInterface extends \Countable { + /** + * Checks if specified package registered (installed). + * + * @param PackageInterface $package package instance + * + * @return Boolean + */ + function hasPackage(PackageInterface $package); + + /** + * Returns list of registered packages. + * + * @return array + */ function getPackages(); } diff --git a/src/Composer/Repository/RepositoryManager.php b/src/Composer/Repository/RepositoryManager.php new file mode 100644 index 000000000..7d9022b2f --- /dev/null +++ b/src/Composer/Repository/RepositoryManager.php @@ -0,0 +1,83 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Repository; + +/** + * Repositories manager. + * + * @author Konstantin Kudryashov + */ +class RepositoryManager +{ + private $localRepository; + private $repositories = array(); + + /** + * Sets repository with specific name. + * + * @param string $type repository name + * @param RepositoryInterface $repository repository instance + */ + public function setRepository($type, RepositoryInterface $repository) + { + $this->repositories[$type] = $repository; + } + + /** + * Returns repository for a specific installation type. + * + * @param string $type installation type + * + * @return RepositoryInterface + * + * @throws InvalidArgumentException if repository for provided type is not registeterd + */ + public function getRepository($type) + { + if (!isset($this->repositories[$type])) { + throw new \InvalidArgumentException('Repository is not registered: '.$type); + } + + return $this->repositories[$type]; + } + + /** + * Returns all repositories, except local one. + * + * @return array + */ + public function getRepositories() + { + return $this->repositories; + } + + /** + * Sets local repository for the project. + * + * @param RepositoryInterface $repository repository instance + */ + public function setLocalRepository(RepositoryInterface $repository) + { + $this->localRepository = $repository; + } + + /** + * Returns local repository for the project. + * + * @return RepositoryInterface + */ + public function getLocalRepository() + { + return $this->localRepository; + } +} diff --git a/src/Composer/Repository/WrapperRepository.php b/src/Composer/Repository/WrapperRepository.php new file mode 100644 index 000000000..f298cb50b --- /dev/null +++ b/src/Composer/Repository/WrapperRepository.php @@ -0,0 +1,64 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Repository; + +use Composer\Package\PackageInterface; + +/** + * @author Jordi Boggiano + */ +class WrapperRepository extends ArrayRepository implements WritableRepositoryInterface +{ + private $repositories; + + public function __construct(array $repositories) + { + $this->repositories = $repositories; + } + + protected function initialize() + { + parent::initialize(); + + foreach ($this->repositories as $repo) { + foreach ($repo->getPackages() as $package) { + $this->packages[] = $package; + } + } + } + + /** + * {@inheritDoc} + */ + public function addPackage(PackageInterface $package) + { + throw new \LogicException('Can not add packages to a wrapper repository'); + } + + /** + * {@inheritDoc} + */ + public function removePackage(PackageInterface $package) + { + throw new \LogicException('Can not remove packages to a wrapper repository'); + } + + public function write() + { + foreach ($this->repositories as $repo) { + if ($repo instanceof WritableRepositoryInterface) { + $repo->write(); + } + } + } +} diff --git a/src/Composer/Repository/WritableRepositoryInterface.php b/src/Composer/Repository/WritableRepositoryInterface.php new file mode 100644 index 000000000..77291e889 --- /dev/null +++ b/src/Composer/Repository/WritableRepositoryInterface.php @@ -0,0 +1,42 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Repository; + +use Composer\Package\PackageInterface; + +/** + * Writable repository interface. + * + * @author Konstantin Kudryashov + */ +interface WritableRepositoryInterface extends RepositoryInterface +{ + /** + * Writes repository (f.e. to the disc). + */ + function write(); + + /** + * Adds package to the repository. + * + * @param PackageInterface $package package instance + */ + function addPackage(PackageInterface $package); + + /** + * Removes package from the repository. + * + * @param PackageInterface $package package instance + */ + function removePackage(PackageInterface $package); +} diff --git a/tests/Composer/Test/DependencyResolver/SolverTest.php b/tests/Composer/Test/DependencyResolver/SolverTest.php index 03e1558e7..dd5d267a3 100644 --- a/tests/Composer/Test/DependencyResolver/SolverTest.php +++ b/tests/Composer/Test/DependencyResolver/SolverTest.php @@ -213,25 +213,6 @@ class SolverTest extends \PHPUnit_Framework_TestCase )); } - public function testSolverWithComposerRepo() - { - $this->repoInstalled = new PlatformRepository; - - // overwrite solver with custom installed repo - $this->solver = new Solver($this->policy, $this->pool, $this->repoInstalled); - - $this->repo = new ComposerRepository('http://packagist.org'); - list($monolog) = $this->repo->getPackages(); - - $this->reposComplete(); - - $this->request->install('Monolog'); - - $this->checkSolverResult(array( - array('job' => 'install', 'package' => $monolog), - )); - } - protected function reposComplete() { $this->pool->addRepository($this->repoInstalled); @@ -240,10 +221,23 @@ class SolverTest extends \PHPUnit_Framework_TestCase protected function checkSolverResult(array $expected) { - $result = $this->solver->solve($this->request); + $transaction = $this->solver->solve($this->request); - foreach ($result as &$step) { - unset($step['why']); + $result = array(); + foreach ($transaction as $operation) { + if ('update' === $operation->getJobType()) { + $result[] = array( + 'job' => 'update', + 'from' => $operation->getInitialPackage(), + 'to' => $operation->getTargetPackage() + ); + } else { + $job = ('uninstall' === $operation->getJobType() ? 'remove' : 'install'); + $result[] = array( + 'job' => $job, + 'package' => $operation->getPackage() + ); + } } $this->assertEquals($expected, $result); diff --git a/tests/Composer/Test/Downloader/DownloadManagerTest.php b/tests/Composer/Test/Downloader/DownloadManagerTest.php new file mode 100644 index 000000000..cc48bf4d1 --- /dev/null +++ b/tests/Composer/Test/Downloader/DownloadManagerTest.php @@ -0,0 +1,499 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Downloader; + +use Composer\Downloader\DownloadManager; + +class DownloadManagerTest extends \PHPUnit_Framework_TestCase +{ + public function testSetGetDownloader() + { + $downloader = $this->createDownloaderMock(); + $manager = new DownloadManager(); + + $manager->setDownloader('test', $downloader); + $this->assertSame($downloader, $manager->getDownloader('test')); + + $this->setExpectedException('UnexpectedValueException'); + $manager->getDownloader('unregistered'); + } + + public function testFullPackageDownload() + { + $package = $this->createPackageMock(); + $package + ->expects($this->once()) + ->method('getSourceType') + ->will($this->returnValue('git')); + $package + ->expects($this->once()) + ->method('getDistType') + ->will($this->returnValue('pear')); + + $package + ->expects($this->once()) + ->method('getDistUrl') + ->will($this->returnValue('dist_url')); + $package + ->expects($this->once()) + ->method('getDistSha1Checksum') + ->will($this->returnValue('sha1')); + + $package + ->expects($this->once()) + ->method('setInstallationSource') + ->with('dist'); + + $pearDownloader = $this->createDownloaderMock(); + $pearDownloader + ->expects($this->once()) + ->method('download') + ->with($package, 'target_dir', 'dist_url', 'sha1', false); + + $manager = new DownloadManager(); + $manager->setDownloader('pear', $pearDownloader); + + $manager->download($package, 'target_dir'); + } + + public function testBadPackageDownload() + { + $package = $this->createPackageMock(); + $package + ->expects($this->once()) + ->method('getSourceType') + ->will($this->returnValue(null)); + $package + ->expects($this->once()) + ->method('getDistType') + ->will($this->returnValue(null)); + + $manager = new DownloadManager(); + + $this->setExpectedException('InvalidArgumentException'); + $manager->download($package, 'target_dir'); + } + + public function testDistOnlyPackageDownload() + { + $package = $this->createPackageMock(); + $package + ->expects($this->once()) + ->method('getSourceType') + ->will($this->returnValue(null)); + $package + ->expects($this->once()) + ->method('getDistType') + ->will($this->returnValue('pear')); + + $package + ->expects($this->once()) + ->method('getDistUrl') + ->will($this->returnValue('dist_url')); + $package + ->expects($this->once()) + ->method('getDistSha1Checksum') + ->will($this->returnValue('sha1')); + + $package + ->expects($this->once()) + ->method('setInstallationSource') + ->with('dist'); + + $pearDownloader = $this->createDownloaderMock(); + $pearDownloader + ->expects($this->once()) + ->method('download') + ->with($package, 'target_dir', 'dist_url', 'sha1', false); + + $manager = new DownloadManager(); + $manager->setDownloader('pear', $pearDownloader); + + $manager->download($package, 'target_dir'); + } + + public function testSourceOnlyPackageDownload() + { + $package = $this->createPackageMock(); + $package + ->expects($this->once()) + ->method('getSourceType') + ->will($this->returnValue('git')); + $package + ->expects($this->once()) + ->method('getDistType') + ->will($this->returnValue(null)); + + $package + ->expects($this->once()) + ->method('getSourceUrl') + ->will($this->returnValue('source_url')); + + $package + ->expects($this->once()) + ->method('setInstallationSource') + ->with('source'); + + $gitDownloader = $this->createDownloaderMock(); + $gitDownloader + ->expects($this->once()) + ->method('download') + ->with($package, 'vendor/pkg', 'source_url', false); + + $manager = new DownloadManager(); + $manager->setDownloader('git', $gitDownloader); + + $manager->download($package, 'vendor/pkg'); + } + + public function testFullPackageDownloadWithSourcePreferred() + { + $package = $this->createPackageMock(); + $package + ->expects($this->once()) + ->method('getSourceType') + ->will($this->returnValue('git')); + $package + ->expects($this->once()) + ->method('getDistType') + ->will($this->returnValue('pear')); + + $package + ->expects($this->once()) + ->method('getSourceUrl') + ->will($this->returnValue('source_url')); + + $package + ->expects($this->once()) + ->method('setInstallationSource') + ->with('source'); + + $gitDownloader = $this->createDownloaderMock(); + $gitDownloader + ->expects($this->once()) + ->method('download') + ->with($package, 'vendor/pkg', 'source_url', true); + + $manager = new DownloadManager(); + $manager->setDownloader('git', $gitDownloader); + $manager->preferSource(); + + $manager->download($package, 'vendor/pkg'); + } + + public function testDistOnlyPackageDownloadWithSourcePreferred() + { + $package = $this->createPackageMock(); + $package + ->expects($this->once()) + ->method('getSourceType') + ->will($this->returnValue(null)); + $package + ->expects($this->once()) + ->method('getDistType') + ->will($this->returnValue('pear')); + + $package + ->expects($this->once()) + ->method('getDistUrl') + ->will($this->returnValue('dist_url')); + $package + ->expects($this->once()) + ->method('getDistSha1Checksum') + ->will($this->returnValue('sha1')); + + $package + ->expects($this->once()) + ->method('setInstallationSource') + ->with('dist'); + + $pearDownloader = $this->createDownloaderMock(); + $pearDownloader + ->expects($this->once()) + ->method('download') + ->with($package, 'target_dir', 'dist_url', 'sha1', true); + + $manager = new DownloadManager(); + $manager->setDownloader('pear', $pearDownloader); + $manager->preferSource(); + + $manager->download($package, 'target_dir'); + } + + public function testSourceOnlyPackageDownloadWithSourcePreferred() + { + $package = $this->createPackageMock(); + $package + ->expects($this->once()) + ->method('getSourceType') + ->will($this->returnValue('git')); + $package + ->expects($this->once()) + ->method('getDistType') + ->will($this->returnValue(null)); + + $package + ->expects($this->once()) + ->method('getSourceUrl') + ->will($this->returnValue('source_url')); + + $package + ->expects($this->once()) + ->method('setInstallationSource') + ->with('source'); + + $gitDownloader = $this->createDownloaderMock(); + $gitDownloader + ->expects($this->once()) + ->method('download') + ->with($package, 'vendor/pkg', 'source_url', true); + + $manager = new DownloadManager(); + $manager->setDownloader('git', $gitDownloader); + $manager->preferSource(); + + $manager->download($package, 'vendor/pkg'); + } + + public function testBadPackageDownloadWithSourcePreferred() + { + $package = $this->createPackageMock(); + $package + ->expects($this->once()) + ->method('getSourceType') + ->will($this->returnValue(null)); + $package + ->expects($this->once()) + ->method('getDistType') + ->will($this->returnValue(null)); + + $manager = new DownloadManager(); + $manager->preferSource(); + + $this->setExpectedException('InvalidArgumentException'); + $manager->download($package, 'target_dir'); + } + + public function testUpdateDistWithEqualTypes() + { + $initial = $this->createPackageMock(); + $initial + ->expects($this->once()) + ->method('getInstallationSource') + ->will($this->returnValue('dist')); + $initial + ->expects($this->once()) + ->method('getDistType') + ->will($this->returnValue('pear')); + + $target = $this->createPackageMock(); + $target + ->expects($this->once()) + ->method('getDistType') + ->will($this->returnValue('pear')); + + $pearDownloader = $this->createDownloaderMock(); + $pearDownloader + ->expects($this->once()) + ->method('update') + ->with($initial, $target, 'vendor/bundles/FOS/UserBundle', false); + + $manager = new DownloadManager(); + $manager->setDownloader('pear', $pearDownloader); + + $manager->update($initial, $target, 'vendor/bundles/FOS/UserBundle'); + } + + public function testUpdateDistWithNotEqualTypes() + { + $initial = $this->createPackageMock(); + $initial + ->expects($this->once()) + ->method('getInstallationSource') + ->will($this->returnValue('dist')); + $initial + ->expects($this->once()) + ->method('getDistType') + ->will($this->returnValue('pear')); + + $target = $this->createPackageMock(); + $target + ->expects($this->once()) + ->method('getDistType') + ->will($this->returnValue('composer')); + + $pearDownloader = $this->createDownloaderMock(); + $pearDownloader + ->expects($this->once()) + ->method('remove') + ->with($initial, 'vendor/bundles/FOS/UserBundle', false); + + $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') + ->setMethods(array('download')) + ->getMock(); + $manager + ->expects($this->once()) + ->method('download') + ->with($target, 'vendor/bundles/FOS/UserBundle', false); + + $manager->setDownloader('pear', $pearDownloader); + $manager->update($initial, $target, 'vendor/bundles/FOS/UserBundle'); + } + + public function testUpdateSourceWithEqualTypes() + { + $initial = $this->createPackageMock(); + $initial + ->expects($this->once()) + ->method('getInstallationSource') + ->will($this->returnValue('source')); + $initial + ->expects($this->once()) + ->method('getSourceType') + ->will($this->returnValue('svn')); + + $target = $this->createPackageMock(); + $target + ->expects($this->once()) + ->method('getSourceType') + ->will($this->returnValue('svn')); + + $svnDownloader = $this->createDownloaderMock(); + $svnDownloader + ->expects($this->once()) + ->method('update') + ->with($initial, $target, 'vendor/pkg', true); + + $manager = new DownloadManager(); + $manager->setDownloader('svn', $svnDownloader); + + $manager->update($initial, $target, 'vendor/pkg'); + } + + public function testUpdateSourceWithNotEqualTypes() + { + $initial = $this->createPackageMock(); + $initial + ->expects($this->once()) + ->method('getInstallationSource') + ->will($this->returnValue('source')); + $initial + ->expects($this->once()) + ->method('getSourceType') + ->will($this->returnValue('svn')); + + $target = $this->createPackageMock(); + $target + ->expects($this->once()) + ->method('getSourceType') + ->will($this->returnValue('git')); + + $svnDownloader = $this->createDownloaderMock(); + $svnDownloader + ->expects($this->once()) + ->method('remove') + ->with($initial, 'vendor/pkg', true); + + $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager') + ->setMethods(array('download')) + ->getMock(); + $manager + ->expects($this->once()) + ->method('download') + ->with($target, 'vendor/pkg', true); + $manager->setDownloader('svn', $svnDownloader); + + $manager->update($initial, $target, 'vendor/pkg'); + } + + public function testUpdateBadlyInstalledPackage() + { + $initial = $this->createPackageMock(); + $target = $this->createPackageMock(); + + $this->setExpectedException('InvalidArgumentException'); + + $manager = new DownloadManager(); + $manager->update($initial, $target, 'vendor/pkg'); + } + + public function testRemoveDist() + { + $package = $this->createPackageMock(); + $package + ->expects($this->once()) + ->method('getInstallationSource') + ->will($this->returnValue('dist')); + $package + ->expects($this->once()) + ->method('getDistType') + ->will($this->returnValue('pear')); + + $pearDownloader = $this->createDownloaderMock(); + $pearDownloader + ->expects($this->once()) + ->method('remove') + ->with($package, 'vendor/bundles/FOS/UserBundle'); + + $manager = new DownloadManager(); + $manager->setDownloader('pear', $pearDownloader); + + $manager->remove($package, 'vendor/bundles/FOS/UserBundle'); + } + + public function testRemoveSource() + { + $package = $this->createPackageMock(); + $package + ->expects($this->once()) + ->method('getInstallationSource') + ->will($this->returnValue('source')); + $package + ->expects($this->once()) + ->method('getSourceType') + ->will($this->returnValue('svn')); + + $svnDownloader = $this->createDownloaderMock(); + $svnDownloader + ->expects($this->once()) + ->method('remove') + ->with($package, 'vendor/pkg'); + + $manager = new DownloadManager(); + $manager->setDownloader('svn', $svnDownloader); + + $manager->remove($package, 'vendor/pkg'); + } + + public function testRemoveBadlyInstalledPackage() + { + $package = $this->createPackageMock(); + $manager = new DownloadManager(); + + $this->setExpectedException('InvalidArgumentException'); + + $manager->remove($package, 'vendor/pkg'); + } + + private function createDownloaderMock() + { + return $this->getMockBuilder('Composer\Downloader\DownloaderInterface') + ->getMock(); + } + + private function createPackageMock() + { + return $this->getMockBuilder('Composer\Package\PackageInterface') + ->getMock(); + } +} diff --git a/tests/Composer/Test/Installer/InstallationManagerTest.php b/tests/Composer/Test/Installer/InstallationManagerTest.php new file mode 100644 index 000000000..a8b1a9818 --- /dev/null +++ b/tests/Composer/Test/Installer/InstallationManagerTest.php @@ -0,0 +1,180 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Installer; + +use Composer\Installer\InstallationManager; +use Composer\DependencyResolver\Operation\InstallOperation; +use Composer\DependencyResolver\Operation\UpdateOperation; +use Composer\DependencyResolver\Operation\UninstallOperation; + +class InstallationManagerTest extends \PHPUnit_Framework_TestCase +{ + public function testSetGetInstaller() + { + $installer = $this->createInstallerMock(); + $manager = new InstallationManager(); + + $manager->setInstaller('vendor', $installer); + $this->assertSame($installer, $manager->getInstaller('vendor')); + + $this->setExpectedException('InvalidArgumentException'); + $manager->getInstaller('unregistered'); + } + + public function testExecute() + { + $manager = $this->getMockBuilder('Composer\Installer\InstallationManager') + ->setMethods(array('install', 'update', 'uninstall')) + ->getMock(); + + $installOperation = new InstallOperation($this->createPackageMock()); + $removeOperation = new UninstallOperation($this->createPackageMock()); + $updateOperation = new UpdateOperation( + $this->createPackageMock(), $this->createPackageMock() + ); + + $manager + ->expects($this->once()) + ->method('install') + ->with($installOperation); + $manager + ->expects($this->once()) + ->method('uninstall') + ->with($removeOperation); + $manager + ->expects($this->once()) + ->method('update') + ->with($updateOperation); + + $manager->execute($installOperation); + $manager->execute($removeOperation); + $manager->execute($updateOperation); + } + + public function testInstall() + { + $installer = $this->createInstallerMock(); + $manager = new InstallationManager(); + $manager->setInstaller('library', $installer); + + $package = $this->createPackageMock(); + $operation = new InstallOperation($package, 'test'); + + $package + ->expects($this->once()) + ->method('getType') + ->will($this->returnValue('library')); + + $installer + ->expects($this->once()) + ->method('install') + ->with($package); + + $manager->install($operation); + } + + public function testUpdateWithEqualTypes() + { + $installer = $this->createInstallerMock(); + $manager = new InstallationManager(); + $manager->setInstaller('library', $installer); + + $initial = $this->createPackageMock(); + $target = $this->createPackageMock(); + $operation = new UpdateOperation($initial, $target, 'test'); + + $initial + ->expects($this->once()) + ->method('getType') + ->will($this->returnValue('library')); + $target + ->expects($this->once()) + ->method('getType') + ->will($this->returnValue('library')); + + $installer + ->expects($this->once()) + ->method('update') + ->with($initial, $target); + + $manager->update($operation); + } + + public function testUpdateWithNotEqualTypes() + { + $installer1 = $this->createInstallerMock(); + $installer2 = $this->createInstallerMock(); + $manager = new InstallationManager(); + $manager->setInstaller('library', $installer1); + $manager->setInstaller('bundles', $installer2); + + $initial = $this->createPackageMock(); + $target = $this->createPackageMock(); + $operation = new UpdateOperation($initial, $target, 'test'); + + $initial + ->expects($this->once()) + ->method('getType') + ->will($this->returnValue('library')); + $target + ->expects($this->once()) + ->method('getType') + ->will($this->returnValue('bundles')); + + $installer1 + ->expects($this->once()) + ->method('uninstall') + ->with($initial); + + $installer2 + ->expects($this->once()) + ->method('install') + ->with($target); + + $manager->update($operation); + } + + public function testUninstall() + { + $installer = $this->createInstallerMock(); + $manager = new InstallationManager(); + $manager->setInstaller('library', $installer); + + $package = $this->createPackageMock(); + $operation = new UninstallOperation($package, 'test'); + + $package + ->expects($this->once()) + ->method('getType') + ->will($this->returnValue('library')); + + $installer + ->expects($this->once()) + ->method('uninstall') + ->with($package); + + $manager->uninstall($operation); + } + + private function createInstallerMock() + { + return $this->getMockBuilder('Composer\Installer\InstallerInterface') + ->getMock(); + } + + private function createPackageMock() + { + return $this->getMockBuilder('Composer\Package\PackageInterface') + ->getMock(); + } +} diff --git a/tests/Composer/Test/Installer/LibraryInstallerTest.php b/tests/Composer/Test/Installer/LibraryInstallerTest.php new file mode 100644 index 000000000..c5be9cbb6 --- /dev/null +++ b/tests/Composer/Test/Installer/LibraryInstallerTest.php @@ -0,0 +1,169 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Installer; + +use Composer\Installer\LibraryInstaller; +use Composer\DependencyResolver\Operation; + +class LibraryInstallerTest extends \PHPUnit_Framework_TestCase +{ + private $dir; + private $dm; + private $repository; + private $library; + + protected function setUp() + { + $this->dir = sys_get_temp_dir().'/composer'; + if (is_dir($this->dir)) { + rmdir($this->dir); + } + + $this->dm = $this->getMockBuilder('Composer\Downloader\DownloadManager') + ->disableOriginalConstructor() + ->getMock(); + + $this->repository = $this->getMockBuilder('Composer\Repository\WritableRepositoryInterface') + ->disableOriginalConstructor() + ->getMock(); + } + + public function testInstallerCreation() + { + $library = new LibraryInstaller($this->dir, $this->dm, $this->repository); + $this->assertTrue(is_dir($this->dir)); + + $file = sys_get_temp_dir().'/file'; + touch($file); + + $this->setExpectedException('UnexpectedValueException'); + $library = new LibraryInstaller($file, $this->dm, $this->repository); + } + + public function testIsInstalled() + { + $library = new LibraryInstaller($this->dir, $this->dm, $this->repository); + $package = $this->createPackageMock(); + + $this->repository + ->expects($this->exactly(2)) + ->method('hasPackage') + ->with($package) + ->will($this->onConsecutiveCalls(true, false)); + + $this->assertTrue($library->isInstalled($package)); + $this->assertFalse($library->isInstalled($package)); + } + + public function testInstall() + { + $library = new LibraryInstaller($this->dir, $this->dm, $this->repository); + $package = $this->createPackageMock(); + + $package + ->expects($this->once()) + ->method('getName') + ->will($this->returnValue('some/package')); + + $this->dm + ->expects($this->once()) + ->method('download') + ->with($package, $this->dir.'/some/package'); + + $this->repository + ->expects($this->once()) + ->method('addPackage') + ->with($package); + + $library->install($package); + } + + public function testUpdate() + { + $library = new LibraryInstaller($this->dir, $this->dm, $this->repository); + $initial = $this->createPackageMock(); + $target = $this->createPackageMock(); + + $initial + ->expects($this->once()) + ->method('getName') + ->will($this->returnValue('package1')); + + $this->repository + ->expects($this->exactly(2)) + ->method('hasPackage') + ->with($initial) + ->will($this->onConsecutiveCalls(true, false)); + + $this->dm + ->expects($this->once()) + ->method('update') + ->with($initial, $target, $this->dir.'/package1'); + + $this->repository + ->expects($this->once()) + ->method('removePackage') + ->with($initial); + + $this->repository + ->expects($this->once()) + ->method('addPackage') + ->with($target); + + $library->update($initial, $target); + + $this->setExpectedException('InvalidArgumentException'); + + $library->update($initial, $target); + } + + public function testUninstall() + { + $library = new LibraryInstaller($this->dir, $this->dm, $this->repository); + $package = $this->createPackageMock(); + + $package + ->expects($this->once()) + ->method('getName') + ->will($this->returnValue('pkg')); + + $this->repository + ->expects($this->exactly(2)) + ->method('hasPackage') + ->with($package) + ->will($this->onConsecutiveCalls(true, false)); + + $this->dm + ->expects($this->once()) + ->method('remove') + ->with($package, $this->dir.'/pkg'); + + $this->repository + ->expects($this->once()) + ->method('removePackage') + ->with($package); + + $library->uninstall($package); + + $this->setExpectedException('InvalidArgumentException'); + + $library->uninstall($package); + } + + private function createPackageMock() + { + return $this->getMockBuilder('Composer\Package\MemoryPackage') + ->setConstructorArgs(array(md5(rand()), '1.0.0')) + ->getMock(); + } +} diff --git a/tests/Composer/Test/Repository/ArrayRepositoryTest.php b/tests/Composer/Test/Repository/ArrayRepositoryTest.php index 7c9f64947..64e62fff2 100644 --- a/tests/Composer/Test/Repository/ArrayRepositoryTest.php +++ b/tests/Composer/Test/Repository/ArrayRepositoryTest.php @@ -17,11 +17,37 @@ use Composer\Package\MemoryPackage; class ArrayRepositoryTest extends \PHPUnit_Framework_TestCase { - public function testAddLiteral() + public function testAddPackage() { $repo = new ArrayRepository; $repo->addPackage(new MemoryPackage('foo', '1')); $this->assertEquals(1, count($repo)); } + + public function testRemovePackage() + { + $package = new MemoryPackage('bar', '2'); + + $repo = new ArrayRepository; + $repo->addPackage(new MemoryPackage('foo', '1')); + $repo->addPackage($package); + + $this->assertEquals(2, count($repo)); + + $repo->removePackage(new MemoryPackage('foo', '1')); + + $this->assertEquals(1, count($repo)); + $this->assertEquals(array($package), $repo->getPackages()); + } + + public function testHasPackage() + { + $repo = new ArrayRepository; + $repo->addPackage(new MemoryPackage('foo', '1')); + $repo->addPackage(new MemoryPackage('bar', '2')); + + $this->assertTrue($repo->hasPackage(new MemoryPackage('foo', '1'))); + $this->assertFalse($repo->hasPackage(new MemoryPackage('bar', '1'))); + } } diff --git a/tests/Composer/Test/Repository/FilesystemRepositoryTest.php b/tests/Composer/Test/Repository/FilesystemRepositoryTest.php new file mode 100644 index 000000000..ee777b52f --- /dev/null +++ b/tests/Composer/Test/Repository/FilesystemRepositoryTest.php @@ -0,0 +1,55 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Repository; + +use Composer\Repository\FilesystemRepository; + +class FilesystemRepositoryTest extends \PHPUnit_Framework_TestCase +{ + private $dir; + private $repositoryFile; + + protected function setUp() + { + $this->dir = sys_get_temp_dir().'/.composer'; + $this->repositoryFile = $this->dir.'/some_registry-reg.json'; + + if (file_exists($this->repositoryFile)) { + unlink($this->repositoryFile); + } + } + + public function testRepositoryReadWrite() + { + $this->assertFileNotExists($this->repositoryFile); + $repository = new FilesystemRepository($this->repositoryFile); + + $repository->getPackages(); + $repository->write(); + $this->assertFileExists($this->repositoryFile); + + file_put_contents($this->repositoryFile, json_encode(array( + array('name' => 'package1', 'version' => '1.0.0-beta', 'type' => 'vendor') + ))); + + $repository = new FilesystemRepository($this->repositoryFile); + $repository->getPackages(); + $repository->write(); + $this->assertFileExists($this->repositoryFile); + + $data = json_decode(file_get_contents($this->repositoryFile), true); + $this->assertEquals(array( + array('name' => 'package1', 'type' => 'vendor', 'version' => '1.0.0', 'releaseType' => 'beta', 'names' => array('package1')) + ), $data); + } +}