Source for file Installer.php
Documentation is available at Installer.php
// +----------------------------------------------------------------------+
// +----------------------------------------------------------------------+
// | Copyright (c) 1997-2004 The PHP Group |
// +----------------------------------------------------------------------+
// | This source file is subject to version 3.0 of the PHP license, |
// | that is bundled with this package in the file LICENSE, and is |
// | available through the world-wide-web at the following url: |
// | http://www.php.net/license/3_0.txt. |
// | If you did not receive a copy of the PHP license and are unable to |
// | obtain it through the world-wide-web, please send a note to |
// | license@php.net so we can mail you a copy immediately. |
// +----------------------------------------------------------------------+
// | Authors: Stig Bakken <ssb@php.net> |
// | Tomas V.V.Cox <cox@idecnet.com> |
// | Martin Jansen <mj@php.net> |
// +----------------------------------------------------------------------+
// $Id: Installer.php,v 1.150.2.2 2005/02/17 17:47:55 cellog Exp $
require_once 'PEAR/Downloader.php';
* Administration class used to install PEAR packages and maintain the
* installed package database.
* - Check dependencies break on package uninstall (when no force given)
* - add a guessInstallDest() method with the code from _installFile() and
* use that method in Registry::_rebuildFileMap() & Command_Registry::doList(),
* @author Stig Bakken <ssb@php.net>
* @author Martin Jansen <mj@php.net>
* @author Greg Beaver <cellog@php.net>
class PEAR_Installer extends PEAR_Downloader
/** name of the package directory, for example Foo-1.0
/** directory where PHP code files go
/** directory where PHP extension files go
/** directory where documentation goes
/** installation root directory (ala PHP's INSTALL_ROOT or
/** PEAR_Registry object used by the installer
/** List of file transactions queued for an install/upgrade/uninstall.
* 0 => array("rename => array("from-file", "to-file")),
* 1 => array("delete" => array("file-to-delete")),
var $file_operations = array ();
* PEAR_Installer constructor.
* @param object $ui user interface object (instance of PEAR_Frontend_*)
function PEAR_Installer (&$ui)
$this->setFrontendObject ($ui);
$this->debug = $this->config->get ('verbose');
//$this->registry = &new PEAR_Registry($this->config->get('php_dir'));
// {{{ _deletePackageFiles()
* Delete a package's installed files, does not remove empty directories.
* @param string $package package name
* @return bool TRUE on success, or a PEAR error on failure
function _deletePackageFiles ($package)
return $this->raiseError ("No package to uninstall given");
$filelist = $this->registry->packageInfo ($package, 'filelist');
return $this->raiseError (" $package not installed" );
foreach ($filelist as $file => $props) {
if (empty ($props['installed_as'])) {
$path = $this->_prependPath ($props['installed_as'], $this->installroot);
$this->addFileOperation ('delete', array ($path));
* @param array attributes from <file> tag in package.xml
* @param string path to install the file in
* @param array options from command-line
function _installFile ($file, $atts, $tmp_path, $options)
// {{{ return if this file is meant for another platform
if (isset ($atts['platform'])) {
include_once "OS/Guess.php";
if (strlen($atts['platform']) && $atts['platform']{0 } == '!') {
$platform = substr($atts['platform'], 1 );
$platform = $atts['platform'];
if ((bool) $os->matchSignature ($platform) === $negate) {
$this->log (3 , " skipped $file (meant for $atts[platform], we are ". $os->getSignature (). ")");
// {{{ assemble the destination paths
$dest_dir = $this->config->get ($atts['role'] . '_dir') .
DIRECTORY_SEPARATOR . $this->pkginfo['package'];
unset ($atts['baseinstalldir']);
$dest_dir = $this->config->get ($atts['role'] . '_dir');
$dest_dir = $this->config->get ('bin_dir');
return $this->raiseError (" Invalid role `$atts[role]' for file $file" );
$save_destdir = $dest_dir;
if (!empty ($atts['baseinstalldir'])) {
$dest_dir .= DIRECTORY_SEPARATOR . $atts['baseinstalldir'];
if (dirname($file) != '.' && empty ($atts['install-as'])) {
$dest_dir .= DIRECTORY_SEPARATOR . dirname($file);
if (empty ($atts['install-as'])) {
$dest_file = $dest_dir . DIRECTORY_SEPARATOR . basename($file);
$dest_file = $dest_dir . DIRECTORY_SEPARATOR . $atts['install-as'];
$orig_file = $tmp_path . DIRECTORY_SEPARATOR . $file;
// Clean up the DIRECTORY_SEPARATOR mess
$ds2 = DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR;
list ($dest_file, $orig_file) = preg_replace(array ('!\\\\+!', '!/!', " !$ds2+!" ),
array ($dest_file, $orig_file));
$installed_as = $dest_file;
$final_dest_file = $this->_prependPath ($dest_file, $this->installroot);
$dest_dir = dirname($final_dest_file);
$dest_file = $dest_dir . DIRECTORY_SEPARATOR . '.tmp' . basename($final_dest_file);
if (!$this->mkDirHier ($dest_dir)) {
return $this->raiseError (" failed to mkdir $dest_dir" ,
$this->log (3 , " + mkdir $dest_dir" );
if (empty ($atts['replacements'])) {
return $this->raiseError ("file does not exist",
if (!@copy($orig_file, $dest_file)) {
return $this->raiseError (" failed to write $dest_file" ,
$this->log (3 , " + cp $orig_file $dest_file" );
if (isset ($atts['md5sum'])) {
// {{{ file with replacements
return $this->raiseError ("file does not exist",
$fp = fopen($orig_file, "r");
if (isset ($atts['md5sum'])) {
$md5sum = md5($contents);
$subst_from = $subst_to = array ();
foreach ($atts['replacements'] as $a) {
if ($a['type'] == 'php-const') {
$this->log (0 , " invalid php-const replacement: $a[to]" );
} elseif ($a['type'] == 'pear-config') {
$to = $this->config->get ($a['to']);
$this->log (0 , " invalid pear-config replacement: $a[to]" );
} elseif ($a['type'] == 'package-info') {
if (isset ($this->pkginfo[$a['to']]) && is_string($this->pkginfo[$a['to']])) {
$to = $this->pkginfo[$a['to']];
$this->log (0 , " invalid package-info replacement: $a[to]" );
$subst_from[] = $a['from'];
$this->log (3 , "doing ". sizeof($subst_from)." substitution(s) for $final_dest_file" );
$contents = str_replace($subst_from, $subst_to, $contents);
$wp = @fopen($dest_file, "wb");
return $this->raiseError (" failed to create $dest_file: $php_errormsg" ,
if (!fwrite($wp, $contents)) {
return $this->raiseError (" failed writing to $dest_file: $php_errormsg" ,
$this->log (2 , " md5sum ok: $final_dest_file" );
if (empty ($options['force'])) {
return $this->raiseError (" bad md5sum for file $final_dest_file" ,
$this->log (0 , " warning : bad md5sum for file $final_dest_file" );
// {{{ set file permissions
if ($atts['role'] == 'script') {
$mode = 0777 & ~(int) octdec($this->config->get ('umask'));
$this->log (3 , " + chmod +x $dest_file" );
$mode = 0666 & ~(int) octdec($this->config->get ('umask'));
$this->addFileOperation ("chmod", array ($mode, $dest_file));
if (!@chmod($dest_file, $mode)) {
$this->log (0 , " failed to change mode of $dest_file" );
$this->addFileOperation ("rename", array ($dest_file, $final_dest_file));
// Store the full path where the file was installed for easy unistall
$this->addFileOperation ("installed_as", array ($file, $installed_as,
//$this->log(2, "installed: $dest_file");
// {{{ addFileOperation()
* Add a file operation to the current file transaction.
* @see startFileTransaction()
* @var string $type This can be one of:
* - rename: rename a file ($data has 2 values)
* - chmod: change permissions on a file ($data has 2 values)
* - delete: delete a file ($data has 1 value)
* - rmdir: delete a directory if empty ($data has 1 value)
* - installed_as: mark a file as installed ($data has 4 values).
* @var array $data For all file operations, this array must contain the
* full path to the file or directory that is being operated on. For
* the rename command, the first parameter must be the file to rename,
* the second its new name.
* The installed_as operation contains 4 elements in this order:
* 1. Filename as listed in the filelist element from package.xml
* 2. Full path to the installed file
* 3. Full path from the php_dir configuration variable used in this
* 4. Relative path from the php_dir that this file is installed in
function addFileOperation ($type, $data)
return $this->raiseError ('Internal Error: $data in addFileOperation'
. ' must be an array, was ' . gettype($data));
$this->log (3 , " adding to transaction: $type $octmode $data[1]" );
$this->log (3 , " adding to transaction: $type " . implode(" ", $data));
$this->file_operations[] = array ($type, $data);
// {{{ startFileTransaction()
function startFileTransaction ($rollback_in_case = false )
if (count($this->file_operations) && $rollback_in_case) {
$this->rollbackFileTransaction ();
$this->file_operations = array ();
// {{{ commitFileTransaction()
function commitFileTransaction ()
$n = count($this->file_operations);
$this->log (2 , " about to commit $n file operations" );
// {{{ first, check permissions and such manually
foreach ($this->file_operations as $tr) {
list ($type, $data) = $tr;
$errors[] = " cannot rename file $data[0], doesn't exist";
// check that dest dir. is writable
$errors[] = " permission denied ($type): $data[1]";
// check that file is writable
$errors[] = " permission denied ($type): $data[1] " . decoct($data[0 ]);
$this->log (2 , " warning: file $data[0] doesn't exist, can't be deleted" );
// check that directory is writable
$errors[] = " permission denied ($type): $data[0]";
foreach ($errors as $error) {
// {{{ really commit the transaction
foreach ($this->file_operations as $tr) {
list ($type, $data) = $tr;
$this->log (3 , " + mv $data[0] $data[1]" );
@chmod($data[1 ], $data[0 ]);
$this->log (3 , " + chmod $octmode $data[1]" );
$this->log (3 , " + rm $data[0]" );
$this->log (3 , " + rmdir $data[0]" );
$this->pkginfo['filelist'][$data[0 ]]['installed_as'] = $data[1 ];
if (!isset ($this->pkginfo['filelist']['dirtree'][dirname($data[1 ])])) {
$this->pkginfo['filelist']['dirtree'][dirname($data[1 ])] = true;
while (!empty ($data[3 ]) && $data[3 ] != '/' && $data[3 ] != '\\'
$this->pkginfo['filelist']['dirtree']
[$this->_prependPath ($data[3 ], $data[2 ])] = true;
$this->log (2 , " successfully committed $n file operations" );
$this->file_operations = array ();
// {{{ rollbackFileTransaction()
function rollbackFileTransaction ()
$n = count($this->file_operations);
$this->log (2 , " rolling back $n file operations" );
foreach ($this->file_operations as $tr) {
list ($type, $data) = $tr;
$this->log (3 , " + rm $data[0]" );
$this->log (3 , " + rmdir $data[0]" );
if (isset ($this->pkginfo['filelist'])) {
unset ($this->pkginfo['filelist'][$data[0 ]]['installed_as']);
if (isset ($this->pkginfo['filelist']['dirtree'][dirname($data[1 ])])) {
unset ($this->pkginfo['filelist']['dirtree'][dirname($data[1 ])]);
while (!empty ($data[3 ]) && $data[3 ] != '/' && $data[3 ] != '\\'
unset ($this->pkginfo['filelist']['dirtree']
[$this->_prependPath ($data[3 ], $data[2 ])]);
if (isset ($this->pkginfo['filelist']['dirtree'])
&& !count($this->pkginfo['filelist']['dirtree'])) {
unset ($this->pkginfo['filelist']['dirtree']);
$this->file_operations = array ();
$this->addFileOperation ('mkdir', array ($dir));
return parent ::mkDirHier ($dir);
* Download any files and their dependencies, if necessary
* @param array a mixed list of package names, local files, or package.xml
* @param array options from the command line
* @param array this is the array that will be populated with packages to
* install. Format of each entry:
* array('pkg' => 'package_name', 'file' => '/path/to/local/file',
* 'info' => array() // parsed package.xml
* @param array this will be populated with any error messages
* @param false private recursion variable
* @param false private recursion variable
* @param false private recursion variable
* @deprecated in favor of PEAR_Downloader
function download ($packages, $options, &$config, &$installpackages,
&$errors, $installed = false , $willinstall = false , $state = false )
// trickiness: initialize here
parent ::PEAR_Downloader ($this->ui, $options, $config);
$ret = parent ::download ($packages);
$errors = $this->getErrorMsgs ();
$installpackages = $this->getDownloadedPackages ();
trigger_error("PEAR Warning: PEAR_Installer::download() is deprecated " .
"in favor of PEAR_Downloader class", E_USER_WARNING );
* Installs the files within the package file specified.
* @param string $pkgfile path to the package file
* - installroot : optional prefix directory for installation
* - force : force installation
* - register-only : update registry but don't install files
* - upgrade : upgrade existing install
* - nodeps : ignore dependency conflicts/missing dependencies
* - alldeps : install all dependencies
* - onlyreqdeps : install only required dependencies
* @return array|PEAR_Errorpackage info if successful
function install ($pkgfile, $options = array ())
$php_dir = $this->config->get ('php_dir');
if (isset ($options['installroot'])) {
if (substr($options['installroot'], -1 ) == DIRECTORY_SEPARATOR ) {
$options['installroot'] = substr($options['installroot'], 0 , -1 );
$php_dir = $this->_prependPath ($php_dir, $options['installroot']);
$this->installroot = $options['installroot'];
$this->registry = &new PEAR_Registry ($php_dir);
// ==> XXX should be removed later on
$flag_old_format = false;
if (substr($pkgfile, -4 ) == '.xml') {
// {{{ Decompress pack in tmp dir -------------------------------------
// To allow relative package file names
$this->log (3 , '+ tmp dir created at ' . $tmpdir);
$tar = new Archive_Tar ($pkgfile);
if (!@$tar->extract ($tmpdir)) {
return $this->raiseError (" unable to unpack $pkgfile" );
// {{{ Look for existing package file
$descfile = $tmpdir . DIRECTORY_SEPARATOR . 'package.xml';
// ----- Look for old package archive format
// In this format the package.xml file was inside the
} while ($pkgdir{0 } == '.');
$descfile = $tmpdir . DIRECTORY_SEPARATOR . $pkgdir . DIRECTORY_SEPARATOR . 'package.xml';
$this->log (0 , "warning : you are using an archive with an old format");
// <== XXX This part should be removed later on
return $this->raiseError ("no package.xml file after extracting the archive");
// Parse xml file -----------------------------------------------
$pkginfo = $this->infoFromDescriptionFile ($descfile);
if (PEAR ::isError ($pkginfo)) {
$this->validatePackageInfo ($pkginfo, $errors, $warnings);
// XXX We allow warnings, do we have to do it?
if (empty ($options['force'])) {
return $this->raiseError ("The following errors where found (use force option to install anyway):\n".
$this->log (0 , "warning : the following errors were found:\n".
$pkgname = $pkginfo['package'];
// {{{ Check dependencies -------------------------------------------
if (isset ($pkginfo['release_deps']) && empty ($options['nodeps'])) {
$error = $this->checkDeps ($pkginfo, $dep_errors);
if (empty ($options['soft'])) {
$this->log (0 , substr($dep_errors, 1 ));
return $this->raiseError (" $pkgname: Dependencies failed" );
} else if (!empty ($dep_errors)) {
// Print optional dependencies
if (empty ($options['soft'])) {
$this->log (0 , $dep_errors);
// {{{ checks to do when not in "force" mode
if (empty ($options['force'])) {
$test = $this->registry->checkFileMap ($pkginfo);
foreach ($tmp as $file => $pkg) {
$msg = " $pkgname: conflicting files found:\n";
$fmt = " %${longest}s (%s)\n";
foreach ($test as $file => $pkg) {
$msg .= sprintf($fmt, $file, $pkg);
return $this->raiseError ($msg);
$this->startFileTransaction ();
if (empty ($options['upgrade'])) {
// checks to do only when installing new packages
if (empty ($options['force']) && $this->registry->packageExists ($pkgname)) {
return $this->raiseError (" $pkgname already installed" );
if ($this->registry->packageExists ($pkgname)) {
$v1 = $this->registry->packageInfo ($pkgname, 'version');
$v2 = $pkginfo['version'];
return $this->raiseError (" upgrade to a newer version ($v2 is not newer than $v1)" );
if (empty ($options['register-only'])) {
// when upgrading, remove old release's files first:
if (PEAR ::isError ($err = $this->_deletePackageFiles ($pkgname))) {
return $this->raiseError ($err);
// {{{ Copy files to dest dir ---------------------------------------
// info from the package it self we want to access from _installFile
$this->pkginfo = &$pkginfo;
// used to determine whether we should build any C code
if (empty ($options['register-only'])) {
return $this->raiseError ("no script destination directory\n",
if (substr($pkgfile, -4 ) != '.xml') {
$tmp_path .= DIRECTORY_SEPARATOR . $pkgname . '-' . $pkginfo['version'];
// ==> XXX This part should be removed later on
// <== XXX This part should be removed later on
foreach ($pkginfo['filelist'] as $file => $atts) {
$res = $this->_installFile ($file, $atts, $tmp_path, $options);
if (PEAR ::isError ($res)) {
if (empty ($options['ignore-errors'])) {
$this->rollbackFileTransaction ();
if ($res->getMessage () == "file does not exist") {
$this->raiseError (" file $file in package.xml does not exist" );
return $this->raiseError ($res);
$this->log (0 , "Warning: " . $res->getMessage ());
// Do not register files that were not installed
unset ($pkginfo['filelist'][$file]);
// {{{ compile and install source files
if ($this->source_files > 0 && empty ($options['nobuild'])) {
$this->log (1 , " $this->source_files source files, building" );
$bob = &new PEAR_Builder ($this->ui);
$bob->debug = $this->debug;
$built = $bob->build ($descfile, array (&$this, '_buildCallback'));
if (PEAR ::isError ($built)) {
$this->rollbackFileTransaction ();
$this->log (1 , "\nBuild process completed successfully");
foreach ($built as $ext) {
list ($_ext_name, $_ext_suff) = explode('.', $bn);
if ($_ext_suff == '.so' || $_ext_suff == '.dll') {
$this->raiseError (" Extension '$_ext_name' already loaded. " .
'Please unload it in your php.ini file ' .
'prior to install or upgrade');
$this->log (1 , " Installing '$ext[file]'" );
$copyto = $this->_prependPath ($dest, $this->installroot);
if (!$this->mkDirHier ($copydir)) {
return $this->raiseError (" failed to mkdir $copydir" ,
$this->log (3 , " + mkdir $copydir" );
if (!@copy($ext['file'], $copyto)) {
$this->log (3 , " + cp $ext[file] $copyto" );
$mode = 0666 & ~(int) octdec($this->config->get ('umask'));
$this->addFileOperation ('chmod', array ($mode, $copyto));
if (!@chmod($copyto, $mode)) {
$this->log (0 , " failed to change mode of $copyto" );
$this->addFileOperation ('rename', array ($ext['file'], $copyto));
$pkginfo['filelist'][$bn] = array (
'php_api' => $ext['php_api'],
'zend_mod_api' => $ext['zend_mod_api'],
'zend_ext_api' => $ext['zend_ext_api'],
if (!$this->commitFileTransaction ()) {
$this->rollbackFileTransaction ();
// {{{ Register that the package is installed -----------------------
if (empty ($options['upgrade'])) {
// if 'force' is used, replace the info in registry
if (!empty ($options['force']) && $this->registry->packageExists ($pkgname)) {
$this->registry->deletePackage ($pkgname);
$ret = $this->registry->addPackage ($pkgname, $pkginfo);
// new: upgrade installs a package if it isn't installed
if (!$this->registry->packageExists ($pkgname)) {
$ret = $this->registry->addPackage ($pkgname, $pkginfo);
$ret = $this->registry->updatePackage ($pkgname, $pkginfo, false );
return $this->raiseError (" Adding package $pkgname to registry failed" );
* This method removes all files installed by the application, and then
* removes any empty directories.
* @param string package name
* @param array Command-line options. Possibilities include:
* - installroot: base installation dir, if not the default
* - nodeps: do not process dependencies of other packages to ensure
* uninstallation does not break things
function uninstall ($package, $options = array ())
$php_dir = $this->config->get ('php_dir');
if (isset ($options['installroot'])) {
if (substr($options['installroot'], -1 ) == DIRECTORY_SEPARATOR ) {
$options['installroot'] = substr($options['installroot'], 0 , -1 );
$this->installroot = $options['installroot'];
$php_dir = $this->_prependPath ($php_dir, $this->installroot);
$this->registry = &new PEAR_Registry ($php_dir);
$filelist = $this->registry->packageInfo ($package, 'filelist');
return $this->raiseError (" $package not installed" );
if (empty ($options['nodeps'])) {
$depchecker = &new PEAR_Dependency ($this->registry);
$error = $depchecker->checkPackageUninstall ($errors, $warning, $package);
return $this->raiseError ($errors . 'uninstall failed');
$this->startFileTransaction ();
if (PEAR ::isError ($err = $this->_deletePackageFiles ($package))) {
$this->rollbackFileTransaction ();
return $this->raiseError ($err);
if (!$this->commitFileTransaction ()) {
$this->rollbackFileTransaction ();
return $this->raiseError ("uninstall failed");
$this->startFileTransaction ();
if (!isset ($filelist['dirtree']) || !count($filelist['dirtree'])) {
return $this->registry->deletePackage ($package);
// attempt to delete empty directories
uksort($filelist['dirtree'], array ($this, '_sortDirs'));
foreach($filelist['dirtree'] as $dir => $notused) {
$this->addFileOperation ('rmdir', array ($dir));
if (!$this->commitFileTransaction ()) {
$this->rollbackFileTransaction ();
// Register that the package is no longer installed
return $this->registry->deletePackage ($package);
function _sortDirs ($a, $b)
* Check if the package meets all dependencies
* @param array Package information (passed by reference)
* @param string Error message (passed by reference)
* @return boolean False when no error occured, otherwise true
function checkDeps (&$pkginfo, &$errors)
if (empty ($this->registry)) {
$this->registry = &new PEAR_Registry ($this->config->get ('php_dir'));
$depchecker = &new PEAR_Dependency ($this->registry);
$failed_deps = $optional_deps = array ();
if (is_array($pkginfo['release_deps'])) {
foreach($pkginfo['release_deps'] as $dep) {
$code = $depchecker->callCheckMethod ($error, $dep);
if (isset ($dep['optional']) && $dep['optional'] == 'yes') {
$optional_deps[] = array ($dep, $code, $error);
$failed_deps[] = array ($dep, $code, $error);
// {{{ failed dependencies
$n = count($failed_deps);
for ($i = 0; $i < $n; $i++ ) {
if (isset ($failed_deps[$i]['type'])) {
$type = $failed_deps[$i]['type'];
switch ($failed_deps[$i][1 ]) {
$errors .= "\n" . $failed_deps[$i][2 ];
$errors .= "\n" . $failed_deps[$i][2 ];
$errors .= "\n" . $failed_deps[$i][2 ];
// {{{ optional dependencies
$count_optional = count($optional_deps);
if ($count_optional > 0 ) {
$errors = "Optional dependencies:";
for ($i = 0; $i < $count_optional; $i++ ) {
if (isset ($optional_deps[$i]['type'])) {
$type = $optional_deps[$i]['type'];
switch ($optional_deps[$i][1 ]) {
$errors .= "\n" . $optional_deps[$i][2 ];
function _buildCallback ($what, $data)
if (($what == 'cmdoutput' && $this->debug > 1 ) ||
($what == 'output' && $this->debug > 0 )) {
$this->ui->outputData (rtrim($data), 'build');
// {{{ md5_file() utility function
$fp = fopen($filename, "r");
Documentation generated on Mon, 11 Mar 2019 14:23:58 -0400 by phpDocumentor 1.4.4. PEAR Logo Copyright © PHP Group 2004.
|