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 * @version CVS: $Id: DependencyDB.php 313023 2011-07-06 19:17:11Z dufuz $
14 * @link http://pear.php.net/package/PEAR
15 * @since File available since Release 1.4.0a1
19 * Needed for error handling
21 require_once 'PEAR.php';
22 require_once 'PEAR/Config.php';
24 $GLOBALS['_PEAR_DEPENDENCYDB_INSTANCE'] = array();
26 * Track dependency relationships between installed packages
29 * @author Greg Beaver <cellog@php.net>
30 * @author Tomas V.V.Cox <cox@idec.net.com>
31 * @copyright 1997-2009 The Authors
32 * @license http://opensource.org/licenses/bsd-license.php New BSD License
33 * @version Release: 1.9.4
34 * @link http://pear.php.net/package/PEAR
35 * @since Class available since Release 1.4.0a1
37 class PEAR_DependencyDB
42 * This is initialized by {@link setConfig()}
48 * This is initialized by {@link setConfig()}
54 * Filename of the dependency DB (usually .depdb)
60 * File name of the lockfile (usually .depdblock)
64 var $_lockfile = false;
66 * Open file resource for locking the lockfile
72 * API version of this class, used to validate a file on-disk
76 var $_version = '1.0';
78 * Cached dependency database file
88 * Get a raw dependency database. Calls setConfig() and assertDepsDB()
90 * @param string|false full path to the dependency database, or false to use default
91 * @return PEAR_DependencyDB|PEAR_Error
94 function &singleton(&$config, $depdb = false)
96 $phpdir = $config->get('php_dir', null, 'pear.php.net');
97 if (!isset($GLOBALS['_PEAR_DEPENDENCYDB_INSTANCE'][$phpdir])) {
98 $a = new PEAR_DependencyDB;
99 $GLOBALS['_PEAR_DEPENDENCYDB_INSTANCE'][$phpdir] = &$a;
100 $a->setConfig($config, $depdb);
101 $e = $a->assertDepsDB();
102 if (PEAR::isError($e)) {
107 return $GLOBALS['_PEAR_DEPENDENCYDB_INSTANCE'][$phpdir];
111 * Set up the registry/location of dependency DB
112 * @param PEAR_Config|false
113 * @param string|false full path to the dependency database, or false to use default
115 function setConfig(&$config, $depdb = false)
118 $this->_config = &PEAR_Config::singleton();
120 $this->_config = &$config;
123 $this->_registry = &$this->_config->getRegistry();
125 $this->_depdb = $this->_config->get('php_dir', null, 'pear.php.net') .
126 DIRECTORY_SEPARATOR . '.depdb';
128 $this->_depdb = $depdb;
131 $this->_lockfile = dirname($this->_depdb) . DIRECTORY_SEPARATOR . '.depdblock';
135 function hasWriteAccess()
137 if (!file_exists($this->_depdb)) {
138 $dir = $this->_depdb;
139 while ($dir && $dir != '.') {
140 $dir = dirname($dir); // cd ..
141 if ($dir != '.' && file_exists($dir)) {
142 if (is_writeable($dir)) {
153 return is_writeable($this->_depdb);
156 // {{{ assertDepsDB()
159 * Create the dependency database, if it doesn't exist. Error if the database is
160 * newer than the code reading it.
161 * @return void|PEAR_Error
163 function assertDepsDB()
165 if (!is_file($this->_depdb)) {
170 $depdb = $this->_getDepDB();
171 // Datatype format has been changed, rebuild the Deps DB
172 if ($depdb['_version'] < $this->_version) {
176 if ($depdb['_version']{0} > $this->_version{0}) {
177 return PEAR::raiseError('Dependency database is version ' .
178 $depdb['_version'] . ', and we are version ' .
179 $this->_version . ', cannot continue');
184 * Get a list of installed packages that depend on this package
185 * @param PEAR_PackageFile_v1|PEAR_PackageFile_v2|array
186 * @return array|false
188 function getDependentPackages(&$pkg)
190 $data = $this->_getDepDB();
191 if (is_object($pkg)) {
192 $channel = strtolower($pkg->getChannel());
193 $package = strtolower($pkg->getPackage());
195 $channel = strtolower($pkg['channel']);
196 $package = strtolower($pkg['package']);
199 if (isset($data['packages'][$channel][$package])) {
200 return $data['packages'][$channel][$package];
207 * Get a list of the actual dependencies of installed packages that depend on
209 * @param PEAR_PackageFile_v1|PEAR_PackageFile_v2|array
210 * @return array|false
212 function getDependentPackageDependencies(&$pkg)
214 $data = $this->_getDepDB();
215 if (is_object($pkg)) {
216 $channel = strtolower($pkg->getChannel());
217 $package = strtolower($pkg->getPackage());
219 $channel = strtolower($pkg['channel']);
220 $package = strtolower($pkg['package']);
223 $depend = $this->getDependentPackages($pkg);
228 $dependencies = array();
229 foreach ($depend as $info) {
230 $temp = $this->getDependencies($info);
231 foreach ($temp as $dep) {
233 isset($dep['dep'], $dep['dep']['channel'], $dep['dep']['name']) &&
234 strtolower($dep['dep']['channel']) == $channel &&
235 strtolower($dep['dep']['name']) == $package
237 if (!isset($dependencies[$info['channel']])) {
238 $dependencies[$info['channel']] = array();
241 if (!isset($dependencies[$info['channel']][$info['package']])) {
242 $dependencies[$info['channel']][$info['package']] = array();
244 $dependencies[$info['channel']][$info['package']][] = $dep;
249 return $dependencies;
253 * Get a list of dependencies of this installed package
254 * @param PEAR_PackageFile_v1|PEAR_PackageFile_v2|array
255 * @return array|false
257 function getDependencies(&$pkg)
259 if (is_object($pkg)) {
260 $channel = strtolower($pkg->getChannel());
261 $package = strtolower($pkg->getPackage());
263 $channel = strtolower($pkg['channel']);
264 $package = strtolower($pkg['package']);
267 $data = $this->_getDepDB();
268 if (isset($data['dependencies'][$channel][$package])) {
269 return $data['dependencies'][$channel][$package];
276 * Determine whether $parent depends on $child, near or deep
277 * @param array|PEAR_PackageFile_v2|PEAR_PackageFile_v2
278 * @param array|PEAR_PackageFile_v2|PEAR_PackageFile_v2
280 function dependsOn($parent, $child)
284 return $this->_dependsOn($parent, $child, $c);
287 function _dependsOn($parent, $child, &$checked)
289 if (is_object($parent)) {
290 $channel = strtolower($parent->getChannel());
291 $package = strtolower($parent->getPackage());
293 $channel = strtolower($parent['channel']);
294 $package = strtolower($parent['package']);
297 if (is_object($child)) {
298 $depchannel = strtolower($child->getChannel());
299 $deppackage = strtolower($child->getPackage());
301 $depchannel = strtolower($child['channel']);
302 $deppackage = strtolower($child['package']);
305 if (isset($checked[$channel][$package][$depchannel][$deppackage])) {
306 return false; // avoid endless recursion
309 $checked[$channel][$package][$depchannel][$deppackage] = true;
310 if (!isset($this->_cache['dependencies'][$channel][$package])) {
314 foreach ($this->_cache['dependencies'][$channel][$package] as $info) {
315 if (isset($info['dep']['uri'])) {
316 if (is_object($child)) {
317 if ($info['dep']['uri'] == $child->getURI()) {
320 } elseif (isset($child['uri'])) {
321 if ($info['dep']['uri'] == $child['uri']) {
328 if (strtolower($info['dep']['channel']) == $depchannel &&
329 strtolower($info['dep']['name']) == $deppackage) {
334 foreach ($this->_cache['dependencies'][$channel][$package] as $info) {
335 if (isset($info['dep']['uri'])) {
336 if ($this->_dependsOn(array(
337 'uri' => $info['dep']['uri'],
338 'package' => $info['dep']['name']), $child, $checked)) {
342 if ($this->_dependsOn(array(
343 'channel' => $info['dep']['channel'],
344 'package' => $info['dep']['name']), $child, $checked)) {
354 * Register dependencies of a package that is being installed or upgraded
355 * @param PEAR_PackageFile_v2|PEAR_PackageFile_v2
357 function installPackage(&$package)
359 $data = $this->_getDepDB();
360 unset($this->_cache);
361 $this->_setPackageDeps($data, $package);
362 $this->_writeDepDB($data);
366 * Remove dependencies of a package that is being uninstalled, or upgraded.
368 * Upgraded packages first uninstall, then install
369 * @param PEAR_PackageFile_v1|PEAR_PackageFile_v2|array If an array, then it must have
370 * indices 'channel' and 'package'
372 function uninstallPackage(&$pkg)
374 $data = $this->_getDepDB();
375 unset($this->_cache);
376 if (is_object($pkg)) {
377 $channel = strtolower($pkg->getChannel());
378 $package = strtolower($pkg->getPackage());
380 $channel = strtolower($pkg['channel']);
381 $package = strtolower($pkg['package']);
384 if (!isset($data['dependencies'][$channel][$package])) {
388 foreach ($data['dependencies'][$channel][$package] as $dep) {
390 $depchannel = isset($dep['dep']['uri']) ? '__uri' : strtolower($dep['dep']['channel']);
391 $depname = strtolower($dep['dep']['name']);
392 if (isset($data['packages'][$depchannel][$depname])) {
393 foreach ($data['packages'][$depchannel][$depname] as $i => $info) {
394 if ($info['channel'] == $channel && $info['package'] == $package) {
402 unset($data['packages'][$depchannel][$depname][$i]);
403 if (!count($data['packages'][$depchannel][$depname])) {
404 unset($data['packages'][$depchannel][$depname]);
405 if (!count($data['packages'][$depchannel])) {
406 unset($data['packages'][$depchannel]);
409 $data['packages'][$depchannel][$depname] =
410 array_values($data['packages'][$depchannel][$depname]);
415 unset($data['dependencies'][$channel][$package]);
416 if (!count($data['dependencies'][$channel])) {
417 unset($data['dependencies'][$channel]);
420 if (!count($data['dependencies'])) {
421 unset($data['dependencies']);
424 if (!count($data['packages'])) {
425 unset($data['packages']);
428 $this->_writeDepDB($data);
432 * Rebuild the dependency DB by reading registry entries.
433 * @return true|PEAR_Error
437 $depdb = array('_version' => $this->_version);
438 if (!$this->hasWriteAccess()) {
439 // allow startup for read-only with older Registry
443 $packages = $this->_registry->listAllPackages();
444 if (PEAR::isError($packages)) {
448 foreach ($packages as $channel => $ps) {
449 foreach ($ps as $package) {
450 $package = $this->_registry->getPackage($package, $channel);
451 if (PEAR::isError($package)) {
454 $this->_setPackageDeps($depdb, $package);
458 $error = $this->_writeDepDB($depdb);
459 if (PEAR::isError($error)) {
463 $this->_cache = $depdb;
468 * Register usage of the dependency DB to prevent race conditions
469 * @param int one of the LOCK_* constants
470 * @return true|PEAR_Error
473 function _lock($mode = LOCK_EX)
475 if (stristr(php_uname(), 'Windows 9')) {
479 if ($mode != LOCK_UN && is_resource($this->_lockFp)) {
480 // XXX does not check type of lock (LOCK_SH/LOCK_EX)
485 // XXX People reported problems with LOCK_SH and 'w'
486 if ($mode === LOCK_SH) {
487 if (!file_exists($this->_lockfile)) {
488 touch($this->_lockfile);
489 } elseif (!is_file($this->_lockfile)) {
490 return PEAR::raiseError('could not create Dependency lock file, ' .
491 'it exists and is not a regular file');
496 if (!is_resource($this->_lockFp)) {
497 $this->_lockFp = @fopen($this->_lockfile, $open_mode);
500 if (!is_resource($this->_lockFp)) {
501 return PEAR::raiseError("could not create Dependency lock file" .
502 (isset($php_errormsg) ? ": " . $php_errormsg : ""));
505 if (!(int)flock($this->_lockFp, $mode)) {
507 case LOCK_SH: $str = 'shared'; break;
508 case LOCK_EX: $str = 'exclusive'; break;
509 case LOCK_UN: $str = 'unlock'; break;
510 default: $str = 'unknown'; break;
513 return PEAR::raiseError("could not acquire $str lock ($this->_lockfile)");
520 * Release usage of dependency DB
521 * @return true|PEAR_Error
526 $ret = $this->_lock(LOCK_UN);
527 if (is_resource($this->_lockFp)) {
528 fclose($this->_lockFp);
530 $this->_lockFp = null;
535 * Load the dependency database from disk, or return the cache
536 * @return array|PEAR_Error
540 if (!$this->hasWriteAccess()) {
541 return array('_version' => $this->_version);
544 if (isset($this->_cache)) {
545 return $this->_cache;
548 if (!$fp = fopen($this->_depdb, 'r')) {
549 $err = PEAR::raiseError("Could not open dependencies file `".$this->_depdb."'");
553 $rt = get_magic_quotes_runtime();
554 set_magic_quotes_runtime(0);
557 $data = unserialize(file_get_contents($this->_depdb));
558 set_magic_quotes_runtime($rt);
559 $this->_cache = $data;
564 * Write out the dependency database to disk
565 * @param array the database
566 * @return true|PEAR_Error
569 function _writeDepDB(&$deps)
571 if (PEAR::isError($e = $this->_lock(LOCK_EX))) {
575 if (!$fp = fopen($this->_depdb, 'wb')) {
577 return PEAR::raiseError("Could not open dependencies file `".$this->_depdb."' for writing");
580 $rt = get_magic_quotes_runtime();
581 set_magic_quotes_runtime(0);
582 fwrite($fp, serialize($deps));
583 set_magic_quotes_runtime($rt);
586 $this->_cache = $deps;
591 * Register all dependencies from a package in the dependencies database, in essence
592 * "installing" the package's dependency information
593 * @param array the database
594 * @param PEAR_PackageFile_v1|PEAR_PackageFile_v2
597 function _setPackageDeps(&$data, &$pkg)
599 $pkg->setConfig($this->_config);
600 if ($pkg->getPackagexmlVersion() == '1.0') {
601 $gen = &$pkg->getDefaultGenerator();
602 $deps = $gen->dependenciesToV2();
604 $deps = $pkg->getDeps(true);
611 if (!is_array($data)) {
615 if (!isset($data['dependencies'])) {
616 $data['dependencies'] = array();
619 $channel = strtolower($pkg->getChannel());
620 $package = strtolower($pkg->getPackage());
622 if (!isset($data['dependencies'][$channel])) {
623 $data['dependencies'][$channel] = array();
626 $data['dependencies'][$channel][$package] = array();
627 if (isset($deps['required']['package'])) {
628 if (!isset($deps['required']['package'][0])) {
629 $deps['required']['package'] = array($deps['required']['package']);
632 foreach ($deps['required']['package'] as $dep) {
633 $this->_registerDep($data, $pkg, $dep, 'required');
637 if (isset($deps['optional']['package'])) {
638 if (!isset($deps['optional']['package'][0])) {
639 $deps['optional']['package'] = array($deps['optional']['package']);
642 foreach ($deps['optional']['package'] as $dep) {
643 $this->_registerDep($data, $pkg, $dep, 'optional');
647 if (isset($deps['required']['subpackage'])) {
648 if (!isset($deps['required']['subpackage'][0])) {
649 $deps['required']['subpackage'] = array($deps['required']['subpackage']);
652 foreach ($deps['required']['subpackage'] as $dep) {
653 $this->_registerDep($data, $pkg, $dep, 'required');
657 if (isset($deps['optional']['subpackage'])) {
658 if (!isset($deps['optional']['subpackage'][0])) {
659 $deps['optional']['subpackage'] = array($deps['optional']['subpackage']);
662 foreach ($deps['optional']['subpackage'] as $dep) {
663 $this->_registerDep($data, $pkg, $dep, 'optional');
667 if (isset($deps['group'])) {
668 if (!isset($deps['group'][0])) {
669 $deps['group'] = array($deps['group']);
672 foreach ($deps['group'] as $group) {
673 if (isset($group['package'])) {
674 if (!isset($group['package'][0])) {
675 $group['package'] = array($group['package']);
678 foreach ($group['package'] as $dep) {
679 $this->_registerDep($data, $pkg, $dep, 'optional',
680 $group['attribs']['name']);
684 if (isset($group['subpackage'])) {
685 if (!isset($group['subpackage'][0])) {
686 $group['subpackage'] = array($group['subpackage']);
689 foreach ($group['subpackage'] as $dep) {
690 $this->_registerDep($data, $pkg, $dep, 'optional',
691 $group['attribs']['name']);
697 if ($data['dependencies'][$channel][$package] == array()) {
698 unset($data['dependencies'][$channel][$package]);
699 if (!count($data['dependencies'][$channel])) {
700 unset($data['dependencies'][$channel]);
706 * @param array the database
707 * @param PEAR_PackageFile_v1|PEAR_PackageFile_v2
708 * @param array the specific dependency
709 * @param required|optional whether this is a required or an optional dep
710 * @param string|false dependency group this dependency is from, or false for ordinary dep
712 function _registerDep(&$data, &$pkg, $dep, $type, $group = false)
720 $dep = array_map('strtolower', $dep);
721 $depchannel = isset($dep['channel']) ? $dep['channel'] : '__uri';
722 if (!isset($data['dependencies'])) {
723 $data['dependencies'] = array();
726 $channel = strtolower($pkg->getChannel());
727 $package = strtolower($pkg->getPackage());
729 if (!isset($data['dependencies'][$channel])) {
730 $data['dependencies'][$channel] = array();
733 if (!isset($data['dependencies'][$channel][$package])) {
734 $data['dependencies'][$channel][$package] = array();
737 $data['dependencies'][$channel][$package][] = $info;
738 if (isset($data['packages'][$depchannel][$dep['name']])) {
740 foreach ($data['packages'][$depchannel][$dep['name']] as $i => $p) {
741 if ($p['channel'] == $channel && $p['package'] == $package) {
747 if (!isset($data['packages'])) {
748 $data['packages'] = array();
751 if (!isset($data['packages'][$depchannel])) {
752 $data['packages'][$depchannel] = array();
755 if (!isset($data['packages'][$depchannel][$dep['name']])) {
756 $data['packages'][$depchannel][$dep['name']] = array();
763 $data['packages'][$depchannel][$dep['name']][] = array(
764 'channel' => $channel,
765 'package' => $package