1<?php 2/* 3 * $Id: 747f836b665364f27fc2f8fa777d882a877a9743 $ 4 * 5 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 6 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 7 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 8 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 9 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 10 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 11 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 12 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 13 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 14 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 15 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 16 * 17 * This software consists of voluntary contributions made by many individuals 18 * and is licensed under the LGPL. For more information please see 19 * <http://phing.info>. 20 */ 21 22include_once 'phing/system/io/PhingFile.php'; 23include_once 'phing/util/FileUtils.php'; 24include_once 'phing/TaskAdapter.php'; 25include_once 'phing/util/StringHelper.php'; 26include_once 'phing/BuildEvent.php'; 27include_once 'phing/input/DefaultInputHandler.php'; 28include_once 'phing/types/PropertyValue.php'; 29 30/** 31 * The Phing project class. Represents a completely configured Phing project. 32 * The class defines the project and all tasks/targets. It also contains 33 * methods to start a build as well as some properties and FileSystem 34 * abstraction. 35 * 36 * @author Andreas Aderhold <andi@binarycloud.com> 37 * @author Hans Lellelid <hans@xmpl.org> 38 * @version $Id: 747f836b665364f27fc2f8fa777d882a877a9743 $ 39 * @package phing 40 */ 41class Project { 42 43 // Logging level constants. 44 const MSG_DEBUG = 4; 45 const MSG_VERBOSE = 3; 46 const MSG_INFO = 2; 47 const MSG_WARN = 1; 48 const MSG_ERR = 0; 49 50 /** contains the targets */ 51 private $targets = array(); 52 /** global filterset (future use) */ 53 private $globalFilterSet = array(); 54 /** all globals filters (future use) */ 55 private $globalFilters = array(); 56 57 /** Project properties map (usually String to String). */ 58 private $properties = array(); 59 60 /** 61 * Map of "user" properties (as created in the Ant task, for example). 62 * Note that these key/value pairs are also always put into the 63 * project properties, so only the project properties need to be queried. 64 * Mapping is String to String. 65 */ 66 private $userProperties = array(); 67 68 /** 69 * Map of inherited "user" properties - that are those "user" 70 * properties that have been created by tasks and not been set 71 * from the command line or a GUI tool. 72 * Mapping is String to String. 73 */ 74 private $inheritedProperties = array(); 75 76 /** task definitions for this project*/ 77 private $taskdefs = array(); 78 79 /** type definitions for this project */ 80 private $typedefs = array(); 81 82 /** holds ref names and a reference to the referred object*/ 83 private $references = array(); 84 85 /** The InputHandler being used by this project. */ 86 private $inputHandler; 87 88 /* -- properties that come in via xml attributes -- */ 89 90 /** basedir (PhingFile object) */ 91 private $basedir; 92 93 /** the default target name */ 94 private $defaultTarget = 'all'; 95 96 /** project name (required) */ 97 private $name; 98 99 /** project description */ 100 private $description; 101 102 /** require phing version */ 103 private $phingVersion; 104 105 /** a FileUtils object */ 106 private $fileUtils; 107 108 /** Build listeneers */ 109 private $listeners = array(); 110 111 /** 112 * Constructor, sets any default vars. 113 */ 114 public function __construct() { 115 $this->fileUtils = new FileUtils(); 116 $this->inputHandler = new DefaultInputHandler(); 117 } 118 119 /** 120 * Sets the input handler 121 * @param InputHandler $handler 122 */ 123 public function setInputHandler(InputHandler $handler) { 124 $this->inputHandler = $handler; 125 } 126 127 /** 128 * Retrieves the current input handler. 129 * @return InputHandler 130 */ 131 public function getInputHandler() { 132 return $this->inputHandler; 133 } 134 135 /** inits the project, called from main app */ 136 public function init() { 137 // set builtin properties 138 $this->setSystemProperties(); 139 140 // load default tasks 141 $taskdefs = Phing::getResourcePath("phing/tasks/defaults.properties"); 142 143 try { // try to load taskdefs 144 $props = new Properties(); 145 $in = new PhingFile((string)$taskdefs); 146 147 if ($in === null) { 148 throw new BuildException("Can't load default task list"); 149 } 150 $props->load($in); 151 152 $enum = $props->propertyNames(); 153 foreach($enum as $key) { 154 $value = $props->getProperty($key); 155 $this->addTaskDefinition($key, $value); 156 } 157 } catch (IOException $ioe) { 158 throw new BuildException("Can't load default task list"); 159 } 160 161 // load default tasks 162 $typedefs = Phing::getResourcePath("phing/types/defaults.properties"); 163 164 try { // try to load typedefs 165 $props = new Properties(); 166 $in = new PhingFile((string)$typedefs); 167 if ($in === null) { 168 throw new BuildException("Can't load default datatype list"); 169 } 170 $props->load($in); 171 172 $enum = $props->propertyNames(); 173 foreach($enum as $key) { 174 $value = $props->getProperty($key); 175 $this->addDataTypeDefinition($key, $value); 176 } 177 } catch(IOException $ioe) { 178 throw new BuildException("Can't load default datatype list"); 179 } 180 } 181 182 /** returns the global filterset (future use) */ 183 public function getGlobalFilterSet() { 184 return $this->globalFilterSet; 185 } 186 187 // --------------------------------------------------------- 188 // Property methods 189 // --------------------------------------------------------- 190 191 /** 192 * Sets a property. Any existing property of the same name 193 * is overwritten, unless it is a user property. 194 * @param string $name The name of property to set. 195 * Must not be <code>null</code>. 196 * @param string $value The new value of the property. 197 * Must not be <code>null</code>. 198 * @return void 199 */ 200 public function setProperty($name, $value) { 201 202 // command line properties take precedence 203 if (isset($this->userProperties[$name])) { 204 $this->log("Override ignored for user property " . $name, Project::MSG_VERBOSE); 205 return; 206 } 207 208 if (isset($this->properties[$name])) { 209 $this->log("Overriding previous definition of property " . $name, Project::MSG_VERBOSE); 210 } 211 212 $this->log("Setting project property: " . $name . " -> " . $value, Project::MSG_DEBUG); 213 $this->properties[$name] = $value; 214 $this->addReference($name, new PropertyValue($value)); 215 } 216 217 /** 218 * Sets a property if no value currently exists. If the property 219 * exists already, a message is logged and the method returns with 220 * no other effect. 221 * 222 * @param string $name The name of property to set. 223 * Must not be <code>null</code>. 224 * @param string $value The new value of the property. 225 * Must not be <code>null</code>. 226 * @since 2.0 227 */ 228 public function setNewProperty($name, $value) { 229 if (isset($this->properties[$name])) { 230 $this->log("Override ignored for property " . $name, Project::MSG_DEBUG); 231 return; 232 } 233 $this->log("Setting project property: " . $name . " -> " . $value, Project::MSG_DEBUG); 234 $this->properties[$name] = $value; 235 $this->addReference($name, new PropertyValue($value)); 236 } 237 238 /** 239 * Sets a user property, which cannot be overwritten by 240 * set/unset property calls. Any previous value is overwritten. 241 * @param string $name The name of property to set. 242 * Must not be <code>null</code>. 243 * @param string $value The new value of the property. 244 * Must not be <code>null</code>. 245 * @see #setProperty() 246 */ 247 public function setUserProperty($name, $value) { 248 $this->log("Setting user project property: " . $name . " -> " . $value, Project::MSG_DEBUG); 249 $this->userProperties[$name] = $value; 250 $this->properties[$name] = $value; 251 $this->addReference($name, new PropertyValue($value)); 252 } 253 254 /** 255 * Sets a user property, which cannot be overwritten by set/unset 256 * property calls. Any previous value is overwritten. Also marks 257 * these properties as properties that have not come from the 258 * command line. 259 * 260 * @param string $name The name of property to set. 261 * Must not be <code>null</code>. 262 * @param string $value The new value of the property. 263 * Must not be <code>null</code>. 264 * @see #setProperty() 265 */ 266 public function setInheritedProperty($name, $value) { 267 $this->inheritedProperties[$name] = $value; 268 $this->setUserProperty($name, $value); 269 } 270 271 /** 272 * Sets a property unless it is already defined as a user property 273 * (in which case the method returns silently). 274 * 275 * @param name The name of the property. 276 * Must not be <code>null</code>. 277 * @param value The property value. Must not be <code>null</code>. 278 */ 279 private function setPropertyInternal($name, $value) { 280 if (isset($this->userProperties[$name])) { 281 $this->log("Override ignored for user property " . $name, Project::MSG_VERBOSE); 282 return; 283 } 284 $this->properties[$name] = $value; 285 $this->addReference($name, new PropertyValue($value)); 286 } 287 288 /** 289 * Returns the value of a property, if it is set. 290 * 291 * @param string $name The name of the property. 292 * May be <code>null</code>, in which case 293 * the return value is also <code>null</code>. 294 * @return string The property value, or <code>null</code> for no match 295 * or if a <code>null</code> name is provided. 296 */ 297 public function getProperty($name) { 298 if (!isset($this->properties[$name])) { 299 return null; 300 } 301 $found = $this->properties[$name]; 302 // check to see if there are unresolved property references 303 if (false !== strpos($found, '${')) { 304 // attempt to resolve properties 305 $found = $this->replaceProperties($found); 306 // save resolved value 307 $this->properties[$name] = $found; 308 } 309 return $found; 310 } 311 312 /** 313 * Replaces ${} style constructions in the given value with the 314 * string value of the corresponding data types. 315 * 316 * @param value The string to be scanned for property references. 317 * May be <code>null</code>. 318 * 319 * @return the given string with embedded property names replaced 320 * by values, or <code>null</code> if the given string is 321 * <code>null</code>. 322 * 323 * @exception BuildException if the given value has an unclosed 324 * property name, e.g. <code>${xxx</code> 325 */ 326 public function replaceProperties($value) { 327 return ProjectConfigurator::replaceProperties($this, $value, $this->properties); 328 } 329 330 /** 331 * Returns the value of a user property, if it is set. 332 * 333 * @param string $name The name of the property. 334 * May be <code>null</code>, in which case 335 * the return value is also <code>null</code>. 336 * @return string The property value, or <code>null</code> for no match 337 * or if a <code>null</code> name is provided. 338 */ 339 public function getUserProperty($name) { 340 if (!isset($this->userProperties[$name])) { 341 return null; 342 } 343 return $this->userProperties[$name]; 344 } 345 346 /** 347 * Returns a copy of the properties table. 348 * @return array A hashtable containing all properties 349 * (including user properties). 350 */ 351 public function getProperties() { 352 return $this->properties; 353 } 354 355 /** 356 * Returns a copy of the user property hashtable 357 * @return a hashtable containing just the user properties 358 */ 359 public function getUserProperties() { 360 return $this->userProperties; 361 } 362 363 /** 364 * Copies all user properties that have been set on the command 365 * line or a GUI tool from this instance to the Project instance 366 * given as the argument. 367 * 368 * <p>To copy all "user" properties, you will also have to call 369 * {@link #copyInheritedProperties copyInheritedProperties}.</p> 370 * 371 * @param Project $other the project to copy the properties to. Must not be null. 372 * @return void 373 * @since phing 2.0 374 */ 375 public function copyUserProperties(Project $other) { 376 foreach($this->userProperties as $arg => $value) { 377 if (isset($this->inheritedProperties[$arg])) { 378 continue; 379 } 380 $other->setUserProperty($arg, $value); 381 } 382 } 383 384 /** 385 * Copies all user properties that have not been set on the 386 * command line or a GUI tool from this instance to the Project 387 * instance given as the argument. 388 * 389 * <p>To copy all "user" properties, you will also have to call 390 * {@link #copyUserProperties copyUserProperties}.</p> 391 * 392 * @param Project $other the project to copy the properties to. Must not be null. 393 * 394 * @since phing 2.0 395 */ 396 public function copyInheritedProperties(Project $other) { 397 foreach($this->userProperties as $arg => $value) { 398 if ($other->getUserProperty($arg) !== null) { 399 continue; 400 } 401 $other->setInheritedProperty($arg, $value); 402 } 403 } 404 405 // --------------------------------------------------------- 406 // END Properties methods 407 // --------------------------------------------------------- 408 409 410 /** 411 * Sets default target 412 * @param string $targetName 413 */ 414 public function setDefaultTarget($targetName) { 415 $this->defaultTarget = (string) trim($targetName); 416 } 417 418 /** 419 * Returns default target 420 * @return string 421 */ 422 public function getDefaultTarget() { 423 return (string) $this->defaultTarget; 424 } 425 426 /** 427 * Sets the name of the current project 428 * 429 * @param string $name name of project 430 * @return void 431 * @access public 432 * @author Andreas Aderhold, andi@binarycloud.com 433 */ 434 public function setName($name) { 435 $this->name = (string) trim($name); 436 $this->setProperty("phing.project.name", $this->name); 437 } 438 439 /** 440 * Returns the name of this project 441 * 442 * @return string projectname 443 * @access public 444 * @author Andreas Aderhold, andi@binarycloud.com 445 */ 446 public function getName() { 447 return (string) $this->name; 448 } 449 450 /** 451 * Set the projects description 452 * @param string $description 453 */ 454 public function setDescription($description) { 455 $this->description = (string) trim($description); 456 } 457 458 /** 459 * return the description, null otherwise 460 * @return string|null 461 */ 462 public function getDescription() { 463 return $this->description; 464 } 465 466 /** 467 * Set the minimum required phing version 468 * @param string $version 469 */ 470 public function setPhingVersion($version) { 471 $version = str_replace('phing', '', strtolower($version)); 472 $this->phingVersion = (string)trim($version); 473 } 474 475 /** 476 * Get the minimum required phing version 477 * @return string 478 */ 479 public function getPhingVersion() { 480 if($this->phingVersion === null) { 481 $this->setPhingVersion(Phing::getPhingVersion()); 482 } 483 return $this->phingVersion; 484 } 485 486 /** 487 * Set basedir object from xm 488 * @param PhingFile|string $dir 489 */ 490 public function setBasedir($dir) { 491 if ($dir instanceof PhingFile) { 492 $dir = $dir->getAbsolutePath(); 493 } 494 495 $dir = $this->fileUtils->normalize($dir); 496 $dir = FileSystem::getFilesystem()->canonicalize($dir); 497 498 $dir = new PhingFile((string) $dir); 499 if (!$dir->exists()) { 500 throw new BuildException("Basedir ".$dir->getAbsolutePath()." does not exist"); 501 } 502 if (!$dir->isDirectory()) { 503 throw new BuildException("Basedir ".$dir->getAbsolutePath()." is not a directory"); 504 } 505 $this->basedir = $dir; 506 $this->setPropertyInternal("project.basedir", $this->basedir->getAbsolutePath()); 507 $this->log("Project base dir set to: " . $this->basedir->getPath(), Project::MSG_VERBOSE); 508 509 // [HL] added this so that ./ files resolve correctly. This may be a mistake ... or may be in wrong place. 510 chdir($dir->getAbsolutePath()); 511 } 512 513 /** 514 * Returns the basedir of this project 515 * 516 * @return PhingFile Basedir PhingFile object 517 * @access public 518 * @throws BuildException 519 * @author Andreas Aderhold, andi@binarycloud.com 520 */ 521 public function getBasedir() { 522 if ($this->basedir === null) { 523 try { // try to set it 524 $this->setBasedir("."); 525 } catch (BuildException $exc) { 526 throw new BuildException("Can not set default basedir. ".$exc->getMessage()); 527 } 528 } 529 return $this->basedir; 530 } 531 532 /** 533 * Sets system properties and the environment variables for this project. 534 * 535 * @return void 536 */ 537 public function setSystemProperties() { 538 539 // first get system properties 540 $systemP = array_merge( self::getProperties(), Phing::getProperties() ); 541 foreach($systemP as $name => $value) { 542 $this->setPropertyInternal($name, $value); 543 } 544 545 // and now the env vars 546 foreach($_SERVER as $name => $value) { 547 // skip arrays 548 if (is_array($value)) { 549 continue; 550 } 551 $this->setPropertyInternal('env.' . $name, $value); 552 } 553 return true; 554 } 555 556 557 /** 558 * Adds a task definition. 559 * @param string $name Name of tag. 560 * @param string $class The class path to use. 561 * @param string $classpath The classpat to use. 562 */ 563 public function addTaskDefinition($name, $class, $classpath = null) { 564 $name = $name; 565 $class = $class; 566 if ($class === "") { 567 $this->log("Task $name has no class defined.", Project::MSG_ERR); 568 } elseif (!isset($this->taskdefs[$name])) { 569 Phing::import($class, $classpath); 570 $this->taskdefs[$name] = $class; 571 $this->log(" +Task definition: $name ($class)", Project::MSG_DEBUG); 572 } else { 573 $this->log("Task $name ($class) already registered, skipping", Project::MSG_VERBOSE); 574 } 575 } 576 577 /** 578 * Returns the task definitions 579 * @return array 580 */ 581 public function getTaskDefinitions() { 582 return $this->taskdefs; 583 } 584 585 /** 586 * Adds a data type definition. 587 * @param string $typeName Name of the type. 588 * @param string $typeClass The class to use. 589 * @param string $classpath The classpath to use. 590 */ 591 public function addDataTypeDefinition($typeName, $typeClass, $classpath = null) { 592 if (!isset($this->typedefs[$typeName])) { 593 Phing::import($typeClass, $classpath); 594 $this->typedefs[$typeName] = $typeClass; 595 $this->log(" +User datatype: $typeName ($typeClass)", Project::MSG_DEBUG); 596 } else { 597 $this->log("Type $typeName ($typeClass) already registered, skipping", Project::MSG_VERBOSE); 598 } 599 } 600 601 /** 602 * Returns the data type definitions 603 * @return array 604 */ 605 public function getDataTypeDefinitions() { 606 return $this->typedefs; 607 } 608 609 /** 610 * Add a new target to the project 611 * @param string $targetName 612 * @param Target $target 613 */ 614 public function addTarget($targetName, &$target) { 615 if (isset($this->targets[$targetName])) { 616 throw new BuildException("Duplicate target: $targetName"); 617 } 618 $this->addOrReplaceTarget($targetName, $target); 619 } 620 621 /** 622 * Adds or replaces a target in the project 623 * @param string $targetName 624 * @param Target $target 625 */ 626 public function addOrReplaceTarget($targetName, &$target) { 627 $this->log(" +Target: $targetName", Project::MSG_DEBUG); 628 $target->setProject($this); 629 $this->targets[$targetName] = $target; 630 631 $ctx = $this->getReference("phing.parsing.context"); 632 $current = $ctx->getConfigurator()->getCurrentTargets(); 633 $current[$targetName] = $target; 634 } 635 636 /** 637 * Returns the available targets 638 * @return array 639 */ 640 public function getTargets() { 641 return $this->targets; 642 } 643 644 /** 645 * Create a new task instance and return reference to it. This method is 646 * sorta factory like. A _local_ instance is created and a reference returned to 647 * that instance. Usually PHP destroys local variables when the function call 648 * ends. But not if you return a reference to that variable. 649 * This is kinda error prone, because if no reference exists to the variable 650 * it is destroyed just like leaving the local scope with primitive vars. There's no 651 * central place where the instance is stored as in other OOP like languages. 652 * 653 * [HL] Well, ZE2 is here now, and this is still working. We'll leave this alone 654 * unless there's any good reason not to. 655 * 656 * @param string $taskType Task name 657 * @return Task A task object 658 * @throws BuildException 659 * Exception 660 */ 661 public function createTask($taskType) { 662 try { 663 $classname = ""; 664 $tasklwr = strtolower($taskType); 665 foreach ($this->taskdefs as $name => $class) { 666 if (strtolower($name) === $tasklwr) { 667 $classname = $class; 668 break; 669 } 670 } 671 672 if ($classname === "") { 673 return null; 674 } 675 676 $cls = Phing::import($classname); 677 678 if (!class_exists($cls)) { 679 throw new BuildException("Could not instantiate class $cls, even though a class was specified. (Make sure that the specified class file contains a class with the correct name.)"); 680 } 681 682 $o = new $cls(); 683 684 if ($o instanceof Task) { 685 $task = $o; 686 } else { 687 $this->log (" (Using TaskAdapter for: $taskType)", Project::MSG_DEBUG); 688 // not a real task, try adapter 689 $taskA = new TaskAdapter(); 690 $taskA->setProxy($o); 691 $task = $taskA; 692 } 693 $task->setProject($this); 694 $task->setTaskType($taskType); 695 // set default value, can be changed by the user 696 $task->setTaskName($taskType); 697 $this->log (" +Task: " . $taskType, Project::MSG_DEBUG); 698 } catch (Exception $t) { 699 throw new BuildException("Could not create task of type: " . $taskType, $t); 700 } 701 // everything fine return reference 702 return $task; 703 } 704 705 /** 706 * Creates a new condition and returns the reference to it 707 * 708 * @param string $conditionType 709 * @return Condition 710 * @throws BuildException 711 */ 712 public function createCondition($conditionType) 713 { 714 try { 715 $classname = ""; 716 $tasklwr = strtolower($conditionType); 717 foreach ($this->typedefs as $name => $class) { 718 if (strtolower($name) === $tasklwr) { 719 $classname = $class; 720 break; 721 } 722 } 723 724 if ($classname === "") { 725 return null; 726 } 727 728 $cls = Phing::import($classname); 729 730 if (!class_exists($cls)) { 731 throw new BuildException("Could not instantiate class $cls, even though a class was specified. (Make sure that the specified class file contains a class with the correct name.)"); 732 } 733 734 $o = new $cls(); 735 if ($o instanceof Condition) { 736 return $o; 737 } else { 738 throw new BuildException("Not actually a condition"); 739 } 740 } catch (Exception $e) { 741 throw new BuildException("Could not create condition of type: " . $conditionType, $e); 742 } 743 } 744 745 /** 746 * Create a datatype instance and return reference to it 747 * See createTask() for explanation how this works 748 * 749 * @param string $typeName Type name 750 * @return object A datatype object 751 * @throws BuildException 752 * Exception 753 */ 754 public function createDataType($typeName) { 755 try { 756 $cls = ""; 757 $typelwr = strtolower($typeName); 758 foreach ($this->typedefs as $name => $class) { 759 if (strtolower($name) === $typelwr) { 760 $cls = StringHelper::unqualify($class); 761 break; 762 } 763 } 764 765 if ($cls === "") { 766 return null; 767 } 768 769 if (!class_exists($cls)) { 770 throw new BuildException("Could not instantiate class $cls, even though a class was specified. (Make sure that the specified class file contains a class with the correct name.)"); 771 } 772 773 $type = new $cls(); 774 $this->log(" +Type: $typeName", Project::MSG_DEBUG); 775 if (!($type instanceof DataType)) { 776 throw new Exception("$class is not an instance of phing.types.DataType"); 777 } 778 if ($type instanceof ProjectComponent) { 779 $type->setProject($this); 780 } 781 } catch (Exception $t) { 782 throw new BuildException("Could not create type: $typeName", $t); 783 } 784 // everything fine return reference 785 return $type; 786 } 787 788 /** 789 * Executes a list of targets 790 * 791 * @param array $targetNames List of target names to execute 792 * @return void 793 * @throws BuildException 794 */ 795 public function executeTargets($targetNames) { 796 foreach($targetNames as $tname) { 797 $this->executeTarget($tname); 798 } 799 } 800 801 /** 802 * Executes a target 803 * 804 * @param string $targetName Name of Target to execute 805 * @return void 806 * @throws BuildException 807 */ 808 public function executeTarget($targetName) { 809 810 // complain about executing void 811 if ($targetName === null) { 812 throw new BuildException("No target specified"); 813 } 814 815 // invoke topological sort of the target tree and run all targets 816 // until targetName occurs. 817 $sortedTargets = $this->_topoSort($targetName, $this->targets); 818 819 $curIndex = (int) 0; 820 $curTarget = null; 821 do { 822 try { 823 $curTarget = $sortedTargets[$curIndex++]; 824 $curTarget->performTasks(); 825 } catch (BuildException $exc) { 826 $this->log("Execution of target \"".$curTarget->getName()."\" failed for the following reason: ".$exc->getMessage(), Project::MSG_ERR); 827 throw $exc; 828 } 829 } while ($curTarget->getName() !== $targetName); 830 } 831 832 /** 833 * Helper function 834 */ 835 public function resolveFile($fileName, $rootDir = null) { 836 if ($rootDir === null) { 837 return $this->fileUtils->resolveFile($this->basedir, $fileName); 838 } else { 839 return $this->fileUtils->resolveFile($rootDir, $fileName); 840 } 841 } 842 843 /** 844 * Topologically sort a set of Targets. 845 * @param string $root is the (String) name of the root Target. The sort is 846 * created in such a way that the sequence of Targets until the root 847 * target is the minimum possible such sequence. 848 * @param array $targets is a array representing a "name to Target" mapping 849 * @return An array of Strings with the names of the targets in 850 * sorted order. 851 */ 852 public function _topoSort($root, &$targets) { 853 854 $root = (string) $root; 855 $ret = array(); 856 $state = array(); 857 $visiting = array(); 858 859 // We first run a DFS based sort using the root as the starting node. 860 // This creates the minimum sequence of Targets to the root node. 861 // We then do a sort on any remaining unVISITED targets. 862 // This is unnecessary for doing our build, but it catches 863 // circular dependencies or missing Targets on the entire 864 // dependency tree, not just on the Targets that depend on the 865 // build Target. 866 867 $this->_tsort($root, $targets, $state, $visiting, $ret); 868 869 $retHuman = ""; 870 for ($i=0, $_i=count($ret); $i < $_i; $i++) { 871 $retHuman .= $ret[$i]->toString()." "; 872 } 873 $this->log("Build sequence for target '$root' is: $retHuman", Project::MSG_VERBOSE); 874 875 $keys = array_keys($targets); 876 while($keys) { 877 $curTargetName = (string) array_shift($keys); 878 if (!isset($state[$curTargetName])) { 879 $st = null; 880 } else { 881 $st = (string) $state[$curTargetName]; 882 } 883 884 if ($st === null) { 885 $this->_tsort($curTargetName, $targets, $state, $visiting, $ret); 886 } elseif ($st === "VISITING") { 887 throw new Exception("Unexpected node in visiting state: $curTargetName"); 888 } 889 } 890 891 $retHuman = ""; 892 for ($i=0,$_i=count($ret); $i < $_i; $i++) { 893 $retHuman .= $ret[$i]->toString()." "; 894 } 895 $this->log("Complete build sequence is: $retHuman", Project::MSG_VERBOSE); 896 897 return $ret; 898 } 899 900 // one step in a recursive DFS traversal of the target dependency tree. 901 // - The array "state" contains the state (VISITED or VISITING or null) 902 // of all the target names. 903 // - The stack "visiting" contains a stack of target names that are 904 // currently on the DFS stack. (NB: the target names in "visiting" are 905 // exactly the target names in "state" that are in the VISITING state.) 906 // 1. Set the current target to the VISITING state, and push it onto 907 // the "visiting" stack. 908 // 2. Throw a BuildException if any child of the current node is 909 // in the VISITING state (implies there is a cycle.) It uses the 910 // "visiting" Stack to construct the cycle. 911 // 3. If any children have not been VISITED, tsort() the child. 912 // 4. Add the current target to the Vector "ret" after the children 913 // have been visited. Move the current target to the VISITED state. 914 // "ret" now contains the sorted sequence of Targets upto the current 915 // Target. 916 917 public function _tsort($root, &$targets, &$state, &$visiting, &$ret) { 918 $state[$root] = "VISITING"; 919 $visiting[] = $root; 920 921 if (!isset($targets[$root]) || !($targets[$root] instanceof Target)) { 922 $target = null; 923 } else { 924 $target = $targets[$root]; 925 } 926 927 // make sure we exist 928 if ($target === null) { 929 $sb = "Target '$root' does not exist in this project."; 930 array_pop($visiting); 931 if (!empty($visiting)) { 932 $parent = (string) $visiting[count($visiting)-1]; 933 $sb .= " It is a dependency of target '$parent'."; 934 } 935 throw new BuildException($sb); 936 } 937 938 $deps = $target->getDependencies(); 939 940 while($deps) { 941 $cur = (string) array_shift($deps); 942 if (!isset($state[$cur])) { 943 $m = null; 944 } else { 945 $m = (string) $state[$cur]; 946 } 947 if ($m === null) { 948 // not been visited 949 $this->_tsort($cur, $targets, $state, $visiting, $ret); 950 } elseif ($m == "VISITING") { 951 // currently visiting this node, so have a cycle 952 throw $this->_makeCircularException($cur, $visiting); 953 } 954 } 955 956 $p = (string) array_pop($visiting); 957 if ($root !== $p) { 958 throw new Exception("Unexpected internal error: expected to pop $root but got $p"); 959 } 960 961 $state[$root] = "VISITED"; 962 $ret[] = $target; 963 } 964 965 public function _makeCircularException($end, $stk) { 966 $sb = "Circular dependency: $end"; 967 do { 968 $c = (string) array_pop($stk); 969 $sb .= " <- ".$c; 970 } while($c != $end); 971 return new BuildException($sb); 972 } 973 974 /** 975 * Adds a reference to an object. This method is called when the parser 976 * detects a id="foo" attribute. It passes the id as $name and a reference 977 * to the object assigned to this id as $value 978 * @param string $name 979 * @param object $object 980 */ 981 public function addReference($name, $object) { 982 if (isset($this->references[$name])) { 983 $this->log("Overriding previous definition of reference to $name", Project::MSG_VERBOSE); 984 } 985 $this->log("Adding reference: $name -> ".get_class($object), Project::MSG_DEBUG); 986 $this->references[$name] = $object; 987 } 988 989 /** 990 * Returns the references array. 991 * @return array 992 */ 993 public function getReferences() { 994 return $this->references; 995 } 996 997 /** 998 * Returns a specific reference. 999 * @param string $key The reference id/key. 1000 * @return object Reference or null if not defined 1001 */ 1002 public function getReference($key) 1003 { 1004 if (isset($this->references[$key])) { 1005 return $this->references[$key]; 1006 } 1007 return null; // just to be explicit 1008 } 1009 1010 /** 1011 * Abstracting and simplifyling Logger calls for project messages 1012 * @param string $msg 1013 * @param int $level 1014 */ 1015 public function log($msg, $level = Project::MSG_INFO) { 1016 $this->logObject($this, $msg, $level); 1017 } 1018 1019 function logObject($obj, $msg, $level) { 1020 $this->fireMessageLogged($obj, $msg, $level); 1021 } 1022 1023 function addBuildListener(BuildListener $listener) { 1024 $this->listeners[] = $listener; 1025 } 1026 1027 function removeBuildListener(BuildListener $listener) { 1028 $newarray = array(); 1029 for ($i=0, $size=count($this->listeners); $i < $size; $i++) { 1030 if ($this->listeners[$i] !== $listener) { 1031 $newarray[] = $this->listeners[$i]; 1032 } 1033 } 1034 $this->listeners = $newarray; 1035 } 1036 1037 function getBuildListeners() { 1038 return $this->listeners; 1039 } 1040 1041 function fireBuildStarted() { 1042 $event = new BuildEvent($this); 1043 foreach($this->listeners as $listener) { 1044 $listener->buildStarted($event); 1045 } 1046 } 1047 1048 function fireBuildFinished($exception) { 1049 $event = new BuildEvent($this); 1050 $event->setException($exception); 1051 foreach($this->listeners as $listener) { 1052 $listener->buildFinished($event); 1053 } 1054 } 1055 1056 function fireTargetStarted($target) { 1057 $event = new BuildEvent($target); 1058 foreach($this->listeners as $listener) { 1059 $listener->targetStarted($event); 1060 } 1061 } 1062 1063 function fireTargetFinished($target, $exception) { 1064 $event = new BuildEvent($target); 1065 $event->setException($exception); 1066 foreach($this->listeners as $listener) { 1067 $listener->targetFinished($event); 1068 } 1069 } 1070 1071 function fireTaskStarted($task) { 1072 $event = new BuildEvent($task); 1073 foreach($this->listeners as $listener) { 1074 $listener->taskStarted($event); 1075 } 1076 } 1077 1078 function fireTaskFinished($task, $exception) { 1079 $event = new BuildEvent($task); 1080 $event->setException($exception); 1081 foreach($this->listeners as $listener) { 1082 $listener->taskFinished($event); 1083 } 1084 } 1085 1086 function fireMessageLoggedEvent($event, $message, $priority) { 1087 $event->setMessage($message, $priority); 1088 foreach($this->listeners as $listener) { 1089 $listener->messageLogged($event); 1090 } 1091 } 1092 1093 function fireMessageLogged($object, $message, $priority) { 1094 $this->fireMessageLoggedEvent(new BuildEvent($object), $message, $priority); 1095 } 1096} 1097