1
<?php
2
/**
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the LGPL. For more information please see
17
 * <http://phing.info>.
18
 */
19

20
/**
21
 *  The Phing project class. Represents a completely configured Phing project.
22
 *  The class defines the project and all tasks/targets. It also contains
23
 *  methods to start a build as well as some properties and FileSystem
24
 *  abstraction.
25
 *
26
 * @author Andreas Aderhold <andi@binarycloud.com>
27
 * @author Hans Lellelid <hans@xmpl.org>
28
 *
29
 * @package phing
30
 */
31
class Project
32
{
33

34
    // Logging level constants.
35
    public const MSG_DEBUG = 4;
36
    public const MSG_VERBOSE = 3;
37
    public const MSG_INFO = 2;
38
    public const MSG_WARN = 1;
39
    public const MSG_ERR = 0;
40

41
    /**
42
     * contains the targets
43
     *
44
     * @var Target[]
45
     */
46
    private $targets = [];
47
    /**
48
     * global filterset (future use)
49
     */
50
    private $globalFilterSet = [];
51
    /**
52
     * all globals filters (future use)
53
     */
54
    private $globalFilters = [];
55

56
    /**
57
     * holds ref names and a reference to the referred object
58
     */
59
    private $references = [];
60

61
    /**
62
     * The InputHandler being used by this project.
63
     *
64
     * @var InputHandler
65
     */
66
    private $inputHandler;
67

68
    /* -- properties that come in via xml attributes -- */
69

70
    /**
71
     * basedir (PhingFile object)
72
     */
73
    private $basedir;
74

75
    /**
76
     * the default target name
77
     */
78
    private $defaultTarget = 'all';
79

80
    /**
81
     * project name (required)
82
     */
83
    private $name;
84

85
    /**
86
     * project description
87
     */
88
    private $description;
89

90
    /**
91
     * require phing version
92
     */
93
    private $phingVersion;
94

95
    /**
96
     * project strict mode
97
     */
98
    private $strictMode = false;
99

100
    /**
101
     * a FileUtils object
102
     */
103
    private $fileUtils;
104

105
    /**
106
     * Build listeneers
107
     */
108
    private $listeners = [];
109

110
    /**
111
     * Keep going flag.
112
     */
113
    private $keepGoingMode = false;
114

115
    /**
116
     * @var string[]
117
     */
118
    private $executedTargetNames = [];
119

120
    /**
121
     *  Constructor, sets any default vars.
122
     */
123 1
    public function __construct()
124
    {
125 1
        $this->fileUtils = new FileUtils();
126
    }
127

128
    /**
129
     * Sets the input handler
130
     *
131
     * @param InputHandler $handler
132
     */
133 1
    public function setInputHandler($handler)
134
    {
135 1
        $this->inputHandler = $handler;
136
    }
137

138
    /**
139
     * Retrieves the current input handler.
140
     *
141
     * @return InputHandler
142
     */
143 1
    public function getInputHandler()
144
    {
145 1
        return $this->inputHandler;
146
    }
147

148
    /**
149
     * inits the project, called from main app
150
     */
151 1
    public function init()
152
    {
153
        // set builtin properties
154 1
        $this->setSystemProperties();
155

156 1
        $componentHelper = ComponentHelper::getComponentHelper($this);
157

158 1
        $componentHelper->initDefaultDefinitions();
159
    }
160

161
    /**
162
     * Create and initialize a subproject. By default the subproject will be of
163
     * the same type as its parent. If a no-arg constructor is unavailable, the
164
     * <code>Project</code> class will be used.
165
     * @return Project instance configured as a subproject of this Project.
166
     */
167 1
    public function createSubProject(): \Project
168
    {
169
        try {
170 1
            $ref = new ReflectionObject($this);
171 1
            $subProject = $ref->newInstance();
172 0
        } catch (ReflectionException $e) {
173 0
            $subProject = new Project();
174
        }
175 1
        $this->initSubProject($subProject);
176 1
        return $subProject;
177
    }
178

179
    /**
180
     * Initialize a subproject.
181
     * @param Project $subProject the subproject to initialize.
182
     */
183 1
    public function initSubProject(Project $subProject): void
184
    {
185 1
        ComponentHelper::getComponentHelper($subProject)
186 1
            ->initSubProject(ComponentHelper::getComponentHelper($this));
187 1
        $subProject->setKeepGoingMode($this->isKeepGoingMode());
188 1
        $subProject->setStrictMode($this->strictMode);
189
    }
190

191
    /**
192
     * returns the global filterset (future use)
193
     */
194 0
    public function getGlobalFilterSet()
195
    {
196 0
        return $this->globalFilterSet;
197
    }
198

199
    // ---------------------------------------------------------
200
    // Property methods
201
    // ---------------------------------------------------------
202

203
    /**
204
     * Sets a property. Any existing property of the same name
205
     * is overwritten, unless it is a user property.
206
     *
207
     * @param string $name The name of property to set.
208
     *                       Must not be <code>null</code>.
209
     * @param string $value The new value of the property.
210
     *                       Must not be <code>null</code>.
211
     */
212 1
    public function setProperty($name, $value)
213
    {
214 1
        PropertyHelper::getPropertyHelper($this)->setProperty(null, $name, $value, true);
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 1
    public function setNewProperty($name, $value)
229
    {
230 1
        PropertyHelper::getPropertyHelper($this)->setNewProperty(null, $name, $value);
231
    }
232

233
    /**
234
     * Sets a user property, which cannot be overwritten by
235
     * set/unset property calls. Any previous value is overwritten.
236
     *
237
     * @param string $name The name of property to set.
238
     *                      Must not be <code>null</code>.
239
     * @param string $value The new value of the property.
240
     *                      Must not be <code>null</code>.
241
     * @see   setProperty()
242
     */
243 1
    public function setUserProperty($name, $value)
244
    {
245 1
        PropertyHelper::getPropertyHelper($this)->setUserProperty(null, $name, $value);
246
    }
247

248
    /**
249
     * Sets a user property, which cannot be overwritten by set/unset
250
     * property calls. Any previous value is overwritten. Also marks
251
     * these properties as properties that have not come from the
252
     * command line.
253
     *
254
     * @param string $name The name of property to set.
255
     *                      Must not be <code>null</code>.
256
     * @param string $value The new value of the property.
257
     *                      Must not be <code>null</code>.
258
     * @see   setProperty()
259
     */
260 1
    public function setInheritedProperty($name, $value)
261
    {
262 1
        PropertyHelper::getPropertyHelper($this)->setInheritedProperty(null, $name, $value);
263
    }
264

265
    /**
266
     * Sets a property unless it is already defined as a user property
267
     * (in which case the method returns silently).
268
     *
269
     * @param string $name The name of the property.
270
     *                      Must not be
271
     *                      <code>null</code>.
272
     * @param string $value The property value. Must not be <code>null</code>.
273
     */
274 1
    private function setPropertyInternal($name, $value)
275
    {
276 1
        PropertyHelper::getPropertyHelper($this)->setProperty(null, $name, $value, false);
277
    }
278

279
    /**
280
     * Returns the value of a property, if it is set.
281
     *
282
     * @param  string $name The name of the property.
283
     *                      May be <code>null</code>, in which case
284
     *                      the return value is also <code>null</code>.
285
     * @return string The property value, or <code>null</code> for no match
286
     *                     or if a <code>null</code> name is provided.
287
     */
288 1
    public function getProperty($name)
289
    {
290 1
        return PropertyHelper::getPropertyHelper($this)->getProperty(null, $name);
291
    }
292

293
    /**
294
     * Replaces ${} style constructions in the given value with the
295
     * string value of the corresponding data types.
296
     *
297
     * @param string $value The value string to be scanned for property references.
298
     *                      May be <code>null</code>.
299
     *
300
     * @return string the given string with embedded property names replaced
301
     *                by values, or <code>null</code> if the given string is
302
     *                <code>null</code>.
303
     *
304
     * @throws BuildException if the given value has an unclosed
305
     *                           property name, e.g. <code>${xxx</code>
306
     */
307 1
    public function replaceProperties($value)
308
    {
309 1
        return PropertyHelper::getPropertyHelper($this)->replaceProperties($value, $this->getProperties());
310
    }
311

312
    /**
313
     * Returns the value of a user property, if it is set.
314
     *
315
     * @param  string $name The name of the property.
316
     *                      May be <code>null</code>, in which case
317
     *                      the return value is also <code>null</code>.
318
     * @return string The property value, or <code>null</code> for no match
319
     *                     or if a <code>null</code> name is provided.
320
     */
321 1
    public function getUserProperty($name)
322
    {
323 1
        return PropertyHelper::getPropertyHelper($this)->getUserProperty(null, $name);
324
    }
325

326
    /**
327
     * Returns a copy of the properties table.
328
     *
329
     * @return array A hashtable containing all properties
330
     *               (including user properties).
331
     */
332 1
    public function getProperties()
333
    {
334 1
        return PropertyHelper::getPropertyHelper($this)->getProperties();
335
    }
336

337
    /**
338
     * Returns a copy of the user property hashtable
339
     *
340
     * @return array a hashtable containing just the user properties
341
     */
342 0
    public function getUserProperties()
343
    {
344 0
        return PropertyHelper::getPropertyHelper($this)->getUserProperties();
345
    }
346

347 0
    public function getInheritedProperties()
348
    {
349 0
        return PropertyHelper::getPropertyHelper($this)->getInheritedProperties();
350
    }
351

352
    /**
353
     * Copies all user properties that have been set on the command
354
     * line or a GUI tool from this instance to the Project instance
355
     * given as the argument.
356
     *
357
     * <p>To copy all "user" properties, you will also have to call
358
     * {@link #copyInheritedProperties copyInheritedProperties}.</p>
359
     *
360
     * @param  Project $other the project to copy the properties to.  Must not be null.
361
     * @return void
362
     * @since  phing 2.0
363
     */
364 1
    public function copyUserProperties(Project $other)
365
    {
366 1
        PropertyHelper::getPropertyHelper($this)->copyUserProperties($other);
367
    }
368

369
    /**
370
     * Copies all user properties that have not been set on the
371
     * command line or a GUI tool from this instance to the Project
372
     * instance given as the argument.
373
     *
374
     * <p>To copy all "user" properties, you will also have to call
375
     * {@link #copyUserProperties copyUserProperties}.</p>
376
     *
377
     * @param Project $other the project to copy the properties to.  Must not be null.
378
     *
379
     * @since phing 2.0
380
     */
381 1
    public function copyInheritedProperties(Project $other)
382
    {
383 1
        PropertyHelper::getPropertyHelper($this)->copyUserProperties($other);
384
    }
385

386
    // ---------------------------------------------------------
387
    //  END Properties methods
388
    // ---------------------------------------------------------
389

390
    /**
391
     * Sets default target
392
     *
393
     * @param string $targetName
394
     */
395 1
    public function setDefaultTarget($targetName)
396
    {
397 1
        $this->defaultTarget = (string) trim($targetName);
398
    }
399

400
    /**
401
     * Returns default target
402
     *
403
     * @return string
404
     */
405 1
    public function getDefaultTarget()
406
    {
407 1
        return (string) $this->defaultTarget;
408
    }
409

410
    /**
411
     * Sets the name of the current project
412
     *
413
     * @param  string $name name of project
414
     * @return void
415
     * @author Andreas Aderhold, andi@binarycloud.com
416
     */
417 1
    public function setName($name)
418
    {
419 1
        $this->name = (string) trim($name);
420 1
        $this->setUserProperty("phing.project.name", $this->name);
421
    }
422

423
    /**
424
     * Returns the name of this project
425
     *
426
     * @return string projectname
427
     * @author Andreas Aderhold, andi@binarycloud.com
428
     */
429 1
    public function getName()
430
    {
431 1
        return (string) $this->name;
432
    }
433

434
    /**
435
     * Set the projects description
436
     *
437
     * @param string $description
438
     */
439 1
    public function setDescription($description)
440
    {
441 1
        $this->description = $description;
442
    }
443

444
    /**
445
     * return the description, null otherwise
446
     *
447
     * @return string|null
448
     */
449 1
    public function getDescription()
450
    {
451 1
        if ($this->description === null) {
452 1
            $this->description = Description::getAll($this);
453
        }
454 1
        return $this->description;
455
    }
456

457
    /**
458
     * Set the minimum required phing version
459
     *
460
     * @param string $version
461
     */
462 1
    public function setPhingVersion($version)
463
    {
464 1
        $version = str_replace('phing', '', strtolower($version));
465 1
        $this->phingVersion = (string) trim($version);
466
    }
467

468
    /**
469
     * Get the minimum required phing version
470
     *
471
     * @return string
472
     */
473 1
    public function getPhingVersion()
474
    {
475 1
        if ($this->phingVersion === null) {
476 1
            $this->setPhingVersion(Phing::getPhingVersion());
477
        }
478

479 1
        return $this->phingVersion;
480
    }
481

482
    /**
483
     * Sets the strict-mode (status) for the current project
484
     * (If strict mode is On, all the warnings would be converted to an error
485
     * (and the build will be stopped/aborted)
486
     *
487
     * @param bool $strictmode
488
     * @return void
489
     * @access public
490
     * @author Utsav Handa, handautsav@hotmail.com
491
     */
492 1
    public function setStrictMode(bool $strictmode)
493
    {
494 1
        $this->strictMode = $strictmode;
495 1
        $this->setProperty("phing.project.strictmode", $this->strictMode);
496
    }
497

498
    /**
499
     * Get the strict-mode status for the project
500
     *
501
     * @return boolean
502
     */
503 0
    public function getStrictmode()
504
    {
505 0
        return $this->strictMode;
506
    }
507

508
    /**
509
     * Set basedir object from xm
510
     *
511
     * @param  PhingFile|string $dir
512
     * @throws BuildException
513
     */
514 1
    public function setBasedir($dir)
515
    {
516 1
        if ($dir instanceof PhingFile) {
517 1
            $dir = $dir->getAbsolutePath();
518
        }
519

520 1
        $dir = $this->fileUtils->normalize($dir);
521 1
        $dir = FileSystem::getFileSystem()->canonicalize($dir);
522

523 1
        $dir = new PhingFile((string) $dir);
524 1
        if (!$dir->exists()) {
525 0
            throw new BuildException("Basedir " . $dir->getAbsolutePath() . " does not exist");
526
        }
527 1
        if (!$dir->isDirectory()) {
528 0
            throw new BuildException("Basedir " . $dir->getAbsolutePath() . " is not a directory");
529
        }
530 1
        $this->basedir = $dir;
531 1
        $this->setPropertyInternal("project.basedir", $this->basedir->getPath());
532 1
        $this->log("Project base dir set to: " . $this->basedir->getPath(), Project::MSG_VERBOSE);
533

534
        // [HL] added this so that ./ files resolve correctly.  This may be a mistake ... or may be in wrong place.
535 1
        chdir($dir->getAbsolutePath());
536
    }
537

538
    /**
539
     * Returns the basedir of this project
540
     *
541
     * @return PhingFile      Basedir PhingFile object
542
     *
543
     * @throws BuildException
544
     *
545
     * @author Andreas Aderhold, andi@binarycloud.com
546
     */
547 1
    public function getBasedir()
548
    {
549 1
        if ($this->basedir === null) {
550
            try { // try to set it
551 0
                $this->setBasedir(".");
552 0
            } catch (BuildException $exc) {
553 0
                throw new BuildException("Can not set default basedir. " . $exc->getMessage());
554
            }
555
        }
556

557 1
        return $this->basedir;
558
    }
559

560
    /**
561
     * Set &quot;keep-going&quot; mode. In this mode Ant will try to execute
562
     * as many targets as possible. All targets that do not depend
563
     * on failed target(s) will be executed.  If the keepGoing settor/getter
564
     * methods are used in conjunction with the <code>ant.executor.class</code>
565
     * property, they will have no effect.
566
     *
567
     * @param bool $keepGoingMode &quot;keep-going&quot; mode
568
     */
569 1
    public function setKeepGoingMode($keepGoingMode)
570
    {
571 1
        $this->keepGoingMode = $keepGoingMode;
572
    }
573

574
    /**
575
     * Return the keep-going mode.  If the keepGoing settor/getter
576
     * methods are used in conjunction with the <code>phing.executor.class</code>
577
     * property, they will have no effect.
578
     *
579
     * @return bool &quot;keep-going&quot; mode
580
     */
581 1
    public function isKeepGoingMode()
582
    {
583 1
        return $this->keepGoingMode;
584
    }
585

586
    /**
587
     * Sets system properties and the environment variables for this project.
588
     *
589
     * @return void
590
     */
591 1
    public function setSystemProperties()
592
    {
593

594
        // first get system properties
595 1
        $systemP = array_merge($this->getProperties(), Phing::getProperties());
596 1
        foreach ($systemP as $name => $value) {
597 1
            $this->setPropertyInternal($name, $value);
598
        }
599

600
        // and now the env vars
601 1
        foreach ($_SERVER as $name => $value) {
602
            // skip arrays
603 1
            if (is_array($value)) {
604 1
                continue;
605
            }
606 1
            $this->setPropertyInternal('env.' . $name, $value);
607
        }
608
    }
609

610
    /**
611
     * Adds a task definition.
612
     *
613
     * @param string $name Name of tag.
614
     * @param string $class The class path to use.
615
     * @param string $classpath The classpat to use.
616
     */
617 1
    public function addTaskDefinition($name, $class, $classpath = null)
618
    {
619 1
        ComponentHelper::getComponentHelper($this)->addTaskDefinition($name, $class, $classpath);
620
    }
621

622
    /**
623
     * Returns the task definitions
624
     *
625
     * @return array
626
     */
627 1
    public function getTaskDefinitions()
628
    {
629 1
        return ComponentHelper::getComponentHelper($this)->getTaskDefinitions();
630
    }
631

632
    /**
633
     * Adds a data type definition.
634
     *
635
     * @param string $typeName Name of the type.
636
     * @param string $typeClass The class to use.
637
     * @param string $classpath The classpath to use.
638
     */
639 1
    public function addDataTypeDefinition($typeName, $typeClass, $classpath = null)
640
    {
641 1
        ComponentHelper::getComponentHelper($this)->addDataTypeDefinition($typeName, $typeClass, $classpath);
642
    }
643

644
    /**
645
     * Returns the data type definitions
646
     *
647
     * @return array
648
     */
649 1
    public function getDataTypeDefinitions()
650
    {
651 1
        return ComponentHelper::getComponentHelper($this)->getDataTypeDefinitions();
652
    }
653

654
    /**
655
     * Add a new target to the project
656
     *
657
     * @param  string $targetName
658
     * @param  Target $target
659
     * @throws BuildException
660
     */
661 1
    public function addTarget($targetName, $target)
662
    {
663 1
        if (isset($this->targets[$targetName])) {
664 0
            throw new BuildException("Duplicate target: $targetName");
665
        }
666 1
        $this->addOrReplaceTarget($targetName, $target);
667
    }
668

669
    /**
670
     * Adds or replaces a target in the project
671
     *
672
     * @param string $targetName
673
     * @param Target $target
674
     */
675 1
    public function addOrReplaceTarget($targetName, &$target)
676
    {
677 1
        $this->log("  +Target: $targetName", Project::MSG_DEBUG);
678 1
        $target->setProject($this);
679 1
        $this->targets[$targetName] = $target;
680

681 1
        $ctx = $this->getReference(ProjectConfigurator::PARSING_CONTEXT_REFERENCE);
682 1
        $current = $ctx->getCurrentTargets();
683 1
        $current[$targetName] = $target;
684
    }
685

686
    /**
687
     * Returns the available targets
688
     *
689
     * @return Target[]
690
     */
691 1
    public function getTargets()
692
    {
693 1
        return $this->targets;
694
    }
695

696
    /**
697
     * @return string[]
698
     */
699 0
    public function getExecutedTargetNames()
700
    {
701 0
        return $this->executedTargetNames;
702
    }
703

704
    /**
705
     * Create a new task instance and return reference to it.
706
     *
707
     * @param string $taskType Task name
708
     * @return Task           A task object
709
     * @throws BuildException
710
     */
711 1
    public function createTask($taskType)
712
    {
713 1
        return ComponentHelper::getComponentHelper($this)->createTask($taskType);
714
    }
715

716
    /**
717
     * Creates a new condition and returns the reference to it
718
     *
719
     * @param  string $conditionType
720
     * @return Condition
721
     * @throws BuildException
722
     */
723 1
    public function createCondition($conditionType)
724
    {
725 1
        return ComponentHelper::getComponentHelper($this)->createCondition($conditionType);
726
    }
727

728
    /**
729
     * Create a datatype instance and return reference to it
730
     * See createTask() for explanation how this works
731
     *
732
     * @param  string $typeName Type name
733
     * @return object         A datatype object
734
     * @throws BuildException
735
     *                                 Exception
736
     */
737 1
    public function createDataType($typeName)
738
    {
739 1
        return ComponentHelper::getComponentHelper($this)->createDataType($typeName);
740
    }
741

742
    /**
743
     * Executes a list of targets
744
     *
745
     * @param  array $targetNames List of target names to execute
746
     * @return void
747
     * @throws BuildException
748
     */
749 0
    public function executeTargets($targetNames)
750
    {
751 0
        $this->executedTargetNames = $targetNames;
752

753 0
        foreach ($targetNames as $tname) {
754 0
            $this->executeTarget($tname);
755
        }
756
    }
757

758
    /**
759
     * Executes a target
760
     *
761
     * @param  string $targetName Name of Target to execute
762
     * @return void
763
     * @throws BuildException
764
     */
765 1
    public function executeTarget($targetName)
766
    {
767

768
        // complain about executing void
769 1
        if ($targetName === null) {
770 0
            throw new BuildException("No target specified");
771
        }
772

773
        // invoke topological sort of the target tree and run all targets
774
        // until targetName occurs.
775 1
        $sortedTargets = $this->topoSort($targetName);
776

777 1
        $curIndex = (int) 0;
778 1
        $curTarget = null;
779 1
        $thrownException = null;
780 1
        $buildException = null;
781
        do {
782
            try {
783 1
                $curTarget = $sortedTargets[$curIndex++];
784 1
                $curTarget->performTasks();
785 1
            } catch (BuildException $exc) {
786 1
                if (!($this->keepGoingMode)) {
787 1
                    throw $exc;
788
                }
789 0
                $thrownException = $exc;
790
            }
791 1
            if ($thrownException != null) {
792 0
                if ($thrownException instanceof BuildException) {
793 0
                    $this->log(
794 0
                        "Target '" . $curTarget->getName()
795 0
                        . "' failed with message '"
796 0
                        . $thrownException->getMessage() . "'.",
797 0
                        Project::MSG_ERR
798
                    );
799
                    // only the first build exception is reported
800 0
                    if ($buildException === null) {
801 0
                        $buildException = $thrownException;
802
                    }
803
                } else {
804 0
                    $this->log(
805 0
                        "Target '" . $curTarget->getName()
806 0
                        . "' failed with message '"
807 0
                        . $thrownException->getMessage() . "'." . PHP_EOL
808 0
                        . $thrownException->getTraceAsString(),
809 0
                        Project::MSG_ERR
810
                    );
811 0
                    if ($buildException === null) {
812 0
                        $buildException = new BuildException($thrownException);
813
                    }
814
                }
815
            }
816 1
        } while ($curTarget->getName() !== $targetName);
817

818 1
        if ($buildException !== null) {
819 0
            throw $buildException;
820
        }
821
    }
822

823
    /**
824
     * Helper function
825
     *
826
     * @param  string $fileName
827
     * @param  PhingFile $rootDir
828
     * @return \PhingFile
829
     * @throws IOException
830
     */
831 1
    public function resolveFile(string $fileName, PhingFile $rootDir = null): PhingFile
832
    {
833 1
        if ($rootDir === null) {
834 1
            return $this->fileUtils->resolveFile($this->basedir, $fileName);
835
        }
836 1
        return $this->fileUtils->resolveFile($rootDir, $fileName);
837
    }
838

839
    /**
840
     * Return the boolean equivalent of a string, which is considered
841
     * <code>true</code> if either <code>"on"</code>, <code>"true"</code>,
842
     * or <code>"yes"</code> is found, ignoring case.
843
     *
844
     * @param string $s The string to convert to a boolean value.
845
     *
846
     * @return <code>true</code> if the given string is <code>"on"</code>,
847
     *         <code>"true"</code> or <code>"yes"</code>, or
848
     *         <code>false</code> otherwise.
849
     */
850 1
    public static function toBoolean($s)
851
    {
852
        return (
853 1
            strcasecmp($s, 'on') === 0
854 1
            || strcasecmp($s, 'true') === 0
855 1
            || strcasecmp($s, 'yes') === 0
856
            // FIXME next condition should be removed if the boolean behavior for properties will be solved
857 1
            || strcasecmp($s, 1) === 0
858
        );
859
    }
860

861
    /**
862
     * Topologically sort a set of Targets.
863
     *
864
     * @param  string $rootTarget is the (String) name of the root Target. The sort is
865
     *                         created in such a way that the sequence of Targets until the root
866
     *                         target is the minimum possible such sequence.
867
     * @return Target[] targets in sorted order
868
     * @throws Exception
869
     * @throws BuildException
870
     */
871 1
    public function topoSort($rootTarget)
872
    {
873 1
        $rootTarget = (string) $rootTarget;
874 1
        $ret = [];
875 1
        $state = [];
876 1
        $visiting = [];
877

878
        // We first run a DFS based sort using the root as the starting node.
879
        // This creates the minimum sequence of Targets to the root node.
880
        // We then do a sort on any remaining unVISITED targets.
881
        // This is unnecessary for doing our build, but it catches
882
        // circular dependencies or missing Targets on the entire
883
        // dependency tree, not just on the Targets that depend on the
884
        // build Target.
885

886 1
        $this->_tsort($rootTarget, $state, $visiting, $ret);
887

888 1
        $retHuman = "";
889 1
        for ($i = 0, $_i = count($ret); $i < $_i; $i++) {
890 1
            $retHuman .= (string) $ret[$i] . " ";
891
        }
892 1
        $this->log("Build sequence for target '$rootTarget' is: $retHuman", Project::MSG_VERBOSE);
893

894 1
        $keys = array_keys($this->targets);
895 1
        while ($keys) {
896 1
            $curTargetName = (string) array_shift($keys);
897 1
            if (!isset($state[$curTargetName])) {
898 1
                $st = null;
899
            } else {
900 1
                $st = (string) $state[$curTargetName];
901
            }
902

903 1
            if ($st === null) {
904 1
                $this->_tsort($curTargetName, $state, $visiting, $ret);
905 1
            } elseif ($st === "VISITING") {
906 0
                throw new Exception("Unexpected node in visiting state: $curTargetName");
907
            }
908
        }
909

910 1
        $retHuman = "";
911 1
        for ($i = 0, $_i = count($ret); $i < $_i; $i++) {
912 1
            $retHuman .= (string) $ret[$i] . " ";
913
        }
914 1
        $this->log("Complete build sequence is: $retHuman", Project::MSG_VERBOSE);
915

916 1
        return $ret;
917
    }
918

919
    // one step in a recursive DFS traversal of the target dependency tree.
920
    // - The array "state" contains the state (VISITED or VISITING or null)
921
    //   of all the target names.
922
    // - The stack "visiting" contains a stack of target names that are
923
    //   currently on the DFS stack. (NB: the target names in "visiting" are
924
    //    exactly the target names in "state" that are in the VISITING state.)
925
    // 1. Set the current target to the VISITING state, and push it onto
926
    //    the "visiting" stack.
927
    // 2. Throw a BuildException if any child of the current node is
928
    //    in the VISITING state (implies there is a cycle.) It uses the
929
    //    "visiting" Stack to construct the cycle.
930
    // 3. If any children have not been VISITED, tsort() the child.
931
    // 4. Add the current target to the Vector "ret" after the children
932
    //    have been visited. Move the current target to the VISITED state.
933
    //    "ret" now contains the sorted sequence of Targets up to the current
934
    //    Target.
935

936
    /**
937
     * @param $root
938
     * @param $state
939
     * @param $visiting
940
     * @param $ret
941
     * @throws BuildException
942
     * @throws Exception
943
     */
944 1
    public function _tsort($root, &$state, &$visiting, &$ret)
945
    {
946 1
        $state[$root] = "VISITING";
947 1
        $visiting[] = $root;
948

949 1
        if (!isset($this->targets[$root]) || !($this->targets[$root] instanceof Target)) {
950 1
            $target = null;
951
        } else {
952 1
            $target = $this->targets[$root];
953
        }
954

955
        // make sure we exist
956 1
        if ($target === null) {
957 1
            $sb = "Target '$root' does not exist in this project.";
958 1
            array_pop($visiting);
959 1
            if (!empty($visiting)) {
960 0
                $parent = (string) $visiting[count($visiting) - 1];
961 0
                $sb .= " It is a dependency of target '$parent'.";
962
            }
963 1
            throw new BuildException($sb);
964
        }
965

966 1
        $deps = $target->getDependencies();
967

968 1
        while ($deps) {
969 1
            $cur = (string) array_shift($deps);
970 1
            if (!isset($state[$cur])) {
971 1
                $m = null;
972
            } else {
973 1
                $m = (string) $state[$cur];
974
            }
975 1
            if ($m === null) {
976
                // not been visited
977 1
                $this->_tsort($cur, $state, $visiting, $ret);
978 1
            } elseif ($m == "VISITING") {
979
                // currently visiting this node, so have a cycle
980 0
                throw $this->_makeCircularException($cur, $visiting);
981
            }
982
        }
983

984 1
        $p = (string) array_pop($visiting);
985 1
        if ($root !== $p) {
986 0
            throw new Exception("Unexpected internal error: expected to pop $root but got $p");
987
        }
988

989 1
        $state[$root] = "VISITED";
990 1
        $ret[] = $target;
991
    }
992

993
    /**
994
     * @param string $end
995
     * @param array $stk
996
     * @return BuildException
997
     */
998 0
    public function _makeCircularException($end, $stk)
999
    {
1000 0
        $sb = "Circular dependency: $end";
1001
        do {
1002 0
            $c = (string) array_pop($stk);
1003 0
            $sb .= " <- " . $c;
1004 0
        } while ($c != $end);
1005

1006 0
        return new BuildException($sb);
1007
    }
1008

1009
    /**
1010
     * Adds a reference to an object. This method is called when the parser
1011
     * detects a id="foo" attribute. It passes the id as $name and a reference
1012
     * to the object assigned to this id as $value
1013
     *
1014
     * @param string $name
1015
     * @param object $object
1016
     */
1017 1
    public function addReference($name, $object)
1018
    {
1019 1
        $ref = $this->references[$name] ?? null;
1020 1
        if ($ref === $object) {
1021 0
            return;
1022
        }
1023 1
        if ($ref !== null && !$ref instanceof UnknownElement) {
1024 1
            $this->log("Overriding previous definition of reference to $name", Project::MSG_VERBOSE);
1025
        }
1026 1
        $refName = (is_scalar($object) || $object instanceof PropertyValue) ? (string) $object : get_class($object);
1027 1
        $this->log("Adding reference: $name -> " . $refName, Project::MSG_DEBUG);
1028 1
        $this->references[$name] = $object;
1029
    }
1030

1031
    /**
1032
     * Returns the references array.
1033
     *
1034
     * @return array
1035
     */
1036 1
    public function getReferences()
1037
    {
1038 1
        return $this->references;
1039
    }
1040

1041
    /**
1042
     * Returns a specific reference.
1043
     *
1044
     * @param  string $key The reference id/key.
1045
     * @return object Reference or null if not defined
1046
     */
1047 1
    public function getReference($key)
1048
    {
1049 1
        return $this->references[$key] ?? null; // just to be explicit
1050
    }
1051

1052
    /**
1053
     * Does the project know this reference?
1054
     *
1055
     * @param  string $key The reference id/key.
1056
     * @return bool
1057
     */
1058 1
    public function hasReference(string $key): bool
1059
    {
1060 1
        return isset($this->references[$key]);
1061
    }
1062

1063
    /**
1064
     * Abstracting and simplifyling Logger calls for project messages
1065
     *
1066
     * @param string $msg
1067
     * @param int $level
1068
     */
1069 1
    public function log($msg, $level = Project::MSG_INFO)
1070
    {
1071 1
        $this->logObject($this, $msg, $level);
1072
    }
1073

1074
    /**
1075
     * @param mixed $obj
1076
     * @param string $msg
1077
     * @param int $level
1078
     * @param Exception|null $t
1079
     */
1080 1
    public function logObject($obj, $msg, $level, Exception $t = null)
1081
    {
1082 1
        $this->fireMessageLogged($obj, $msg, $level, $t);
1083

1084
        // Checking whether the strict-mode is On, then consider all the warnings
1085
        // as errors.
1086 1
        if (($this->strictMode) && (Project::MSG_WARN == $level)) {
1087 0
            throw new BuildException('Build contains warnings, considered as errors in strict mode', null);
1088
        }
1089
    }
1090

1091
    /**
1092
     * @param BuildListener $listener
1093
     */
1094 1
    public function addBuildListener(BuildListener $listener)
1095
    {
1096 1
        $this->listeners[] = $listener;
1097
    }
1098

1099
    /**
1100
     * @param BuildListener $listener
1101
     */
1102 1
    public function removeBuildListener(BuildListener $listener)
1103
    {
1104 1
        $newarray = [];
1105 1
        for ($i = 0, $size = count($this->listeners); $i < $size; $i++) {
1106 1
            if ($this->listeners[$i] !== $listener) {
1107 1
                $newarray[] = $this->listeners[$i];
1108
            }
1109
        }
1110 1
        $this->listeners = $newarray;
1111
    }
1112

1113
    /**
1114
     * @return array
1115
     */
1116 1
    public function getBuildListeners()
1117
    {
1118 1
        return $this->listeners;
1119
    }
1120

1121 0
    public function fireBuildStarted()
1122
    {
1123 0
        $event = new BuildEvent($this);
1124 0
        foreach ($this->listeners as $listener) {
1125 0
            $listener->buildStarted($event);
1126
        }
1127

1128 0
        $this->log((string) $event, Project::MSG_DEBUG);
1129
    }
1130

1131
    /**
1132
     * @param Exception $exception
1133
     */
1134 0
    public function fireBuildFinished($exception)
1135
    {
1136 0
        $event = new BuildEvent($this);
1137 0
        $event->setException($exception);
1138 0
        foreach ($this->listeners as $listener) {
1139 0
            $listener->buildFinished($event);
1140
        }
1141

1142 0
        $this->log((string) $event, Project::MSG_DEBUG);
1143
    }
1144

1145
    /**
1146
     * @param $target
1147
     */
1148 1
    public function fireTargetStarted($target)
1149
    {
1150 1
        $event = new BuildEvent($target);
1151 1
        foreach ($this->listeners as $listener) {
1152 1
            $listener->targetStarted($event);
1153
        }
1154

1155 1
        $this->log((string) $event, Project::MSG_DEBUG);
1156
    }
1157

1158
    /**
1159
     * @param $target
1160
     * @param $exception
1161
     */
1162 1
    public function fireTargetFinished($target, $exception)
1163
    {
1164 1
        $event = new BuildEvent($target);
1165 1
        $event->setException($exception);
1166 1
        foreach ($this->listeners as $listener) {
1167 1
            $listener->targetFinished($event);
1168
        }
1169

1170 1
        $this->log((string) $event, Project::MSG_DEBUG);
1171
    }
1172

1173
    /**
1174
     * @param $task
1175
     */
1176 1
    public function fireTaskStarted($task)
1177
    {
1178 1
        $event = new BuildEvent($task);
1179 1
        foreach ($this->listeners as $listener) {
1180 1
            $listener->taskStarted($event);
1181
        }
1182

1183 1
        $this->log((string) $event, Project::MSG_DEBUG);
1184
    }
1185

1186
    /**
1187
     * @param $task
1188
     * @param $exception
1189
     */
1190 1
    public function fireTaskFinished($task, $exception)
1191
    {
1192 1
        $event = new BuildEvent($task);
1193 1
        $event->setException($exception);
1194 1
        foreach ($this->listeners as $listener) {
1195 1
            $listener->taskFinished($event);
1196
        }
1197

1198 1
        $this->log((string) $event, Project::MSG_DEBUG);
1199
    }
1200

1201
    /**
1202
     * @param $event
1203
     * @param $message
1204
     * @param $priority
1205
     */
1206 1
    public function fireMessageLoggedEvent(BuildEvent $event, $message, $priority)
1207
    {
1208 1
        $event->setMessage($message, $priority);
1209 1
        foreach ($this->listeners as $listener) {
1210 1
            $listener->messageLogged($event);
1211
        }
1212
    }
1213

1214
    /**
1215
     * @param mixed $object
1216
     * @param string $message
1217
     * @param int $priority
1218
     * @param Exception $t
1219
     * @throws \Exception
1220
     */
1221 1
    public function fireMessageLogged($object, $message, $priority, Exception $t = null)
1222
    {
1223 1
        $event = new BuildEvent($object);
1224 1
        if ($t !== null) {
1225 0
            $event->setException($t);
1226
        }
1227 1
        $this->fireMessageLoggedEvent($event, $message, $priority);
1228
    }
1229
}

Read our documentation on viewing source code .

Loading