3 * PEAR_DependencyDB, advanced installed packages dependency database
9 * @author Tomas V. V. Cox <cox@idecnet.com>
10 * @author Greg Beaver <cellog@php.net>
11 * @copyright 1997-2009 The Authors
12 * @license http://opensource.org/licenses/bsd-license.php New BSD License
13 * @link http://pear.php.net/package/PEAR
14 * @since File available since Release 1.4.0a1
18 * Needed for error handling
20 require_once 'PEAR.php';
21 require_once 'PEAR/Config.php';
23 $GLOBALS['_PEAR_DEPENDENCYDB_INSTANCE'] = array();
25 * Track dependency relationships between installed packages
28 * @author Greg Beaver <cellog@php.net>
29 * @author Tomas V.V.Cox <cox@idec.net.com>
30 * @copyright 1997-2009 The Authors
31 * @license http://opensource.org/licenses/bsd-license.php New BSD License
32 * @version Release: 1.10.1
33 * @link http://pear.php.net/package/PEAR
34 * @since Class available since Release 1.4.0a1
36 class PEAR_DependencyDB
41 * This is initialized by {@link setConfig()}
47 * This is initialized by {@link setConfig()}
53 * Filename of the dependency DB (usually .depdb)
59 * File name of the lockfile (usually .depdblock)
63 var $_lockfile = false;
65 * Open file resource for locking the lockfile
71 * API version of this class, used to validate a file on-disk
75 var $_version = '1.0';
77 * Cached dependency database file
87 * Get a raw dependency database. Calls setConfig() and assertDepsDB()
89 * @param string|false full path to the dependency database, or false to use default
90 * @return PEAR_DependencyDB|PEAR_Error
92 public static function &singleton(&$config, $depdb = false)
94 $phpdir = $config->get('php_dir', null, 'pear.php.net');
95 if (!isset($GLOBALS['_PEAR_DEPENDENCYDB_INSTANCE'][$phpdir])) {
96 $a = new PEAR_DependencyDB;
97 $GLOBALS['_PEAR_DEPENDENCYDB_INSTANCE'][$phpdir] = &$a;
98 $a->setConfig($config, $depdb);
99 $e = $a->assertDepsDB();
100 if (PEAR::isError($e)) {
105 return $GLOBALS['_PEAR_DEPENDENCYDB_INSTANCE'][$phpdir];
109 * Set up the registry/location of dependency DB
110 * @param PEAR_Config|false
111 * @param string|false full path to the dependency database, or false to use default
113 function setConfig(&$config, $depdb = false)
116 $this->_config = &PEAR_Config::singleton();
118 $this->_config = &$config;
121 $this->_registry = &$this->_config->getRegistry();
123 $dir = $this->_config->get('metadata_dir', null, 'pear.php.net');
125 $dir = $this->_config->get('php_dir', null, 'pear.php.net');
127 $this->_depdb = $dir . DIRECTORY_SEPARATOR . '.depdb';
129 $this->_depdb = $depdb;
132 $this->_lockfile = dirname($this->_depdb) . DIRECTORY_SEPARATOR . '.depdblock';
136 function hasWriteAccess()
138 if (!file_exists($this->_depdb)) {
139 $dir = $this->_depdb;
140 while ($dir && $dir != '.') {
141 $dir = dirname($dir); // cd ..
142 if ($dir != '.' && file_exists($dir)) {
143 if (is_writeable($dir)) {
154 return is_writeable($this->_depdb);
157 // {{{ assertDepsDB()
160 * Create the dependency database, if it doesn't exist. Error if the database is
161 * newer than the code reading it.
162 * @return void|PEAR_Error
164 function assertDepsDB()
166 if (!is_file($this->_depdb)) {
171 $depdb = $this->_getDepDB();
172 // Datatype format has been changed, rebuild the Deps DB
173 if ($depdb['_version'] < $this->_version) {
177 if ($depdb['_version']{0} > $this->_version{0}) {
178 return PEAR::raiseError('Dependency database is version ' .
179 $depdb['_version'] . ', and we are version ' .
180 $this->_version . ', cannot continue');
185 * Get a list of installed packages that depend on this package
186 * @param PEAR_PackageFile_v1|PEAR_PackageFile_v2|array
187 * @return array|false
189 function getDependentPackages(&$pkg)
191 $data = $this->_getDepDB();
192 if (is_object($pkg)) {
193 $channel = strtolower($pkg->getChannel());
194 $package = strtolower($pkg->getPackage());
196 $channel = strtolower($pkg['channel']);
197 $package = strtolower($pkg['package']);
200 if (isset($data['packages'][$channel][$package])) {
201 return $data['packages'][$channel][$package];
208 * Get a list of the actual dependencies of installed packages that depend on
210 * @param PEAR_PackageFile_v1|PEAR_PackageFile_v2|array
211 * @return array|false
213 function getDependentPackageDependencies(&$pkg)
215 $data = $this->_getDepDB();
216 if (is_object($pkg)) {
217 $channel = strtolower($pkg->getChannel());
218 $package = strtolower($pkg->getPackage());
220 $channel = strtolower($pkg['channel']);
221 $package = strtolower($pkg['package']);
224 $depend = $this->getDependentPackages($pkg);
229 $dependencies = array();
230 foreach ($depend as $info) {
231 $temp = $this->getDependencies($info);
232 foreach ($temp as $dep) {
234 isset($dep['dep'], $dep['dep']['channel'], $dep['dep']['name']) &&
235 strtolower($dep['dep']['channel']) == $channel &&
236 strtolower($dep['dep']['name']) == $package
238 if (!isset($dependencies[$info['channel']])) {
239 $dependencies[$info['channel']] = array();
242 if (!isset($dependencies[$info['channel']][$info['package']])) {
243 $dependencies[$info['channel']][$info['package']] = array();
245 $dependencies[$info['channel']][$info['package']][] = $dep;
250 return $dependencies;
254 * Get a list of dependencies of this installed package
255 * @param PEAR_PackageFile_v1|PEAR_PackageFile_v2|array
256 * @return array|false
258 function getDependencies(&$pkg)
260 if (is_object($pkg)) {
261 $channel = strtolower($pkg->getChannel());
262 $package = strtolower($pkg->getPackage());
264 $channel = strtolower($pkg['channel']);
265 $package = strtolower($pkg['package']);
268 $data = $this->_getDepDB();
269 if (isset($data['dependencies'][$channel][$package])) {
270 return $data['dependencies'][$channel][$package];
277 * Determine whether $parent depends on $child, near or deep
278 * @param array|PEAR_PackageFile_v2|PEAR_PackageFile_v2
279 * @param array|PEAR_PackageFile_v2|PEAR_PackageFile_v2
281 function dependsOn($parent, $child)
285 return $this->_dependsOn($parent, $child, $c);
288 function _dependsOn($parent, $child, &$checked)
290 if (is_object($parent)) {
291 $channel = strtolower($parent->getChannel());
292 $package = strtolower($parent->getPackage());
294 $channel = strtolower($parent['channel']);
295 $package = strtolower($parent['package']);
298 if (is_object($child)) {
299 $depchannel = strtolower($child->getChannel());
300 $deppackage = strtolower($child->getPackage());
302 $depchannel = strtolower($child['channel']);
303 $deppackage = strtolower($child['package']);
306 if (isset($checked[$channel][$package][$depchannel][$deppackage])) {
307 return false; // avoid endless recursion
310 $checked[$channel][$package][$depchannel][$deppackage] = true;
311 if (!isset($this->_cache['dependencies'][$channel][$package])) {
315 foreach ($this->_cache['dependencies'][$channel][$package] as $info) {
316 if (isset($info['dep']['uri'])) {
317 if (is_object($child)) {
318 if ($info['dep']['uri'] == $child->getURI()) {
321 } elseif (isset($child['uri'])) {
322 if ($info['dep']['uri'] == $child['uri']) {
329 if (strtolower($info['dep']['channel']) == $depchannel &&
330 strtolower($info['dep']['name']) == $deppackage) {
335 foreach ($this->_cache['dependencies'][$channel][$package] as $info) {
336 if (isset($info['dep']['uri'])) {
337 if ($this->_dependsOn(array(
338 'uri' => $info['dep']['uri'],
339 'package' => $info['dep']['name']), $child, $checked)) {
343 if ($this->_dependsOn(array(
344 'channel' => $info['dep']['channel'],
345 'package' => $info['dep']['name']), $child, $checked)) {
355 * Register dependencies of a package that is being installed or upgraded
356 * @param PEAR_PackageFile_v2|PEAR_PackageFile_v2
358 function installPackage(&$package)
360 $data = $this->_getDepDB();
361 unset($this->_cache);
362 $this->_setPackageDeps($data, $package);
363 $this->_writeDepDB($data);
367 * Remove dependencies of a package that is being uninstalled, or upgraded.
369 * Upgraded packages first uninstall, then install
370 * @param PEAR_PackageFile_v1|PEAR_PackageFile_v2|array If an array, then it must have
371 * indices 'channel' and 'package'
373 function uninstallPackage(&$pkg)
375 $data = $this->_getDepDB();
376 unset($this->_cache);
377 if (is_object($pkg)) {
378 $channel = strtolower($pkg->getChannel());
379 $package = strtolower($pkg->getPackage());
381 $channel = strtolower($pkg['channel']);
382 $package = strtolower($pkg['package']);
385 if (!isset($data['dependencies'][$channel][$package])) {
389 foreach ($data['dependencies'][$channel][$package] as $dep) {
391 $depchannel = isset($dep['dep']['uri']) ? '__uri' : strtolower($dep['dep']['channel']);
392 $depname = strtolower($dep['dep']['name']);
393 if (isset($data['packages'][$depchannel][$depname])) {
394 foreach ($data['packages'][$depchannel][$depname] as $i => $info) {
395 if ($info['channel'] == $channel && $info['package'] == $package) {
403 unset($data['packages'][$depchannel][$depname][$i]);
404 if (!count($data['packages'][$depchannel][$depname])) {
405 unset($data['packages'][$depchannel][$depname]);
406 if (!count($data['packages'][$depchannel])) {
407 unset($data['packages'][$depchannel]);
410 $data['packages'][$depchannel][$depname] =
411 array_values($data['packages'][$depchannel][$depname]);
416 unset($data['dependencies'][$channel][$package]);
417 if (!count($data['dependencies'][$channel])) {
418 unset($data['dependencies'][$channel]);
421 if (!count($data['dependencies'])) {
422 unset($data['dependencies']);
425 if (!count($data['packages'])) {
426 unset($data['packages']);
429 $this->_writeDepDB($data);
433 * Rebuild the dependency DB by reading registry entries.
434 * @return true|PEAR_Error
438 $depdb = array('_version' => $this->_version);
439 if (!$this->hasWriteAccess()) {
440 // allow startup for read-only with older Registry
444 $packages = $this->_registry->listAllPackages();
445 if (PEAR::isError($packages)) {
449 foreach ($packages as $channel => $ps) {
450 foreach ($ps as $package) {
451 $package = $this->_registry->getPackage($package, $channel);
452 if (PEAR::isError($package)) {
455 $this->_setPackageDeps($depdb, $package);
459 $error = $this->_writeDepDB($depdb);
460 if (PEAR::isError($error)) {
464 $this->_cache = $depdb;
469 * Register usage of the dependency DB to prevent race conditions
470 * @param int one of the LOCK_* constants
471 * @return true|PEAR_Error
474 function _lock($mode = LOCK_EX)
476 if (stristr(php_uname(), 'Windows 9')) {
480 if ($mode != LOCK_UN && is_resource($this->_lockFp)) {
481 // XXX does not check type of lock (LOCK_SH/LOCK_EX)
486 // XXX People reported problems with LOCK_SH and 'w'
487 if ($mode === LOCK_SH) {
488 if (!file_exists($this->_lockfile)) {
489 touch($this->_lockfile);
490 } elseif (!is_file($this->_lockfile)) {
491 return PEAR::raiseError('could not create Dependency lock file, ' .
492 'it exists and is not a regular file');
497 if (!is_resource($this->_lockFp)) {
498 $this->_lockFp = @fopen($this->_lockfile, $open_mode);
501 if (!is_resource($this->_lockFp)) {
502 return PEAR::raiseError("could not create Dependency lock file" .
503 (isset($php_errormsg) ? ": " . $php_errormsg : ""));
506 if (!(int)flock($this->_lockFp, $mode)) {
508 case LOCK_SH: $str = 'shared'; break;
509 case LOCK_EX: $str = 'exclusive'; break;
510 case LOCK_UN: $str = 'unlock'; break;
511 default: $str = 'unknown'; break;
514 return PEAR::raiseError("could not acquire $str lock ($this->_lockfile)");
521 * Release usage of dependency DB
522 * @return true|PEAR_Error
527 $ret = $this->_lock(LOCK_UN);
528 if (is_resource($this->_lockFp)) {
529 fclose($this->_lockFp);
531 $this->_lockFp = null;
536 * Load the dependency database from disk, or return the cache
537 * @return array|PEAR_Error
541 if (!$this->hasWriteAccess()) {
542 return array('_version' => $this->_version);
545 if (isset($this->_cache)) {
546 return $this->_cache;
549 if (!$fp = fopen($this->_depdb, 'r')) {
550 $err = PEAR::raiseError("Could not open dependencies file `".$this->_depdb."'");
556 $data = unserialize(file_get_contents($this->_depdb));
557 $this->_cache = $data;
562 * Write out the dependency database to disk
563 * @param array the database
564 * @return true|PEAR_Error
567 function _writeDepDB(&$deps)
569 if (PEAR::isError($e = $this->_lock(LOCK_EX))) {
573 if (!$fp = fopen($this->_depdb, 'wb')) {
575 return PEAR::raiseError("Could not open dependencies file `".$this->_depdb."' for writing");
578 fwrite($fp, serialize($deps));
581 $this->_cache = $deps;
586 * Register all dependencies from a package in the dependencies database, in essence
587 * "installing" the package's dependency information
588 * @param array the database
589 * @param PEAR_PackageFile_v1|PEAR_PackageFile_v2
592 function _setPackageDeps(&$data, &$pkg)
594 $pkg->setConfig($this->_config);
595 if ($pkg->getPackagexmlVersion() == '1.0') {
596 $gen = &$pkg->getDefaultGenerator();
597 $deps = $gen->dependenciesToV2();
599 $deps = $pkg->getDeps(true);
606 if (!is_array($data)) {
610 if (!isset($data['dependencies'])) {
611 $data['dependencies'] = array();
614 $channel = strtolower($pkg->getChannel());
615 $package = strtolower($pkg->getPackage());
617 if (!isset($data['dependencies'][$channel])) {
618 $data['dependencies'][$channel] = array();
621 $data['dependencies'][$channel][$package] = array();
622 if (isset($deps['required']['package'])) {
623 if (!isset($deps['required']['package'][0])) {
624 $deps['required']['package'] = array($deps['required']['package']);
627 foreach ($deps['required']['package'] as $dep) {
628 $this->_registerDep($data, $pkg, $dep, 'required');
632 if (isset($deps['optional']['package'])) {
633 if (!isset($deps['optional']['package'][0])) {
634 $deps['optional']['package'] = array($deps['optional']['package']);
637 foreach ($deps['optional']['package'] as $dep) {
638 $this->_registerDep($data, $pkg, $dep, 'optional');
642 if (isset($deps['required']['subpackage'])) {
643 if (!isset($deps['required']['subpackage'][0])) {
644 $deps['required']['subpackage'] = array($deps['required']['subpackage']);
647 foreach ($deps['required']['subpackage'] as $dep) {
648 $this->_registerDep($data, $pkg, $dep, 'required');
652 if (isset($deps['optional']['subpackage'])) {
653 if (!isset($deps['optional']['subpackage'][0])) {
654 $deps['optional']['subpackage'] = array($deps['optional']['subpackage']);
657 foreach ($deps['optional']['subpackage'] as $dep) {
658 $this->_registerDep($data, $pkg, $dep, 'optional');
662 if (isset($deps['group'])) {
663 if (!isset($deps['group'][0])) {
664 $deps['group'] = array($deps['group']);
667 foreach ($deps['group'] as $group) {
668 if (isset($group['package'])) {
669 if (!isset($group['package'][0])) {
670 $group['package'] = array($group['package']);
673 foreach ($group['package'] as $dep) {
674 $this->_registerDep($data, $pkg, $dep, 'optional',
675 $group['attribs']['name']);
679 if (isset($group['subpackage'])) {
680 if (!isset($group['subpackage'][0])) {
681 $group['subpackage'] = array($group['subpackage']);
684 foreach ($group['subpackage'] as $dep) {
685 $this->_registerDep($data, $pkg, $dep, 'optional',
686 $group['attribs']['name']);
692 if ($data['dependencies'][$channel][$package] == array()) {
693 unset($data['dependencies'][$channel][$package]);
694 if (!count($data['dependencies'][$channel])) {
695 unset($data['dependencies'][$channel]);
701 * @param array the database
702 * @param PEAR_PackageFile_v1|PEAR_PackageFile_v2
703 * @param array the specific dependency
704 * @param required|optional whether this is a required or an optional dep
705 * @param string|false dependency group this dependency is from, or false for ordinary dep
707 function _registerDep(&$data, &$pkg, $dep, $type, $group = false)
715 $dep = array_map('strtolower', $dep);
716 $depchannel = isset($dep['channel']) ? $dep['channel'] : '__uri';
717 if (!isset($data['dependencies'])) {
718 $data['dependencies'] = array();
721 $channel = strtolower($pkg->getChannel());
722 $package = strtolower($pkg->getPackage());
724 if (!isset($data['dependencies'][$channel])) {
725 $data['dependencies'][$channel] = array();
728 if (!isset($data['dependencies'][$channel][$package])) {
729 $data['dependencies'][$channel][$package] = array();
732 $data['dependencies'][$channel][$package][] = $info;
733 if (isset($data['packages'][$depchannel][$dep['name']])) {
735 foreach ($data['packages'][$depchannel][$dep['name']] as $i => $p) {
736 if ($p['channel'] == $channel && $p['package'] == $package) {
742 if (!isset($data['packages'])) {
743 $data['packages'] = array();
746 if (!isset($data['packages'][$depchannel])) {
747 $data['packages'][$depchannel] = array();
750 if (!isset($data['packages'][$depchannel][$dep['name']])) {
751 $data['packages'][$depchannel][$dep['name']] = array();
758 $data['packages'][$depchannel][$dep['name']][] = array(
759 'channel' => $channel,
760 'package' => $package