source: classes/phing/Project.php @ a05f680

Last change on this file since a05f680 was a05f680, checked in by Matthias Pigulla <mp@…>, 3 years ago

Fix for what I broke in [1144] :(

  • Property mode set to 100644
File size: 32.2 KB
Line 
1<?php
2/*
3 *  $Id$
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';
28require_once('phing/system/util/Properties.php');
29require_once('phing/util/properties/PropertySetImpl.php');
30require_once('phing/util/properties/PropertyExpansionHelper.php');
31
32/**
33 *  The Phing project class. Represents a completely configured Phing project.
34 *  The class defines the project and all tasks/targets. It also contains
35 *  methods to start a build as well as some properties and FileSystem
36 *  abstraction.
37 *
38 * @author    Andreas Aderhold <andi@binarycloud.com>
39 * @author    Hans Lellelid <hans@xmpl.org>
40 * @version   $Revision$
41 * @package   phing
42 */
43class Project {
44
45    // Logging level constants.
46    const MSG_DEBUG = 4;
47    const MSG_VERBOSE = 3;
48    const MSG_INFO = 2;
49    const MSG_WARN = 1;
50    const MSG_ERR = 0;
51   
52    /** contains the targets */
53    private $targets         = array();
54    /** global filterset (future use) */
55    private $globalFilterSet = array();
56    /**  all globals filters (future use) */
57    private $globalFilters   = array();
58   
59    /** Project properties map (usually String to String). */
60    private $properties;
61   
62    /**
63     * Map of "user" properties (as created in the Ant task, for example).
64     * Note that these key/value pairs are also always put into the
65     * project properties, so only the project properties need to be queried.
66     * Mapping is String to String.
67     */
68    private $userProperties;
69   
70    /**
71     * Map of inherited "user" properties - that are those "user"
72     * properties that have been created by tasks and not been set
73     * from the command line or a GUI tool.
74     * Mapping is String to String.
75     */
76    private $inheritedProperties;
77   
78    /** task definitions for this project*/
79    private $taskdefs = array();
80   
81    /** type definitions for this project */
82    private $typedefs = array();
83   
84    /** holds ref names and a reference to the referred object*/
85    private $references = array();
86   
87    /** The InputHandler being used by this project. */
88    private $inputHandler;
89   
90    /* -- properties that come in via xml attributes -- */
91   
92    /** basedir (PhingFile object) */
93    private $basedir;
94   
95    /** the default target name */
96    private $defaultTarget = 'all';
97   
98    /** project name (required) */
99    private $name;
100   
101    /** project description */
102    private $description;
103
104    /** require phing version */
105    private $phingVersion;
106
107    /** a FileUtils object */
108    private $fileUtils;
109   
110    /**  Build listeneers */
111    private $listeners = array();
112
113    /**
114     *  Constructor, sets any default vars.
115     */
116    function __construct() {
117        $this->fileUtils = new FileUtils();
118        $this->inputHandler = new DefaultInputHandler();
119        $this->properties = new PropertySetImpl();
120        $this->inheritedProperties = new PropertySetImpl();
121        $this->userProperties = new PropertySetImpl();
122        $this->propertyExpansionHelper = new PropertyExpansionHelper($this->properties);
123    }
124
125    /**
126     * Sets the input handler
127     */
128    public function setInputHandler(InputHandler $handler) {
129        $this->inputHandler = $handler;
130    }
131
132    /**
133     * Retrieves the current input handler.
134     */
135    public function getInputHandler() {
136        return $this->inputHandler;
137    }
138
139    /** inits the project, called from main app */
140    function init() {
141        // set builtin properties
142        $this->setSystemProperties();
143       
144        // load default tasks
145        $taskdefs = Phing::getResourcePath("phing/tasks/defaults.properties");
146       
147        try { // try to load taskdefs
148            $props = new Properties();
149            $in = new PhingFile((string)$taskdefs);
150
151            if ($in === null) {
152                throw new BuildException("Can't load default task list");
153            }
154            $props->load($in);
155
156            foreach ($props as $key => $value) 
157                $this->addTaskDefinition($key, $value);
158
159        } catch (IOException $ioe) {
160            throw new BuildException("Can't load default task list");
161        }
162
163        // load default tasks
164        $typedefs = Phing::getResourcePath("phing/types/defaults.properties");
165
166        try { // try to load typedefs
167            $props = new Properties();
168            $in    = new PhingFile((string)$typedefs);
169            if ($in === null) {
170                throw new BuildException("Can't load default datatype list");
171            }
172            $props->load($in);
173
174            foreach ($props as $key => $value) 
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    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        // command line properties take precedence
202        if (isset($this->userProperties[$name])) {
203            $this->log("Override ignored for user property " . $name, Project::MSG_VERBOSE);
204            return;
205        }
206
207        if (isset($this->properties[$name])) {
208            $this->log("Overriding previous definition of property " . $name, Project::MSG_VERBOSE);
209        }
210
211        $this->log("Setting project property: " . $name . " -> " . $value, Project::MSG_DEBUG);
212        $this->properties[$name] = $value;
213    }
214
215    /**
216     * Sets a property if no value currently exists. If the property
217     * exists already, a message is logged and the method returns with
218     * no other effect.
219     *
220     * @param string $name The name of property to set.
221     *             Must not be <code>null</code>.
222     * @param string $value The new value of the property.
223     *              Must not be <code>null</code>.
224     * @since 2.0
225     */
226    public function setNewProperty($name, $value) {
227        if (isset($this->properties[$name])) {
228            $this->log("Override ignored for property " . $name, Project::MSG_DEBUG);
229            return;
230        }
231        $this->log("Setting project property: " . $name . " -> " . $value, Project::MSG_DEBUG);
232        $this->properties[$name] = $value;
233    }
234
235    /**
236     * Sets a user property, which cannot be overwritten by
237     * set/unset property calls. Any previous value is overwritten.
238     * @param string $name The name of property to set.
239     *             Must not be <code>null</code>.
240     * @param string $value The new value of the property.
241     *              Must not be <code>null</code>.
242     * @see #setProperty()
243     */
244    public function setUserProperty($name, $value) {
245        $this->log("Setting ro project property: " . $name . " -> " . $value, Project::MSG_DEBUG);
246        $this->userProperties[$name] = $value;
247        $this->properties[$name] = $value;
248    }
249
250    /**
251     * Sets a user property, which cannot be overwritten by set/unset
252     * property calls. Any previous value is overwritten. Also marks
253     * these properties as properties that have not come from the
254     * command line.
255     *
256     * @param string $name The name of property to set.
257     *             Must not be <code>null</code>.
258     * @param string $value The new value of the property.
259     *              Must not be <code>null</code>.
260     * @see #setProperty()
261     */
262    public function setInheritedProperty($name, $value) {
263        $this->inheritedProperties[$name] = $value;
264        $this->setUserProperty($name, $value);
265    }
266
267    /**
268     * Sets a property unless it is already defined as a user property
269     * (in which case the method returns silently).
270     *
271     * @param name The name of the property.
272     *             Must not be <code>null</code>.
273     * @param value The property value. Must not be <code>null</code>.
274     */
275    private function setPropertyInternal($name, $value) {
276        if (isset($this->userProperties[$name])) {
277            $this->log("Override ignored for user property " . $name, Project::MSG_VERBOSE);
278            return;
279        }
280        $this->properties[$name] = $value;
281    }
282
283    /**
284     * Returns the value of a property, if it is set.
285     *
286     * @param string $name The name of the property.
287     *             May be <code>null</code>, in which case
288     *             the return value is also <code>null</code>.
289     * @return string The property value, or <code>null</code> for no match
290     *         or if a <code>null</code> name is provided.
291     */
292    public function getProperty($name) {
293        if (!isset($this->propertyExpansionHelper[$name])) {
294            return null;
295        }
296        return $this->propertyExpansionHelper[$name];
297    }
298   
299    /**
300     * Returns a copy of the properties table in which all property
301     * references are being expanded.
302     *
303     * @return array A hashtable containing all properties
304     *         (including user properties).
305     */
306    public function getProperties() {
307        return $this->propertyExpansionHelper;
308    }
309
310    /**
311     * Returns the value of a user property, if it is set.
312     *
313     * @param string $name The name of the property.
314     *             May be <code>null</code>, in which case
315     *             the return value is also <code>null</code>.
316     * @return string  The property value, or <code>null</code> for no match
317     *         or if a <code>null</code> name is provided.
318     */
319     public function getUserProperty($name) {
320        if (!isset($this->userProperties[$name])) {
321            return null;
322        }
323        return $this->getProperty($name);
324    }
325
326    /**
327     * Copies all user properties that have been set on the command
328     * line or a GUI tool from this instance to the Project instance
329     * given as the argument.
330     *
331     * <p>To copy all "user" properties, you will also have to call
332     * {@link #copyInheritedProperties copyInheritedProperties}.</p>
333     *
334     * @param Project $other the project to copy the properties to.  Must not be null.
335     * @return void
336     * @since phing 2.0
337     */
338    public function copyUserProperties(Project $other) {       
339        foreach($this->userProperties as $arg => $value) {
340            if (isset($this->inheritedProperties[$arg])) {
341                continue;
342            }
343            $other->setUserProperty($arg, $value);
344        }
345    }
346
347    /**
348     * Copies all user properties that have not been set on the
349     * command line or a GUI tool from this instance to the Project
350     * instance given as the argument.
351     *
352     * <p>To copy all "user" properties, you will also have to call
353     * {@link #copyUserProperties copyUserProperties}.</p>
354     *
355     * @param other the project to copy the properties to.  Must not be null.
356     *
357     * @since phing 2.0
358     */
359    public function copyInheritedProperties(Project $other) {
360        foreach($this->userProperties as $arg => $value) {
361            if ($other->getUserProperty($arg) !== null) {
362                continue;
363            }
364            $other->setInheritedProperty($arg, $value);
365        }       
366    }
367
368        public function replaceProperties($buffer) {
369                return $this->propertyExpansionHelper->expand($buffer);
370        }
371    // ---------------------------------------------------------
372    //  END Properties methods
373    // ---------------------------------------------------------
374
375
376    function setDefaultTarget($targetName) {
377        $this->defaultTarget = (string) trim($targetName);
378    }
379
380    function getDefaultTarget() {
381        return (string) $this->defaultTarget;
382    }
383
384    /**
385     * Sets the name of the current project
386     *
387     * @param    string   name of project
388     * @return   void
389     * @access   public
390     * @author   Andreas Aderhold, andi@binarycloud.com
391     */
392
393    function setName($name) {
394        $this->name = (string) trim($name);
395        $this->setProperty("phing.project.name", $this->name);
396    }
397
398    /**
399     * Returns the name of this project
400     *
401     * @return  string  projectname
402     * @access  public
403     * @author  Andreas Aderhold, andi@binarycloud.com
404     */
405    function getName() {
406        return (string) $this->name;
407    }
408
409    /** Set the projects description */
410    function setDescription($description) {
411        $this->description = (string) trim($description);
412    }
413
414    /** return the description, null otherwise */
415    function getDescription() {
416        return $this->description;
417    }
418
419    /** Set the minimum required phing version **/
420    function setPhingVersion($version) {
421        $version = str_replace('phing', '', strtolower($version));
422        $this->phingVersion = (string)trim($version);
423    }
424
425    /** Get the minimum required phing version **/
426    function getPhingVersion() {
427        if($this->phingVersion === null) {
428            $this->setPhingVersion(Phing::getPhingVersion());
429        }
430        return $this->phingVersion;
431    }
432
433    /** Set basedir object from xml*/
434    function setBasedir($dir) {
435        if ($dir instanceof PhingFile) {
436            $dir = $dir->getAbsolutePath();
437        }
438
439        $dir = $this->fileUtils->normalize($dir);
440
441        $dir = new PhingFile((string) $dir);
442        if (!$dir->exists()) {
443            throw new BuildException("Basedir ".$dir->getAbsolutePath()." does not exist");
444        }
445        if (!$dir->isDirectory()) {
446            throw new BuildException("Basedir ".$dir->getAbsolutePath()." is not a directory");
447        }
448        $this->basedir = $dir;
449        $this->setPropertyInternal("project.basedir", $this->basedir->getAbsolutePath());
450        $this->log("Project base dir set to: " . $this->basedir->getPath(), Project::MSG_VERBOSE);
451       
452        // [HL] added this so that ./ files resolve correctly.  This may be a mistake ... or may be in wrong place.               
453        chdir($dir->getAbsolutePath());
454    }
455
456    /**
457     * Returns the basedir of this project
458     *
459     * @return  PhingFile  Basedir PhingFile object
460     * @access  public
461     * @throws  BuildException
462     * @author  Andreas Aderhold, andi@binarycloud.com
463     */
464    function getBasedir() {
465        if ($this->basedir === null) {           
466            try { // try to set it
467                $this->setBasedir(".");
468            } catch (BuildException $exc) {
469                throw new BuildException("Can not set default basedir. ".$exc->getMessage());
470            }
471        }
472        return $this->basedir;
473    }
474
475    /**
476     * Sets system properties and the environment variables for this project.
477     *
478     * @return void
479     */
480    function setSystemProperties() {
481       
482        // first get system properties
483        foreach( self::getProperties() as $name => $value)
484            $this->setPropertyInternal($name, $value);
485
486                foreach(Phing::getProperties() as $name => $value)
487            $this->setPropertyInternal($name, $value);
488       
489        // and now the env vars
490        foreach($_SERVER as $name => $value) {
491            // skip arrays
492            if (is_array($value)) {
493                continue;
494            }
495            $this->setPropertyInternal('env.' . $name, $value);
496        }
497        return true;
498    }
499
500
501    /**
502     * Adds a task definition.
503     * @param string $name Name of tag.
504     * @param string $class The class path to use.
505     * @param string $classpath The classpat to use.
506     */
507    function addTaskDefinition($name, $class, $classpath = null) {
508        $name  = $name;
509        $class = $class;
510        if ($class === "") {
511            $this->log("Task $name has no class defined.", Project::MSG_ERR);
512        }  elseif (!isset($this->taskdefs[$name])) {
513            Phing::import($class, $classpath);
514            $this->taskdefs[$name] = $class;
515            $this->log("  +Task definiton: $name ($class)", Project::MSG_DEBUG);
516        } else {
517            $this->log("Task $name ($class) already registerd, skipping", Project::MSG_VERBOSE);
518        }
519    }
520
521    function getTaskDefinitions() {
522        return $this->taskdefs;
523    }
524
525    /**
526     * Adds a data type definition.
527     * @param string $name Name of tag.
528     * @param string $class The class path to use.
529     * @param string $classpath The classpat to use.
530     */
531    function addDataTypeDefinition($typeName, $typeClass, $classpath = null) {   
532        if (!isset($this->typedefs[$typeName])) {       
533            Phing::import($typeClass, $classpath);
534            $this->typedefs[$typeName] = $typeClass;
535            $this->log("  +User datatype: $typeName ($typeClass)", Project::MSG_DEBUG);
536        } else {
537            $this->log("Type $typeName ($typeClass) already registerd, skipping", Project::MSG_VERBOSE);
538        }
539    }
540
541    function getDataTypeDefinitions() {
542        return $this->typedefs;
543    }
544
545    /** add a new target to the project */
546    function addTarget($targetName, &$target) {
547        if (isset($this->targets[$targetName])) {
548            throw new BuildException("Duplicate target: $targetName");
549        }
550        $this->addOrReplaceTarget($targetName, $target);
551    }
552
553    function addOrReplaceTarget($targetName, &$target) {
554        $this->log("  +Target: $targetName", Project::MSG_DEBUG);
555        $target->setProject($this);
556        $this->targets[$targetName] = $target;
557    }
558
559    function getTargets() {
560        return $this->targets;
561    }
562
563    /**
564     * Create a new task instance and return reference to it. This method is
565     * sorta factory like. A _local_ instance is created and a reference returned to
566     * that instance. Usually PHP destroys local variables when the function call
567     * ends. But not if you return a reference to that variable.
568     * This is kinda error prone, because if no reference exists to the variable
569     * it is destroyed just like leaving the local scope with primitive vars. There's no
570     * central place where the instance is stored as in other OOP like languages.
571     *
572     * [HL] Well, ZE2 is here now, and this is  still working. We'll leave this alone
573     * unless there's any good reason not to.
574     *
575     * @param    string    $taskType    Task name
576     * @return   Task                A task object
577     * @throws   BuildException
578     *           Exception
579     */
580    function createTask($taskType) {
581        try {
582            $classname = "";
583            $tasklwr = strtolower($taskType);
584            foreach ($this->taskdefs as $name => $class) {
585                if (strtolower($name) === $tasklwr) {
586                    $classname = $class;
587                    break;
588                }
589            }
590           
591            if ($classname === "") {
592                return null;
593            }
594           
595            $cls = Phing::import($classname);
596           
597            if (!class_exists($cls)) {
598                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.)");
599            }
600           
601            $o = new $cls();
602   
603            if ($o instanceof Task) {
604                $task = $o;
605            } else {
606                $this->log ("  (Using TaskAdapter for: $taskType)", Project::MSG_DEBUG);
607                // not a real task, try adapter
608                $taskA = new TaskAdapter();
609                $taskA->setProxy($o);
610                $task = $taskA;
611            }
612            $task->setProject($this);
613            $task->setTaskType($taskType);
614            // set default value, can be changed by the user
615            $task->setTaskName($taskType);
616            $this->log ("  +Task: " . $taskType, Project::MSG_DEBUG);
617        } catch (Exception $t) {
618            throw new BuildException("Could not create task of type: " . $taskType, $t);
619        }
620        // everything fine return reference
621        return $task;
622    }
623
624    /**
625     * Create a datatype instance and return reference to it
626     * See createTask() for explanation how this works
627     *
628     * @param   string   Type name
629     * @return  object   A datatype object
630     * @throws  BuildException
631     *          Exception
632     */
633    function createDataType($typeName) {       
634        try {
635            $cls = "";
636            $typelwr = strtolower($typeName);
637            foreach ($this->typedefs as $name => $class) {
638                if (strtolower($name) === $typelwr) {
639                    $cls = StringHelper::unqualify($class);                                   
640                    break;
641                }
642            }
643           
644            if ($cls === "") {
645                return null;
646            }
647           
648            if (!class_exists($cls)) {
649                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.)");
650            }
651           
652            $type = new $cls();
653            $this->log("  +Type: $typeName", Project::MSG_DEBUG);
654            if (!($type instanceof DataType)) {
655                throw new Exception("$class is not an instance of phing.types.DataType");
656            }
657            if ($type instanceof ProjectComponent) {
658                $type->setProject($this);
659            }
660        } catch (Exception $t) {
661            throw new BuildException("Could not create type: $typeName", $t);
662        }
663        // everything fine return reference
664        return $type;
665    }
666
667    /**
668     * Executes a list of targets
669     *
670     * @param   array  List of target names to execute
671     * @return  void
672     * @throws  BuildException
673     */
674    function executeTargets($targetNames) {
675        foreach($targetNames as $tname) {
676            $this->executeTarget($tname);
677        }
678    }
679
680    /**
681     * Executes a target
682     *
683     * @param   string  Name of Target to execute
684     * @return  void
685     * @throws  BuildException
686     */
687    function executeTarget($targetName) {
688
689        // complain about executing void
690        if ($targetName === null) {
691            throw new BuildException("No target specified");
692        }
693
694        // invoke topological sort of the target tree and run all targets
695        // until targetName occurs.
696        foreach ($this->_topoSort($targetName) as $t) {
697
698            try {
699                $t->performTasks();
700            } catch (BuildException $exc) {
701                $this->log("Execution of target \"".$t->getName()."\" failed for the following reason: ".$exc->getMessage(), Project::MSG_ERR);
702                throw $exc;
703            }
704
705            if ($t === $this->targets[$targetName])
706               return;
707        }       
708    }
709
710
711    function resolveFile($fileName, $rootDir = null) {
712        if ($rootDir === null) {
713            return $this->fileUtils->resolveFile($this->basedir, $fileName);
714        } else {
715            return $this->fileUtils->resolveFile($rootDir, $fileName);
716        }
717    }   
718
719    protected function _targetSequenceToString($seq) {
720       $r = "";
721       foreach ($seq as $t) $r .= $t->toString() . " ";
722       return $r;
723    }
724   
725    /**
726     * Topologically sort a set of Targets.
727     * @param  $root is the (String) name of the root Target. The sort is
728     *         created in such a way that the sequence of Targets until the root
729     *         target is the minimum possible such sequence.
730     * @param  $targets is a array representing a "name to Target" mapping
731     * @return An array of Strings with the names of the targets in
732     *         sorted order.
733     */
734    function _topoSort($root) {
735
736        $root     = (string) $root;
737        $ret      = array();
738        $state    = array();
739        $visiting = array();
740
741        // We first run a DFS based sort using the root as the starting node.
742        // This creates the minimum sequence of Targets to the root node.
743        // We then do a sort on any remaining unVISITED targets.
744        // This is unnecessary for doing our build, but it catches
745        // circular dependencies or missing Targets on the entire
746        // dependency tree, not just on the Targets that depend on the
747        // build Target.
748
749        $this->_tsort($root, $state, $visiting, $ret);
750
751        $this->log("Build sequence for target '$root' is: " . $this->_targetSequenceToString($ret), Project::MSG_VERBOSE);
752
753        foreach ($this->targets as $t) {
754               $name = $t->getName(); // "canonical" name
755                       if (!isset($state[$name])) {
756                               $this->_tsort($name, $state, $visiting, $ret);
757                       } else if ($state[$name] == 'VISITING') {
758                               throw new Exception("Unexpected node in visiting state: $name");
759            }
760        }
761
762        $this->log("Complete build sequence is: " . $this->_targetSequenceToString($ret), Project::MSG_VERBOSE);
763
764        return $ret;
765    }
766
767    // one step in a recursive DFS traversal of the target dependency tree.
768    // - The array "state" contains the state (VISITED or VISITING or null)
769    //   of all the target names.
770    // - The stack "visiting" contains a stack of target names that are
771    //   currently on the DFS stack. (NB: the target names in "visiting" are
772    //    exactly the target names in "state" that are in the VISITING state.)
773    // 1. Set the current target to the VISITING state, and push it onto
774    //    the "visiting" stack.
775    // 2. Throw a BuildException if any child of the current node is
776    //    in the VISITING state (implies there is a cycle.) It uses the
777    //    "visiting" Stack to construct the cycle.
778    // 3. If any children have not been VISITED, tsort() the child.
779    // 4. Add the current target to the Vector "ret" after the children
780    //    have been visited. Move the current target to the VISITED state.
781    //    "ret" now contains the sorted sequence of Targets upto the current
782    //    Target.
783
784    function _tsort($root, &$state, &$visiting, &$ret) {
785
786       // Make sure target named $root exists.
787        if (!isset($this->targets[$root]) || !($this->targets[$root] instanceof Target)) {
788            $sb = "Target '$root' does not exist in this project.";
789            if (!empty($visiting)) {
790                $parent = end($visiting);
791                $sb .= "It is used from target '$parent'.";
792            }
793            throw new BuildException($sb);
794        }
795
796        // Fetch $root's "canonical" name
797        $target = $this->targets[$root];
798        $name = $target->getName();
799       
800        // Put $root on DFS stack
801        $state[$name] = 'VISITING';
802        $visiting[] = $name;
803
804        // Recursively visit $root's dependencies
805        foreach ($target->getDependencies() as $dep) {
806
807               // Map names to "canonical" form.
808               $t = $this->targets[$dep];
809               if (!$t) throw new BuildException("Unknown depencency $dep in target $root.");
810               
811               $depname = $t->getName();
812       
813               if (!isset($state[$depname]))
814                       $this->_tsort($depname, $state, $visiting, $ret);
815               else if ($state[$depname] == 'VISITING')
816                       throw $this->_makeCircularException($depname, $visiting);
817        }
818
819        // Finishing/leaving $root.
820        if ($name !== array_pop($visiting)) 
821            throw new Exception("Unexpected internal error: expected to pop $name but got $p");
822        $state[$name] = "VISITED";
823        $ret[] = $target;
824    }
825
826    function _makeCircularException($end, $stk) {
827        $sb = "Circular dependency: $end";
828        do {
829            $c = (string) array_pop($stk);
830            $sb .= " <- ".$c;
831        } while($c != $end);
832        return new BuildException($sb);
833    }
834
835    /**
836     * Adds a reference to an object. This method is called when the parser
837     * detects a id="foo" attribute. It passes the id as $name and a reference
838     * to the object assigned to this id as $value
839     */
840    function addReference($name, $object) {
841        if (isset($this->references[$name])) {
842            $this->log("Overriding previous definition of reference to $name", Project::MSG_WARN);
843        }
844        $this->log("Adding reference: $name -> ".get_class($object), Project::MSG_DEBUG);
845        $this->references[$name] = $object;
846    }
847
848    /**
849     * Returns the references array.
850     * @return array
851     */
852    function getReferences() {
853        return $this->references;
854    }
855   
856    /**
857     * Returns a specific reference.
858     * @param string $key The reference id/key.
859     * @return Reference or null if not defined
860     */
861    function getReference($key)
862    {
863        if (isset($this->references[$key])) {
864            return $this->references[$key];
865        }
866        return null; // just to be explicit
867    }
868
869    /**
870     * Abstracting and simplifyling Logger calls for project messages
871     */
872    function log($msg, $level = Project::MSG_INFO) {
873        $this->logObject($this, $msg, $level);
874    }
875
876    function logObject($obj, $msg, $level) {
877        $this->fireMessageLogged($obj, $msg, $level);
878    }
879
880    function addBuildListener(BuildListener $listener) {
881        $this->listeners[] = $listener;
882    }
883
884    function removeBuildListener(BuildListener $listener) {
885        $newarray = array();
886        for ($i=0, $size=count($this->listeners); $i < $size; $i++) {
887            if ($this->listeners[$i] !== $listener) {
888                $newarray[] = $this->listeners[$i];
889            }
890        }
891        $this->listeners = $newarray;
892    }
893
894    function getBuildListeners() {
895        return $this->listeners;
896    }
897
898    function fireBuildStarted() {
899        $event = new BuildEvent($this);       
900        foreach($this->listeners as $listener) {
901            $listener->buildStarted($event);
902        }
903    }
904
905    function fireBuildFinished($exception) {       
906        $event = new BuildEvent($this);
907        $event->setException($exception);
908        foreach($this->listeners as $listener) {
909            $listener->buildFinished($event);
910        }
911    }
912
913    function fireTargetStarted($target) {
914        $event = new BuildEvent($target);       
915           foreach($this->listeners as $listener) {
916            $listener->targetStarted($event);
917        }
918    }
919
920    function fireTargetFinished($target, $exception) {
921        $event = new BuildEvent($target);       
922        $event->setException($exception);
923        foreach($this->listeners as $listener) {
924            $listener->targetFinished($event);
925        }
926    }
927
928    function fireTaskStarted($task) {
929        $event = new BuildEvent($task);       
930        foreach($this->listeners as $listener) {
931            $listener->taskStarted($event);
932        }
933    }
934
935    function fireTaskFinished($task, $exception) {
936        $event = new BuildEvent($task);       
937        $event->setException($exception);
938        foreach($this->listeners as $listener) {
939            $listener->taskFinished($event);
940        }
941    }
942
943    function fireMessageLoggedEvent($event, $message, $priority) {
944        $event->setMessage($message, $priority);
945        foreach($this->listeners as $listener) {
946            $listener->messageLogged($event);
947        }
948    }
949
950    function fireMessageLogged($object, $message, $priority) {
951        $this->fireMessageLoggedEvent(new BuildEvent($object), $message, $priority);
952    }
953}
Note: See TracBrowser for help on using the repository browser.