Improve the installer.
authorGunnar Wrobel <p@rdus.de>
Tue, 21 Sep 2010 15:49:45 +0000 (17:49 +0200)
committerGunnar Wrobel <p@rdus.de>
Tue, 21 Sep 2010 16:06:56 +0000 (18:06 +0200)
 - Start extracting the PEAR usage into its own class.
 - Added an output handler.
 - Start relying less on network access.

components/lib/Components.php
components/lib/Components/Config/Cli.php
components/lib/Components/Dependencies.php
components/lib/Components/Dependencies/Injector.php
components/lib/Components/Module/Installer.php
components/lib/Components/Output.php [new file with mode: 0644]
components/lib/Components/Pear/InstallLocation.php [new file with mode: 0644]
components/lib/Components/Runner/Installer.php
components/package.xml

index 9071c58..c6da6a1 100644 (file)
@@ -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;
         }
     }
 
index e5bec10..14d0fa2 100644 (file)
@@ -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'
+                )
+            )
+        );
     }
 
     /**
index a5f3f00..229f907 100644 (file)
@@ -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
index 8647018..0cc4ca6 100644 (file)
@@ -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
index 1550f68..1e51eb5 100644 (file)
@@ -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 (file)
index 0000000..d8616d4
--- /dev/null
@@ -0,0 +1,101 @@
+<?php
+/**
+ * Components_Output:: handles output from the components application.
+ *
+ * PHP version 5
+ *
+ * @category Horde
+ * @package  Components
+ * @author   Gunnar Wrobel <wrobel@pardus.de>
+ * @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 <wrobel@pardus.de>
+ * @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 (file)
index 0000000..e036690
--- /dev/null
@@ -0,0 +1,314 @@
+<?php
+/**
+ * Components_Pear_InstallLocation:: handles a specific PEAR installation
+ * location / configuration.
+ *
+ * PHP version 5
+ *
+ * @category Horde
+ * @package  Components
+ * @author   Gunnar Wrobel <wrobel@pardus.de>
+ * @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 <wrobel@pardus.de>
+ * @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
+            )
+        );
+    }
+}
index ffcd60a..0c187e7 100644 (file)
@@ -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;
     }
index ab941a0..52a8d06 100644 (file)
@@ -60,6 +60,9 @@
       <file name="Installer.php" role="php" />
       <file name="PearPackageXml.php" role="php" />
      </dir> <!-- /lib/Components/Module -->
+     <dir name="Pear">
+      <file name="InstallLocation.php" role="php" />
+     </dir> <!-- /lib/Components/Pear -->
      <dir name="Runner">
       <file name="Installer.php" role="php" />
      </dir> <!-- /lib/Components/Runner -->
@@ -72,6 +75,7 @@
      <file name="Exception.php" role="php" />
      <file name="Module.php" role="php" />
      <file name="Modules.php" role="php" />
+     <file name="Output.php" role="php" />
     </dir> <!-- /lib/Components -->
     <file name="Components.php" role="php" />
    </dir> <!-- /lib -->
     <channel>pear.horde.org</channel>
    </package>
    <package>
+    <name>Cli</name>
+    <channel>pear.horde.org</channel>
+   </package>
+   <package>
     <name>Injector</name>
     <channel>pear.horde.org</channel>
    </package>
    <install as="Components/Exception.php" name="lib/Components/Exception.php" />
    <install as="Components/Module.php" name="lib/Components/Module.php" />
    <install as="Components/Modules.php" name="lib/Components/Modules.php" />
+   <install as="Components/Output.php" name="lib/Components/Output.php" />
    <install as="Components/Config/Cli.php" name="lib/Components/Config/Cli.php" />
    <install as="Components/Dependencies/Injector.php" name="lib/Components/Dependencies/Injector.php" />
    <install as="Components/Module/Base.php" name="lib/Components/Module/Base.php" />
    <install as="Components/Module/DevPackage.php" name="lib/Components/Module/DevPackage.php" />
    <install as="Components/Module/Installer.php" name="lib/Components/Module/Installer.php" />
    <install as="Components/Module/PearPackageXml.php" name="lib/Components/Module/PearPackageXml.php" />
+   <install as="Components/Pear/InstallLocation.php" name="lib/Components/Pear/InstallLocation.php" />
    <install as="Components/Runner/Installer.php" name="lib/Components/Runner/Installer.php" />
    <install as="horde-components" name="script/horde-components.php" />
    <install as="Components/AllTests.php" name="test/Components/AllTests.php" />