From 6c717f2ad9fb236e9c064868a29cd0b6805109a2 Mon Sep 17 00:00:00 2001 From: Gunnar Wrobel Date: Tue, 21 Sep 2010 17:49:45 +0200 Subject: [PATCH] Improve the installer. - Start extracting the PEAR usage into its own class. - Added an output handler. - Start relying less on network access. --- components/lib/Components.php | 9 +- components/lib/Components/Config/Cli.php | 21 ++ components/lib/Components/Dependencies.php | 7 + .../lib/Components/Dependencies/Injector.php | 10 + components/lib/Components/Module/Installer.php | 18 +- components/lib/Components/Output.php | 101 +++++++ components/lib/Components/Pear/InstallLocation.php | 314 +++++++++++++++++++++ components/lib/Components/Runner/Installer.php | 102 +++---- components/package.xml | 10 + 9 files changed, 531 insertions(+), 61 deletions(-) create mode 100644 components/lib/Components/Output.php create mode 100644 components/lib/Components/Pear/InstallLocation.php diff --git a/components/lib/Components.php b/components/lib/Components.php index 9071c58a0..c6da6a1bb 100644 --- a/components/lib/Components.php +++ b/components/lib/Components.php @@ -59,8 +59,13 @@ class Components $parser->parserError($e->getMessage()); return; } - foreach ($modules as $module) { - $module->handle($config); + try { + foreach ($modules as $module) { + $module->handle($config); + } + } catch (Components_Exception $e) { + $dependencies->getOutput()->fail($e->getMessage()); + return; } } diff --git a/components/lib/Components/Config/Cli.php b/components/lib/Components/Config/Cli.php index e5bec1003..14d0fa298 100644 --- a/components/lib/Components/Config/Cli.php +++ b/components/lib/Components/Config/Cli.php @@ -59,6 +59,27 @@ implements Components_Config Horde_Argv_Parser $parser ) { $this->_parser = $parser; + + $parser->addOption( + new Horde_Argv_Option( + '-q', + '--quiet', + array( + 'action' => 'store_true', + 'help' => 'Reduce output to a minimum' + ) + ) + ); + $parser->addOption( + new Horde_Argv_Option( + '-v', + '--verbose', + array( + 'action' => 'store_true', + 'help' => 'Reduce output to a maximum' + ) + ) + ); } /** diff --git a/components/lib/Components/Dependencies.php b/components/lib/Components/Dependencies.php index a5f3f0091..229f907f9 100644 --- a/components/lib/Components/Dependencies.php +++ b/components/lib/Components/Dependencies.php @@ -35,4 +35,11 @@ interface Components_Dependencies * @return Components_Runner_Installer The installer. */ public function getRunnerInstaller(); + + /** + * Returns the output handler. + * + * @return Components_Output The output handler. + */ + public function getOutput(); } \ No newline at end of file diff --git a/components/lib/Components/Dependencies/Injector.php b/components/lib/Components/Dependencies/Injector.php index 86470188e..0cc4ca64e 100644 --- a/components/lib/Components/Dependencies/Injector.php +++ b/components/lib/Components/Dependencies/Injector.php @@ -51,4 +51,14 @@ implements Components_Dependencies { return $this->getInstance('Components_Runner_Installer'); } + + /** + * Returns the output handler. + * + * @return Components_Output The output handler. + */ + public function getOutput() + { + return $this->getInstance('Components_Output'); + } } \ No newline at end of file diff --git a/components/lib/Components/Module/Installer.php b/components/lib/Components/Module/Installer.php index 1550f68b4..1e51eb5cf 100644 --- a/components/lib/Components/Module/Installer.php +++ b/components/lib/Components/Module/Installer.php @@ -63,7 +63,23 @@ extends Components_Module_Base '--install', array( 'action' => 'store', - 'help' => 'install the element into the specified absolute INSTALL location' + 'help' => 'Install the element into the specified absolute INSTALL location' + ) + ), + new Horde_Argv_Option( + '-S', + '--sourcepath', + array( + 'action' => 'store', + 'help' => 'Location of downloaded PEAR packages. Specifying this path allows you to avoid accessing the network for installing new packages.' + ) + ), + new Horde_Argv_Option( + '-X', + '--channelxmlpath', + array( + 'action' => 'store', + 'help' => 'Location of static channel XML descriptions. These files need to be named CHANNEL.channel.xml (e.g. pear.php.net.channel.xml). Specifying this path allows you to avoid accessing the network for installing new channels. If this is not specified but SOURCEPATH is given then SOURCEPATH will be checked for such channel XML files.' ) ), ); diff --git a/components/lib/Components/Output.php b/components/lib/Components/Output.php new file mode 100644 index 000000000..d8616d4d3 --- /dev/null +++ b/components/lib/Components/Output.php @@ -0,0 +1,101 @@ + + * @license http://www.fsf.org/copyleft/lgpl.html LGPL + * @link http://pear.horde.org/index.php?package=Components + */ + +/** + * Components_Output:: handles output from the components application. + * + * Copyright 2010 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you + * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html. + * + * @category Horde + * @package Components + * @author Gunnar Wrobel + * @license http://www.fsf.org/copyleft/lgpl.html LGPL + * @link http://pear.horde.org/index.php?package=Components + */ +class Components_Output +{ + /** + * The CLI handler. + * + * @var Horde_Cli + */ + private $_cli; + + /** + * Did the user request verbose output? + * + * @var boolean + */ + private $_verbose; + + /** + * Did the user request quiet output? + * + * @var boolean + */ + private $_quiet; + + /** + * Constructor. + * + * @param Horde_Cli $cli The CLI handler. + * @param Components_Config $config The configuration for the current job. + */ + public function __construct( + Horde_Cli $cli, + Components_Config $config + ) { + $this->_cli = $cli; + $options = $config->getOptions(); + $this->_verbose = !empty($options['verbose']); + $this->_quiet = !empty($options['quiet']); + } + + public function ok($text) + { + if ($this->_quiet) { + return; + } + $this->_cli->message($text, 'cli.success'); + } + + public function warn($text) + { + if ($this->_quiet) { + return; + } + $this->_cli->message($text, 'cli.warning'); + } + + public function fail($text) + { + $this->_cli->fatal($text); + } + + public function pear($text) + { + if (!$this->_verbose) { + return; + } + $this->_cli->message('-------------------------------------------------', 'cli.message'); + $this->_cli->message('PEAR output START', 'cli.message'); + $this->_cli->message('-------------------------------------------------', 'cli.message'); + $this->_cli->writeln($text); + $this->_cli->message('-------------------------------------------------', 'cli.message'); + $this->_cli->message('PEAR output END', 'cli.message'); + $this->_cli->message('-------------------------------------------------', 'cli.message'); + } +} \ No newline at end of file diff --git a/components/lib/Components/Pear/InstallLocation.php b/components/lib/Components/Pear/InstallLocation.php new file mode 100644 index 000000000..e036690ee --- /dev/null +++ b/components/lib/Components/Pear/InstallLocation.php @@ -0,0 +1,314 @@ + + * @license http://www.fsf.org/copyleft/lgpl.html LGPL + * @link http://pear.horde.org/index.php?package=Components + */ + +/** + * Components_Pear_InstallLocation:: handles a specific PEAR installation + * location / configuration. + * + * Copyright 2010 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you + * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html. + * + * @category Horde + * @package Components + * @author Gunnar Wrobel + * @license http://www.fsf.org/copyleft/lgpl.html LGPL + * @link http://pear.horde.org/index.php?package=Components + */ +class Components_Pear_InstallLocation +{ + /** + * The output handler. + * + * @param Component_Output + */ + private $_output; + + /** + * The base directory for the PEAR install location. + * + * @param string + */ + private $_base_directory; + + /** + * The path to the configuration file. + * + * @param string + */ + private $_config_file; + + /** + * The directory that contains channel definitions. + * + * @param string + */ + private $_channel_directory; + + /** + * The directory that contains package sources. + * + * @param string + */ + private $_source_directory; + + /** + * Constructor. + * + * @param Component_Output $output The output handler. + */ + public function __construct(Components_Output $output) + { + $this->_output = $output; + } + + /** + * Set the path to the install location. + * + * @param string $base_directory The base directory for the PEAR install location. + * @param string $config_file The name of the configuration file. + * + * @return NULL + */ + public function setLocation($base_directory, $config_file) + { + $this->_base_directory = $base_directory; + if (!file_exists($this->_base_directory)) { + throw new Components_Exception( + sprintf( + 'The path to the install location (%s) does not exist! Create it first.', + $this->_base_directory + ) + ); + } + $this->_config_file = $base_directory . DIRECTORY_SEPARATOR . $config_file; + } + + /** + * Set the path to the channel directory. + * + * @param string $channel_directory The directory containing channel definitions. + * + * @return NULL + */ + public function setChannelDirectory($channel_directory) + { + $this->_channel_directory = $channel_directory; + if (!file_exists($this->_channel_directory)) { + throw new Components_Exception( + sprintf( + 'The path to the channel directory (%s) does not exist!', + $this->_channel_directory + ) + ); + } + } + + /** + * Set the path to the source directory. + * + * @param string $source_directory The directory containing PEAR packages. + * + * @return NULL + */ + public function setSourceDirectory($source_directory) + { + $this->_source_directory = $source_directory; + if (!file_exists($this->_source_directory)) { + throw new Components_Exception( + sprintf( + 'The path to the source directory (%s) does not exist!', + $this->_source_directory + ) + ); + } + } + + public function createPearConfig() + { + if (file_exists($this->_config_file)) { + throw new Components_Exception( + sprintf( + 'PEAR configuration file %s already exists!', + $this->_config_file + ) + ); + } + ob_start(); + $command_config = new PEAR_Command_Config(new PEAR_Frontend_CLI(), new stdClass); + $command_config->doConfigCreate( + 'config-create', array(), array($this->_base_directory, $this->_config_file) + ); + $this->_output->pear(ob_get_clean()); + $this->_output->ok( + sprintf( + 'Successfully created PEAR configuration %s', + $this->_config_file + ) + ); + } + + public function getPearConfig() + { + if (empty($this->_config_file)) { + throw new Components_Exception( + 'Set the path to the install location first!' + ); + } + if (!file_exists($this->_config_file)) { + $this->createPearConfig(); + } + return PEAR_Config::singleton($this->_config_file); + } + + /** + * Test if a channel exists within the install location. + * + * @param string $channel The channel name. + * + * @return boolean True if the channel exists. + */ + public function channelExists($channel) + { + $registered = $this->getPearConfig()->getRegistry()->getChannels(); + foreach ($registered as $c) { + if ($channel == $c->getName()) { + return true; + } + } + return false; + } + + /** + * Add a channel within the install location. + * + * @param string $channel The channel name. + * + * @return NULL + */ + public function addChannel($channel) + { + $channel_handler = new PEAR_Command_Channels( + new PEAR_Frontend_CLI(), + $this->getPearConfig() + ); + + $static = $this->_channel_directory . DIRECTORY_SEPARATOR + . $channel . '.channel.xml'; + if (file_exists($static)) { + ob_start(); + $channel_handler->doAdd('channel-add', array(), array($static)); + $this->_output->pear(ob_get_clean()); + } else { + $this->_output->warn( + sprintf( + 'Adding channel %s via network.', + $channel + ) + ); + ob_start(); + $channel_handler->doDiscover('channel-discover', array(), array($channel)); + $this->_output->pear(ob_get_clean()); + } + $this->_output->ok( + sprintf( + 'Successfully added channel %s', + $channel + ) + ); + } + + /** + * Ensure the specified channel exists within the install location. + * + * @param string $channel The channel name. + * + * @return NULL + */ + public function provideChannel($channel) + { + if (!$this->channelExists($channel)) { + $this->addChannel($channel); + } + } + + private function getInstallationHandler() + { + return new PEAR_Command_Install( + new PEAR_Frontend_CLI(), + $this->getPearConfig() + ); + } + + /** + * Add a package based on a source directory. + * + * @param string $package The path to the package.xml in the source directory. + * + * @return NULL + */ + public function addPackageFromSource($package) + { + $installer = $this->getInstallationHandler(); + ob_start(); + $installer->doInstall( + 'install', + array('nodeps' => true), + array($package) + ); + $this->_output->pear(ob_get_clean()); + $this->_output->ok( + sprintf( + 'Successfully added package %s', + $package + ) + ); + } + + /** + * Add a package based on a package name or package tarball. + * + * @param string $channel The channel name for the package. + * @param string $package The name of the package of the path of the tarball. + * + * @return NULL + */ + public function addPackageFromPackage($channel, $package) + { + $installer = $this->getInstallationHandler(); + $this->_output->warn( + sprintf( + 'Adding package %s via network.', + $package + ) + ); + ob_start(); + $installer->doInstall( + 'install', + array( + //'force' => true, + 'channel' => $channel, + ), + array($package) + ); + $this->_output->pear(ob_get_clean()); + $this->_output->ok( + sprintf( + 'Successfully added package %s', + $package + ) + ); + } +} diff --git a/components/lib/Components/Runner/Installer.php b/components/lib/Components/Runner/Installer.php index ffcd60ac9..0c187e770 100644 --- a/components/lib/Components/Runner/Installer.php +++ b/components/lib/Components/Runner/Installer.php @@ -34,46 +34,50 @@ class Components_Runner_Installer /** * The configuration for the current job. * - * @param Components_Config + * @var Components_Config */ private $_config; /** + * The install location. + * + * @var Components_Pear_InstallLocation + */ + private $_location; + + /** * Constructor. * * @param Components_Config $config The configuration for the current job. + * @param Components_Pear_InstallLocation $location Represents the install + * location and its + * corresponding configuration. */ - public function __construct(Components_Config $config) - { + public function __construct( + Components_Config $config, + Components_Pear_InstallLocation $location + ) { $this->_config = $config; + $this->_location = $location; } public function run() { $options = $this->_config->getOptions(); - $pear = new PEAR(); - $pear->setErrorHandling(PEAR_ERROR_DIE); - - $pearrc = $options['install'] . DIRECTORY_SEPARATOR . '.pearrc'; - $command_config = new PEAR_Command_Config(new PEAR_Frontend_CLI(), new stdClass); - $command_config->doConfigCreate( - 'config-create', array(), array($options['install'], $pearrc) - ); - - $pear_config = new PEAR_Config($pearrc); - $GLOBALS['_PEAR_Config_instance'] = $pear_config; - - $channel = new PEAR_Command_Channels( - new PEAR_Frontend_CLI(), - $pear_config - ); - $channel->doDiscover('channel-discover', array(), array('pear.horde.org')); - $channel->doDiscover('channel-discover', array(), array('pear.phpunit.de')); - - $installer = new PEAR_Command_Install( - new PEAR_Frontend_CLI(), - $pear_config - ); + $location = realpath($options['install']); + if (!$location) { + $location = $options['install']; + } + $this->_location->setLocation($location, '.pearrc'); + $pear_config = $this->_location->getPearConfig(); + if (!empty($options['channelxmlpath'])) { + $this->_location->setChannelDirectory($options['channelxmlpath']); + } else if (!empty($options['sourcepath'])) { + $this->_location->setChannelDirectory($options['sourcepath']); + } + if (!empty($options['sourcepath'])) { + $this->_location->setSourceDirectory($options['sourcepath']); + } $arguments = $this->_config->getArguments(); $element = basename(realpath($arguments[0])); @@ -82,8 +86,6 @@ class Components_Runner_Installer $this->_run = array(); $this->_installHordeDependency( - $installer, - $pear_config, $root_path, $element ); @@ -92,19 +94,11 @@ class Components_Runner_Installer /** * Install a Horde dependency from the current tree (the framework). * - * @param PEAR_Command_Install $installer Installs the dependency. - * @param PEAR_Config $pear_config The configuration of the PEAR - * environment in which the - * dependency will be installed. - * @param string $root_path Root path to the Horde framework. - * @param string $dependency Package name of the dependency. + * @param string $root_path Root path to the Horde framework. + * @param string $dependency Package name of the dependency. */ - private function _installHordeDependency( - PEAR_Command_Install $installer, - PEAR_Config $pear_config, - $root_path, - $dependency - ) { + private function _installHordeDependency($root_path, $dependency) + { $package_file = $root_path . DIRECTORY_SEPARATOR . $dependency . DIRECTORY_SEPARATOR . 'package.xml'; if (!file_exists($package_file)) { @@ -113,46 +107,38 @@ class Components_Runner_Installer } $parser = new PEAR_PackageFile_Parser_v2(); - $parser->setConfig($pear_config); + $parser->setConfig($this->_location->getPearConfig()); $pkg = $parser->parse(file_get_contents($package_file), $package_file); $dependencies = $pkg->getDeps(); foreach ($dependencies as $dependency) { if (isset($dependency['channel']) && $dependency['channel'] != 'pear.horde.org') { + $this->_location->provideChannel($dependency['channel']); $key = $dependency['channel'] . '/' . $dependency['name']; if (in_array($key, $this->_run)) { continue; } - $installer->doInstall( - 'install', - array( - 'force' => true, - 'channel' => $dependency['channel'], - ), - array($dependency['name']) + $this->_location->addPackageFromPackage( + $dependency['channel'], $dependency['name'] ); $this->_run[] = $key; } else if (isset($dependency['channel'])) { + $this->_location->provideChannel($dependency['channel']); $key = $dependency['channel'] . '/' . $dependency['name']; if (in_array($key, $this->_run)) { continue; } $this->_run[] = $key; - $this->_installHordeDependency( - $installer, - $pear_config, - $root_path, - $dependency['name'] - ); + $this->_installHordeDependency($root_path, $dependency['name']); } } if (in_array($package_file, $this->_run)) { return; } - $installer->doInstall( - 'install', - array('nodeps' => true), - array($package_file) + + $this->_location->provideChannel($pkg->getChannel()); + $this->_location->addPackageFromSource( + $package_file ); $this->_run[] = $package_file; } diff --git a/components/package.xml b/components/package.xml index ab941a051..52a8d0618 100644 --- a/components/package.xml +++ b/components/package.xml @@ -60,6 +60,9 @@ + + + @@ -72,6 +75,7 @@ + @@ -129,6 +133,10 @@ pear.horde.org + Cli + pear.horde.org + + Injector pear.horde.org @@ -153,6 +161,7 @@ + @@ -160,6 +169,7 @@ + -- 2.11.0