diff --git a/bin/compile b/bin/compile index 62e111228..bd8426693 100755 --- a/bin/compile +++ b/bin/compile @@ -1,11 +1,10 @@ #!/usr/bin/env php getOperation()->getPackage(); + // do stuff + } +} +``` diff --git a/src/Composer/Command/InstallCommand.php b/src/Composer/Command/InstallCommand.php index bf8bca726..9e212af10 100644 --- a/src/Composer/Command/InstallCommand.php +++ b/src/Composer/Command/InstallCommand.php @@ -12,6 +12,8 @@ namespace Composer\Command; +use Composer\Script\ScriptEvents; +use Composer\Script\EventDispatcher; use Composer\Autoload\AutoloadGenerator; use Composer\DependencyResolver; use Composer\DependencyResolver\Pool; @@ -23,6 +25,8 @@ use Composer\Repository\PlatformRepository; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Composer\DependencyResolver\Operation\InstallOperation; +use Composer\DependencyResolver\Solver; /** * @author Jordi Boggiano @@ -65,6 +69,8 @@ EOT $dryRun = (Boolean) $input->getOption('dry-run'); $verbose = $dryRun || $input->getOption('verbose'); $composer = $this->getComposer(); + $io = $this->getApplication()->getIO(); + $dispatcher = new EventDispatcher($this->getComposer(), $io); if ($preferSource) { $composer->getDownloadManager()->setPreferSource(true); @@ -82,6 +88,12 @@ EOT $pool->addRepository($repository); } + // dispatch pre event + if (!$dryRun) { + $eventName = $update ? ScriptEvents::PRE_UPDATE_CMD : ScriptEvents::PRE_INSTALL_CMD; + $dispatcher->dispatchCommandEvent($eventName); + } + // creating requirements request $request = new Request($pool); if ($update) { @@ -132,7 +144,10 @@ EOT // TODO this belongs in the solver, but this will do for now to report top-level deps missing at least foreach ($request->getJobs() as $job) { if ('install' === $job['cmd']) { - foreach ($installedRepo->getPackages() as $package) { + foreach ($installedRepo->getPackages() as $package ) { + if ($installedRepo->hasPackage($package) && !$package->isPlatform() && !$installationManager->isPackageInstalled($package)) { + $operations[$job['packageName']] = new InstallOperation($package, Solver::RULE_PACKAGE_NOT_EXIST); + } if (in_array($job['packageName'], $package->getNames())) { continue 2; } @@ -162,7 +177,9 @@ EOT $output->writeln((string) $operation); } if (!$dryRun) { + $dispatcher->dispatchPackageEvent(constant('Composer\Script\ScriptEvents::PRE_PACKAGE_'.strtoupper($operation->getJobType())), $operation); $installationManager->execute($operation); + $dispatcher->dispatchPackageEvent(constant('Composer\Script\ScriptEvents::POST_PACKAGE_'.strtoupper($operation->getJobType())), $operation); } } @@ -177,6 +194,10 @@ EOT $output->writeln('Generating autoload files'); $generator = new AutoloadGenerator; $generator->dump($localRepo, $composer->getPackage(), $installationManager, $installationManager->getVendorPath().'/.composer'); + + // dispatch post event + $eventName = $update ? ScriptEvents::POST_UPDATE_CMD : ScriptEvents::POST_INSTALL_CMD; + $dispatcher->dispatchCommandEvent($eventName); } } diff --git a/src/Composer/Command/SearchCommand.php b/src/Composer/Command/SearchCommand.php index 10df68e35..c7c6ebd3d 100644 --- a/src/Composer/Command/SearchCommand.php +++ b/src/Composer/Command/SearchCommand.php @@ -26,7 +26,7 @@ class SearchCommand extends Command { $this ->setName('search') - ->setDescription('search for packages') + ->setDescription('Search for packages') ->setDefinition(array( new InputArgument('tokens', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'tokens to search for'), )) diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index 334cd8997..9de3ed2de 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -28,7 +28,7 @@ class ShowCommand extends Command { $this ->setName('show') - ->setDescription('show package details') + ->setDescription('Show package details') ->setDefinition(array( new InputArgument('package', InputArgument::REQUIRED, 'the package to inspect'), new InputArgument('version', InputArgument::OPTIONAL, 'the version'), diff --git a/src/Composer/Command/ValidateCommand.php b/src/Composer/Command/ValidateCommand.php index 5e244feff..692b6a323 100644 --- a/src/Composer/Command/ValidateCommand.php +++ b/src/Composer/Command/ValidateCommand.php @@ -28,7 +28,7 @@ class ValidateCommand extends Command { $this ->setName('validate') - ->setDescription('validates a composer.json') + ->setDescription('Validates a composer.json') ->setDefinition(array( new InputArgument('file', InputArgument::OPTIONAL, 'path to composer.json file', './composer.json') )) diff --git a/src/Composer/Installer/LibraryInstaller.php b/src/Composer/Installer/LibraryInstaller.php index 319ac9e98..b1bdfa60d 100644 --- a/src/Composer/Installer/LibraryInstaller.php +++ b/src/Composer/Installer/LibraryInstaller.php @@ -72,7 +72,7 @@ class LibraryInstaller implements InstallerInterface */ public function isInstalled(PackageInterface $package) { - return $this->repository->hasPackage($package); + return $this->repository->hasPackage($package) && is_readable($this->getInstallPath($package)); } /** @@ -82,9 +82,16 @@ class LibraryInstaller implements InstallerInterface { $downloadPath = $this->getInstallPath($package); + // remove the binaries if it appears the package files are missing + if (!is_readable($downloadPath) && $this->repository->hasPackage($package)) { + $this->removeBinaries($package); + } + $this->downloadManager->download($package, $downloadPath); $this->installBinaries($package); - $this->repository->addPackage(clone $package); + if (!$this->repository->hasPackage($package)) { + $this->repository->addPackage(clone $package); + } } /** diff --git a/src/Composer/Package/BasePackage.php b/src/Composer/Package/BasePackage.php index 786c58ae5..d5a0e5230 100644 --- a/src/Composer/Package/BasePackage.php +++ b/src/Composer/Package/BasePackage.php @@ -15,6 +15,7 @@ namespace Composer\Package; use Composer\Package\LinkConstraint\LinkConstraintInterface; use Composer\Package\LinkConstraint\VersionConstraint; use Composer\Repository\RepositoryInterface; +use Composer\Repository\PlatformRepository; /** * Base class for packages providing name storage and default match implementation @@ -134,6 +135,16 @@ abstract class BasePackage implements PackageInterface $this->repository = $repository; } + /** + * checks if this package is a platform package + * + * @return boolean + */ + public function isPlatform() + { + return $this->getRepository() instanceof PlatformRepository; + } + /** * Returns package unique name, constructed from name, version and release type. * diff --git a/src/Composer/Package/Loader/ArrayLoader.php b/src/Composer/Package/Loader/ArrayLoader.php index 183818826..6d6f4f15d 100644 --- a/src/Composer/Package/Loader/ArrayLoader.php +++ b/src/Composer/Package/Loader/ArrayLoader.php @@ -74,6 +74,13 @@ class ArrayLoader $package->setBinaries($config['bin']); } + if (isset($config['scripts']) && is_array($config['scripts'])) { + foreach ($config['scripts'] as $event => $listeners) { + $config['scripts'][$event]= (array) $listeners; + } + $package->setScripts($config['scripts']); + } + if (!empty($config['description']) && is_string($config['description'])) { $package->setDescription($config['description']); } diff --git a/src/Composer/Package/MemoryPackage.php b/src/Composer/Package/MemoryPackage.php index 9167fde90..02222e43b 100644 --- a/src/Composer/Package/MemoryPackage.php +++ b/src/Composer/Package/MemoryPackage.php @@ -40,6 +40,7 @@ class MemoryPackage extends BasePackage protected $homepage; protected $extra = array(); protected $binaries = array(); + protected $scripts = array(); protected $requires = array(); protected $conflicts = array(); @@ -128,6 +129,22 @@ class MemoryPackage extends BasePackage return $this->binaries; } + /** + * @param array $scripts + */ + public function setScripts(array $scripts) + { + $this->scripts = $scripts; + } + + /** + * {@inheritDoc} + */ + public function getScripts() + { + return $this->scripts; + } + /** * {@inheritDoc} */ diff --git a/src/Composer/Package/PackageInterface.php b/src/Composer/Package/PackageInterface.php index 4616143b5..c8f92b581 100644 --- a/src/Composer/Package/PackageInterface.php +++ b/src/Composer/Package/PackageInterface.php @@ -152,6 +152,13 @@ interface PackageInterface */ function getDistSha1Checksum(); + /** + * Returns the scripts of this package + * + * @return array array('script name' => array('listeners')) + */ + function getScripts(); + /** * Returns the version of this package * diff --git a/src/Composer/Repository/Vcs/VcsDriver.php b/src/Composer/Repository/Vcs/VcsDriver.php index cf7af1fa9..6addf26e7 100644 --- a/src/Composer/Repository/Vcs/VcsDriver.php +++ b/src/Composer/Repository/Vcs/VcsDriver.php @@ -79,6 +79,7 @@ abstract class VcsDriver } else if (null !== $this->io->getLastUsername()) { $authStr = base64_encode($this->io->getLastUsername() . ':' . $this->io->getLastPassword()); $params['http'] = array('header' => "Authorization: Basic $authStr\r\n"); + $this->io->setAuthorization($this->url, $this->io->getLastUsername(), $this->io->getLastPassword()); } $ctx = stream_context_create($params); diff --git a/src/Composer/Script/CommandEvent.php b/src/Composer/Script/CommandEvent.php new file mode 100644 index 000000000..2526a99d7 --- /dev/null +++ b/src/Composer/Script/CommandEvent.php @@ -0,0 +1,26 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Script; + +use Composer\Composer; +use Composer\IO\IOInterface; +use Composer\Package\PackageInterface; + +/** + * The Command Event. + * + * @author François Pluchino + */ +class CommandEvent extends Event +{ +} diff --git a/src/Composer/Script/Event.php b/src/Composer/Script/Event.php new file mode 100644 index 000000000..239d494c8 --- /dev/null +++ b/src/Composer/Script/Event.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\Script; + +use Composer\Composer; +use Composer\IO\IOInterface; + +/** + * The base event class + * + * @author François Pluchino + */ +class Event +{ + /** + * @var string This event's name + */ + private $name; + + /** + * @var Composer The composer instance + */ + private $composer; + + /** + * @var IOInterface The IO instance + */ + private $io; + + /** + * Constructor. + * + * @param string $name The event name + * @param Composer $composer The composer objet + * @param IOInterface $io The IOInterface object + */ + public function __construct($name, Composer $composer, IOInterface $io) + { + $this->name = $name; + $this->composer = $composer; + $this->io = $io; + } + + /** + * Returns the event's name. + * + * @return string The event name + */ + public function getName() + { + return $this->name; + } + + /** + * Returns the composer instance. + * + * @return Composer + */ + public function getComposer() + { + return $this->composer; + } + + /** + * Returns the IO instance. + * + * @return IOInterface + */ + public function getIO() + { + return $this->io; + } +} diff --git a/src/Composer/Script/EventDispatcher.php b/src/Composer/Script/EventDispatcher.php new file mode 100644 index 000000000..049762f7c --- /dev/null +++ b/src/Composer/Script/EventDispatcher.php @@ -0,0 +1,120 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Script; + +use Composer\Json\JsonFile; +use Composer\Repository\FilesystemRepository; +use Composer\Autoload\ClassLoader; +use Composer\Package\PackageInterface; +use Composer\IO\IOInterface; +use Composer\Composer; +use Composer\DependencyResolver\Operation\OperationInterface; + +/** + * The Event Dispatcher. + * + * Example in command: + * $dispatcher = new EventDispatcher($this->getComposer(), $this->getApplication()->getIO()); + * // ... + * $dispatcher->dispatch(ScriptEvents::POST_INSTALL_CMD); + * // ... + * + * @author François Pluchino + * @author Jordi Boggiano + */ +class EventDispatcher +{ + protected $composer; + protected $io; + protected $loader; + + /** + * Constructor. + * + * @param Composer $composer The composer instance + * @param IOInterface $io The IOInterface instance + */ + public function __construct(Composer $composer, IOInterface $io) + { + $this->composer = $composer; + $this->io = $io; + $this->loader = new ClassLoader(); + $this->loader->register(); + } + + /** + * Dispatch a package event. + * + * @param string $eventName The constant in ScriptEvents + * @param OperationInterface $operation The package being installed/updated/removed + */ + public function dispatchPackageEvent($eventName, OperationInterface $operation) + { + $this->doDispatch(new PackageEvent($eventName, $this->composer, $this->io, $operation)); + } + + /** + * Dispatch a command event. + * + * @param string $eventName The constant in ScriptEvents + */ + public function dispatchCommandEvent($eventName) + { + $this->doDispatch(new CommandEvent($eventName, $this->composer, $this->io)); + } + + /** + * Triggers the listeners of an event. + * + * @param Event $event The event object to pass to the event handlers/listeners. + */ + protected function doDispatch(Event $event) + { + $listeners = $this->getListeners($event); + + foreach ($listeners as $callable) { + $className = substr($callable, 0, strpos($callable, '::')); + $methodName = substr($callable, strpos($callable, '::') + 2); + + if (!class_exists($className)) { + throw new \UnexpectedValueException('Class '.$className.' is not autoloadable, can not call '.$event->getName().' script'); + } + if (!is_callable($callable)) { + throw new \UnexpectedValueException('Method '.$callable.' is not callable, can not call '.$event->getName().' script'); + } + + $className::$methodName($event); + } + } + + /** + * @param Event $event Event object + * @return array Listeners + */ + protected function getListeners(Event $event) + { + $package = $this->composer->getPackage(); + $scripts = $package->getScripts(); + $autoload = $package->getAutoload(); + + // get namespaces in composer.json project + if (!$this->loader->getPrefixes() && isset($autoload['psr-0'])) { + krsort($autoload['psr-0']); + foreach ($autoload['psr-0'] as $ns => $path) { + $this->loader->add($ns, rtrim(getcwd().'/'.$path, '/')); + } + } + + return isset($scripts[$event->getName()]) ? $scripts[$event->getName()] : array(); + } +} diff --git a/src/Composer/Script/PackageEvent.php b/src/Composer/Script/PackageEvent.php new file mode 100644 index 000000000..7d0e39407 --- /dev/null +++ b/src/Composer/Script/PackageEvent.php @@ -0,0 +1,54 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Script; + +use Composer\Composer; +use Composer\IO\IOInterface; +use Composer\DependencyResolver\Operation\OperationInterface; + +/** + * The Package Event. + * + * @author Jordi Boggiano + */ +class PackageEvent extends Event +{ + /** + * @var OperationInterface The package instance + */ + private $operation; + + /** + * Constructor. + * + * @param string $name The event name + * @param Composer $composer The composer objet + * @param IOInterface $io The IOInterface object + * @param OperationInterface $operation The operation object + */ + public function __construct($name, Composer $composer, IOInterface $io, OperationInterface $operation) + { + parent::__construct($name, $composer, $io); + $this->operation = $operation; + } + + /** + * Returns the package instance. + * + * @return OperationInterface + */ + public function getOperation() + { + return $this->operation; + } +} diff --git a/src/Composer/Script/ScriptEvents.php b/src/Composer/Script/ScriptEvents.php new file mode 100644 index 000000000..9f5131345 --- /dev/null +++ b/src/Composer/Script/ScriptEvents.php @@ -0,0 +1,112 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Script; + +/** + * The Script Events. + * + * @author François Pluchino + * @author Jordi Boggiano + */ +class ScriptEvents +{ + /** + * The PRE_INSTALL_CMD event occurs before the install command is executed. + * + * The event listener method receives a Composer\Script\CommandEvent instance. + * + * @var string + */ + const PRE_INSTALL_CMD = 'pre-install-cmd'; + + /** + * The POST_INSTALL_CMD event occurs after the install command is executed. + * + * The event listener method receives a Composer\Script\CommandEvent instance. + * + * @var string + */ + const POST_INSTALL_CMD = 'post-install-cmd'; + + /** + * The PRE_UPDATE_CMD event occurs before the update command is executed. + * + * The event listener method receives a Composer\Script\CommandEvent instance. + * + * @var string + */ + const PRE_UPDATE_CMD = 'pre-update-cmd'; + + /** + * The POST_UPDATE_CMD event occurs after the update command is executed. + * + * The event listener method receives a Composer\Script\CommandEvent instance. + * + * @var string + */ + const POST_UPDATE_CMD = 'post-update-cmd'; + + /** + * The PRE_PACKAGE_INSTALL event occurs before a package is installed. + * + * The event listener method receives a Composer\Script\PackageEvent instance. + * + * @var string + */ + const PRE_PACKAGE_INSTALL = 'pre-package-install'; + + /** + * The POST_PACKAGE_INSTALL event occurs after a package is installed. + * + * The event listener method receives a Composer\Script\PackageEvent instance. + * + * @var string + */ + const POST_PACKAGE_INSTALL = 'post-package-install'; + + /** + * The PRE_PACKAGE_UPDATE event occurs before a package is updated. + * + * The event listener method receives a Composer\Script\PackageEvent instance. + * + * @var string + */ + const PRE_PACKAGE_UPDATE = 'pre-package-update'; + + /** + * The POST_PACKAGE_UPDATE event occurs after a package is updated. + * + * The event listener method receives a Composer\Script\PackageEvent instance. + * + * @var string + */ + const POST_PACKAGE_UPDATE = 'post-package-update'; + + /** + * The PRE_PACKAGE_UNINSTALL event occurs before a package has been uninstalled. + * + * The event listener method receives a Composer\Script\PackageEvent instance. + * + * @var string + */ + const PRE_PACKAGE_UNINSTALL = 'pre-package-uninstall'; + + /** + * The POST_PACKAGE_UNINSTALL event occurs after a package has been uninstalled. + * + * The event listener method receives a Composer\Script\PackageEvent instance. + * + * @var string + */ + const POST_PACKAGE_UNINSTALL = 'post-package-uninstall'; +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index b2743bb41..3c891e9d8 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -10,6 +10,12 @@ * file that was distributed with this source code. */ +if ((!$loader = @include __DIR__.'/../../../.composer/autoload.php') && (!$loader = @include __DIR__.'/../vendor/.composer/autoload.php')) { + die('You must set up the project dependencies, run the following commands:'.PHP_EOL. + 'curl -s http://getcomposer.org/installer | php'.PHP_EOL. + 'php composer.phar install'.PHP_EOL); +} + $loader = require __DIR__.'/../vendor/.composer/autoload.php'; $loader->add('Composer\Test', __DIR__); $loader->register();