Passed
Push — main ( 01fb06...c551fb )
by Siad
06:07
created

Project::fireMessageLoggedEvent()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 3
dl 0
loc 5
ccs 4
cts 4
cp 1
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
5
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
6
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
7
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
8
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
9
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
10
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
11
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
12
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
13
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
14
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
15
 *
16
 * This software consists of voluntary contributions made by many individuals
17
 * and is licensed under the LGPL. For more information please see
18
 * <http://phing.info>.
19
 */
20
21
namespace Phing;
22
23
use Exception;
24
use Phing\Exception\BuildException;
25
use Phing\Input\InputHandler;
26
use Phing\Io\File;
27
use Phing\Io\FileSystem;
28
use Phing\Io\FileUtils;
29
use Phing\Io\IOException;
30
use Phing\Listener\BuildEvent;
31
use Phing\Listener\BuildListener;
32
use Phing\Parser\ProjectConfigurator;
33
use Phing\Task\System\Condition\Condition;
34
use Phing\Type\Description;
35
use Phing\Type\PropertyValue;
36
use ReflectionException;
37
use ReflectionObject;
38
39
/**
40
 *  The Phing project class. Represents a completely configured Phing project.
41
 *  The class defines the project and all tasks/targets. It also contains
42
 *  methods to start a build as well as some properties and FileSystem
43
 *  abstraction.
44
 *
45
 * @author Andreas Aderhold <[email protected]>
46
 * @author Hans Lellelid <[email protected]>
47
 */
48
class Project
49
{
50
    // Logging level constants.
51
    public const MSG_DEBUG = 4;
52
    public const MSG_VERBOSE = 3;
53
    public const MSG_INFO = 2;
54
    public const MSG_WARN = 1;
55
    public const MSG_ERR = 0;
56
57
    /**
58
     * contains the targets.
59
     *
60
     * @var Target[]
61
     */
62
    private $targets = [];
63
    /**
64
     * global filterset (future use).
65
     */
66
    private $globalFilterSet = [];
67
    /**
68
     * all globals filters (future use).
69
     */
70
    private $globalFilters = [];
0 ignored issues
show
introduced by
The private property $globalFilters is not used, and could be removed.
Loading history...
71
72
    /**
73
     * holds ref names and a reference to the referred object.
74
     */
75
    private $references = [];
76
77
    /**
78
     * The InputHandler being used by this project.
79
     *
80
     * @var InputHandler
81
     */
82
    private $inputHandler;
83
84
    // -- properties that come in via xml attributes --
85
86
    /**
87
     * basedir (PhingFile object).
88
     */
89
    private $basedir;
90
91
    /**
92
     * the default target name.
93
     */
94
    private $defaultTarget = 'all';
95
96
    /**
97
     * project name (required).
98
     */
99
    private $name;
100
101
    /**
102
     * project description.
103
     */
104
    private $description;
105
106
    /**
107
     * require phing version.
108
     */
109
    private $phingVersion;
110
111
    /**
112
     * project strict mode.
113
     */
114
    private $strictMode = false;
115
116
    /**
117
     * a FileUtils object.
118
     */
119
    private $fileUtils;
120
121
    /**
122
     * Build listeneers.
123
     */
124
    private $listeners = [];
125
126
    /**
127
     * Keep going flag.
128
     */
129
    private $keepGoingMode = false;
130
131
    /**
132
     * @var string[]
133
     */
134
    private $executedTargetNames = [];
135
136
    /**
137
     *  Constructor, sets any default vars.
138
     */
139 855
    public function __construct()
140
    {
141 855
        $this->fileUtils = new FileUtils();
142 855
    }
143
144
    /**
145
     * Sets the input handler.
146
     *
147
     * @param InputHandler $handler
148
     */
149 33
    public function setInputHandler($handler)
150
    {
151 33
        $this->inputHandler = $handler;
152 33
    }
153
154
    /**
155
     * Retrieves the current input handler.
156
     *
157
     * @return InputHandler
158
     */
159 33
    public function getInputHandler()
160
    {
161 33
        return $this->inputHandler;
162
    }
163
164
    /**
165
     * inits the project, called from main app.
166
     */
167 827
    public function init()
168
    {
169
        // set builtin properties
170 827
        $this->setSystemProperties();
171
172 827
        $componentHelper = ComponentHelper::getComponentHelper($this);
173
174 827
        $componentHelper->initDefaultDefinitions();
175 827
    }
176
177
    /**
178
     * Create and initialize a subproject. By default the subproject will be of
179
     * the same type as its parent. If a no-arg constructor is unavailable, the
180
     * <code>Project</code> class will be used.
181
     *
182
     * @return Project instance configured as a subproject of this Project
183
     */
184 36
    public function createSubProject(): Project
185
    {
186
        try {
187 36
            $ref = new ReflectionObject($this);
188 36
            $subProject = $ref->newInstance();
189
        } catch (ReflectionException $e) {
190
            $subProject = new Project();
191
        }
192 36
        $this->initSubProject($subProject);
193
194 36
        return $subProject;
195
    }
196
197
    /**
198
     * Initialize a subproject.
199
     *
200
     * @param Project $subProject the subproject to initialize
201
     */
202 36
    public function initSubProject(Project $subProject): void
203
    {
204 36
        ComponentHelper::getComponentHelper($subProject)
205 36
            ->initSubProject(ComponentHelper::getComponentHelper($this))
206
        ;
207 36
        $subProject->setKeepGoingMode($this->isKeepGoingMode());
208 36
        $subProject->setStrictMode($this->strictMode);
209 36
    }
210
211
    /**
212
     * returns the global filterset (future use).
213
     */
214
    public function getGlobalFilterSet()
215
    {
216
        return $this->globalFilterSet;
217
    }
218
219
    // ---------------------------------------------------------
220
    // Property methods
221
    // ---------------------------------------------------------
222
223
    /**
224
     * Sets a property. Any existing property of the same name
225
     * is overwritten, unless it is a user property.
226
     *
227
     * @param string $name  The name of property to set.
228
     *                      Must not be <code>null</code>.
229
     * @param string $value The new value of the property.
230
     *                      Must not be <code>null</code>.
231
     */
232 253
    public function setProperty($name, $value)
233
    {
234 253
        PropertyHelper::getPropertyHelper($this)->setProperty(null, $name, $value, true);
235 253
    }
236
237
    /**
238
     * Sets a property if no value currently exists. If the property
239
     * exists already, a message is logged and the method returns with
240
     * no other effect.
241
     *
242
     * @param string $name  The name of property to set.
243
     *                      Must not be <code>null</code>.
244
     * @param string $value The new value of the property.
245
     *                      Must not be <code>null</code>.
246
     *
247
     * @since 2.0
248
     */
249 93
    public function setNewProperty($name, $value)
250
    {
251 93
        PropertyHelper::getPropertyHelper($this)->setNewProperty(null, $name, $value);
252 93
    }
253
254
    /**
255
     * Sets a user property, which cannot be overwritten by
256
     * set/unset property calls. Any previous value is overwritten.
257
     *
258
     * @param string $name  The name of property to set.
259
     *                      Must not be <code>null</code>.
260
     * @param string $value The new value of the property.
261
     *                      Must not be <code>null</code>.
262
     *
263
     * @see   setProperty()
264
     */
265 827
    public function setUserProperty($name, $value)
266
    {
267 827
        PropertyHelper::getPropertyHelper($this)->setUserProperty(null, $name, $value);
268 827
    }
269
270
    /**
271
     * Sets a user property, which cannot be overwritten by set/unset
272
     * property calls. Any previous value is overwritten. Also marks
273
     * these properties as properties that have not come from the
274
     * command line.
275
     *
276
     * @param string $name  The name of property to set.
277
     *                      Must not be <code>null</code>.
278
     * @param string $value The new value of the property.
279
     *                      Must not be <code>null</code>.
280
     *
281
     * @see   setProperty()
282
     */
283 12
    public function setInheritedProperty($name, $value)
284
    {
285 12
        PropertyHelper::getPropertyHelper($this)->setInheritedProperty(null, $name, $value);
286 12
    }
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
     *
295
     * @return string the property value, or <code>null</code> for no match
296
     *                or if a <code>null</code> name is provided
297
     */
298 827
    public function getProperty($name)
299
    {
300 827
        return PropertyHelper::getPropertyHelper($this)->getProperty(null, $name);
301
    }
302
303
    /**
304
     * Replaces ${} style constructions in the given value with the
305
     * string value of the corresponding data types.
306
     *
307
     * @param string $value The value string to be scanned for property references.
308
     *                      May be <code>null</code>.
309
     *
310
     * @throws BuildException if the given value has an unclosed
311
     *                        property name, e.g. <code>${xxx</code>
312
     *
313
     * @return string the given string with embedded property names replaced
314
     *                by values, or <code>null</code> if the given string is
315
     *                <code>null</code>
316
     */
317 684
    public function replaceProperties($value)
318
    {
319 684
        return PropertyHelper::getPropertyHelper($this)->replaceProperties($value, $this->getProperties());
320
    }
321
322
    /**
323
     * Returns the value of a user property, if it is set.
324
     *
325
     * @param string $name The name of the property.
326
     *                     May be <code>null</code>, in which case
327
     *                     the return value is also <code>null</code>.
328
     *
329
     * @return string the property value, or <code>null</code> for no match
330
     *                or if a <code>null</code> name is provided
331
     */
332 1
    public function getUserProperty($name)
333
    {
334 1
        return PropertyHelper::getPropertyHelper($this)->getUserProperty(null, $name);
335
    }
336
337
    /**
338
     * Returns a copy of the properties table.
339
     *
340
     * @return array a hashtable containing all properties
341
     *               (including user properties)
342
     */
343 828
    public function getProperties()
344
    {
345 828
        return PropertyHelper::getPropertyHelper($this)->getProperties();
346
    }
347
348
    /**
349
     * Returns a copy of the user property hashtable.
350
     *
351
     * @return array a hashtable containing just the user properties
352
     */
353
    public function getUserProperties()
354
    {
355
        return PropertyHelper::getPropertyHelper($this)->getUserProperties();
356
    }
357
358
    public function getInheritedProperties()
359
    {
360
        return PropertyHelper::getPropertyHelper($this)->getInheritedProperties();
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
     *
373
     * @since  phing 2.0
374
     */
375 33
    public function copyUserProperties(Project $other)
376
    {
377 33
        PropertyHelper::getPropertyHelper($this)->copyUserProperties($other);
378 33
    }
379
380
    /**
381
     * Copies all user properties that have not been set on the
382
     * command line or a GUI tool from this instance to the Project
383
     * instance given as the argument.
384
     *
385
     * <p>To copy all "user" properties, you will also have to call
386
     * {@link #copyUserProperties copyUserProperties}.</p>
387
     *
388
     * @param Project $other the project to copy the properties to.  Must not be null.
389
     *
390
     * @since phing 2.0
391
     */
392 33
    public function copyInheritedProperties(Project $other)
393
    {
394 33
        PropertyHelper::getPropertyHelper($this)->copyUserProperties($other);
395 33
    }
396
397
    // ---------------------------------------------------------
398
    //  END Properties methods
399
    // ---------------------------------------------------------
400
401
    /**
402
     * Sets default target.
403
     *
404
     * @param string $targetName
405
     */
406 827
    public function setDefaultTarget($targetName)
407
    {
408 827
        $this->defaultTarget = (string) trim($targetName);
409 827
    }
410
411
    /**
412
     * Returns default target.
413
     *
414
     * @return string
415
     */
416 33
    public function getDefaultTarget()
417
    {
418 33
        return (string) $this->defaultTarget;
419
    }
420
421
    /**
422
     * Sets the name of the current project.
423
     *
424
     * @param string $name name of project
425
     *
426
     * @author Andreas Aderhold, [email protected]
427
     */
428 827
    public function setName($name)
429
    {
430 827
        $this->name = (string) trim($name);
431 827
        $this->setUserProperty('phing.project.name', $this->name);
432 827
    }
433
434
    /**
435
     * Returns the name of this project.
436
     *
437
     * @return string projectname
438
     *
439
     * @author Andreas Aderhold, [email protected]
440
     */
441 1
    public function getName()
442
    {
443 1
        return (string) $this->name;
444
    }
445
446
    /**
447
     * Set the projects description.
448
     *
449
     * @param string $description
450
     */
451 6
    public function setDescription($description)
452
    {
453 6
        $this->description = $description;
454 6
    }
455
456
    /**
457
     * return the description, null otherwise.
458
     *
459
     * @return null|string
460
     */
461 6
    public function getDescription()
462
    {
463 6
        if (null === $this->description) {
464 5
            $this->description = Description::getAll($this);
465
        }
466
467 6
        return $this->description;
468
    }
469
470
    /**
471
     * Set the minimum required phing version.
472
     *
473
     * @param string $version
474
     */
475 5
    public function setPhingVersion($version)
476
    {
477 5
        $version = str_replace('phing', '', strtolower($version));
478 5
        $this->phingVersion = (string) trim($version);
479 5
    }
480
481
    /**
482
     * Get the minimum required phing version.
483
     *
484
     * @return string
485
     */
486 5
    public function getPhingVersion()
487
    {
488 5
        if (null === $this->phingVersion) {
489 5
            $this->setPhingVersion(Phing::getPhingVersion());
490
        }
491
492 5
        return $this->phingVersion;
493
    }
494
495
    /**
496
     * Sets the strict-mode (status) for the current project
497
     * (If strict mode is On, all the warnings would be converted to an error
498
     * (and the build will be stopped/aborted).
499
     *
500
     * @author Utsav Handa, [email protected]
501
     */
502 36
    public function setStrictMode(bool $strictmode)
503
    {
504 36
        $this->strictMode = $strictmode;
505 36
        $this->setProperty('phing.project.strictmode', $this->strictMode);
506 36
    }
507
508
    /**
509
     * Get the strict-mode status for the project.
510
     *
511
     * @return bool
512
     */
513
    public function getStrictmode()
514
    {
515
        return $this->strictMode;
516
    }
517
518
    /**
519
     * Set basedir object from xm.
520
     *
521
     * @param File|string $dir
522
     *
523
     * @throws BuildException
524
     */
525 838
    public function setBasedir($dir)
526
    {
527 838
        if ($dir instanceof File) {
528 96
            $dir = $dir->getAbsolutePath();
529
        }
530
531 838
        $dir = $this->fileUtils->normalize($dir);
532 838
        $dir = FileSystem::getFileSystem()->canonicalize($dir);
533
534 838
        $dir = new File((string) $dir);
535 838
        if (!$dir->exists()) {
536
            throw new BuildException('Basedir ' . $dir->getAbsolutePath() . ' does not exist');
537
        }
538 838
        if (!$dir->isDirectory()) {
539
            throw new BuildException('Basedir ' . $dir->getAbsolutePath() . ' is not a directory');
540
        }
541 838
        $this->basedir = $dir;
542 838
        $this->setPropertyInternal('project.basedir', $this->basedir->getPath());
543 838
        $this->log('Project base dir set to: ' . $this->basedir->getPath(), Project::MSG_VERBOSE);
544
545
        // [HL] added this so that ./ files resolve correctly.  This may be a mistake ... or may be in wrong place.
546 838
        chdir($dir->getAbsolutePath());
547 838
    }
548
549
    /**
550
     * Returns the basedir of this project.
551
     *
552
     * @throws BuildException
553
     *
554
     * @return File Basedir PhingFile object
555
     *
556
     * @author Andreas Aderhold, [email protected]
557
     */
558 173
    public function getBasedir()
559
    {
560 173
        if (null === $this->basedir) {
561
            try { // try to set it
562
                $this->setBasedir('.');
563
            } catch (BuildException $exc) {
564
                throw new BuildException('Can not set default basedir. ' . $exc->getMessage());
565
            }
566
        }
567
568 173
        return $this->basedir;
569
    }
570
571
    /**
572
     * Set &quot;keep-going&quot; mode. In this mode Ant will try to execute
573
     * as many targets as possible. All targets that do not depend
574
     * on failed target(s) will be executed.  If the keepGoing settor/getter
575
     * methods are used in conjunction with the <code>ant.executor.class</code>
576
     * property, they will have no effect.
577
     *
578
     * @param bool $keepGoingMode &quot;keep-going&quot; mode
579
     */
580 36
    public function setKeepGoingMode($keepGoingMode)
581
    {
582 36
        $this->keepGoingMode = $keepGoingMode;
583 36
    }
584
585
    /**
586
     * Return the keep-going mode.  If the keepGoing settor/getter
587
     * methods are used in conjunction with the <code>phing.executor.class</code>
588
     * property, they will have no effect.
589
     *
590
     * @return bool &quot;keep-going&quot; mode
591
     */
592 36
    public function isKeepGoingMode()
593
    {
594 36
        return $this->keepGoingMode;
595
    }
596
597
    /**
598
     * Sets system properties and the environment variables for this project.
599
     */
600 827
    public function setSystemProperties()
601
    {
602
        // first get system properties
603 827
        $systemP = array_merge($this->getProperties(), Phing::getProperties());
604 827
        foreach ($systemP as $name => $value) {
605 827
            $this->setPropertyInternal($name, $value);
606
        }
607
608
        // and now the env vars
609 827
        foreach ($_SERVER as $name => $value) {
610
            // skip arrays
611 827
            if (is_array($value)) {
612 827
                continue;
613
            }
614 827
            $this->setPropertyInternal('env.' . $name, $value);
615
        }
616 827
    }
617
618
    /**
619
     * Adds a task definition.
620
     *
621
     * @param string $name      name of tag
622
     * @param string $class     the class path to use
623
     * @param string $classpath the classpat to use
624
     */
625 67
    public function addTaskDefinition($name, $class, $classpath = null)
626
    {
627 67
        ComponentHelper::getComponentHelper($this)->addTaskDefinition($name, $class, $classpath);
628 67
    }
629
630
    /**
631
     * Returns the task definitions.
632
     *
633
     * @return array
634
     */
635 46
    public function getTaskDefinitions()
636
    {
637 46
        return ComponentHelper::getComponentHelper($this)->getTaskDefinitions();
638
    }
639
640
    /**
641
     * Adds a data type definition.
642
     *
643
     * @param string $typeName  name of the type
644
     * @param string $typeClass the class to use
645
     * @param string $classpath the classpath to use
646
     */
647 40
    public function addDataTypeDefinition($typeName, $typeClass, $classpath = null)
648
    {
649 40
        ComponentHelper::getComponentHelper($this)->addDataTypeDefinition($typeName, $typeClass, $classpath);
650 40
    }
651
652
    /**
653
     * Returns the data type definitions.
654
     *
655
     * @return array
656
     */
657 828
    public function getDataTypeDefinitions()
658
    {
659 828
        return ComponentHelper::getComponentHelper($this)->getDataTypeDefinitions();
660
    }
661
662
    /**
663
     * Add a new target to the project.
664
     *
665
     * @param string $targetName
666
     * @param Target $target
667
     *
668
     * @throws BuildException
669
     */
670 827
    public function addTarget($targetName, $target)
671
    {
672 827
        if (isset($this->targets[$targetName])) {
673
            throw new BuildException("Duplicate target: {$targetName}");
674
        }
675 827
        $this->addOrReplaceTarget($targetName, $target);
676 827
    }
677
678
    /**
679
     * Adds or replaces a target in the project.
680
     *
681
     * @param string $targetName
682
     * @param Target $target
683
     */
684 827
    public function addOrReplaceTarget($targetName, &$target)
685
    {
686 827
        $this->log("  +Target: {$targetName}", Project::MSG_DEBUG);
687 827
        $target->setProject($this);
688 827
        $this->targets[$targetName] = $target;
689
690 827
        $ctx = $this->getReference(ProjectConfigurator::PARSING_CONTEXT_REFERENCE);
691 827
        $current = $ctx->getCurrentTargets();
692 827
        $current[$targetName] = $target;
693 827
    }
694
695
    /**
696
     * Returns the available targets.
697
     *
698
     * @return Target[]
699
     */
700 827
    public function getTargets()
701
    {
702 827
        return $this->targets;
703
    }
704
705
    /**
706
     * @return string[]
707
     */
708
    public function getExecutedTargetNames()
709
    {
710
        return $this->executedTargetNames;
711
    }
712
713
    /**
714
     * Create a new task instance and return reference to it.
715
     *
716
     * @param string $taskType Task name
717
     *
718
     * @throws BuildException
719
     *
720
     * @return Task A task object
721
     */
722 688
    public function createTask($taskType)
723
    {
724 688
        return ComponentHelper::getComponentHelper($this)->createTask($taskType);
725
    }
726
727
    /**
728
     * Creates a new condition and returns the reference to it.
729
     *
730
     * @param string $conditionType
731
     *
732
     * @throws BuildException
733
     *
734
     * @return Condition
735
     */
736 1
    public function createCondition($conditionType)
737
    {
738 1
        return ComponentHelper::getComponentHelper($this)->createCondition($conditionType);
739
    }
740
741
    /**
742
     * Create a datatype instance and return reference to it
743
     * See createTask() for explanation how this works.
744
     *
745
     * @param string $typeName Type name
746
     *
747
     * @throws BuildException
748
     *                        Exception
749
     *
750
     * @return object A datatype object
751
     */
752 77
    public function createDataType($typeName)
753
    {
754 77
        return ComponentHelper::getComponentHelper($this)->createDataType($typeName);
755
    }
756
757
    /**
758
     * Executes a list of targets.
759
     *
760
     * @param array $targetNames List of target names to execute
761
     *
762
     * @throws BuildException
763
     */
764
    public function executeTargets($targetNames)
765
    {
766
        $this->executedTargetNames = $targetNames;
767
768
        foreach ($targetNames as $tname) {
769
            $this->executeTarget($tname);
770
        }
771
    }
772
773
    /**
774
     * Executes a target.
775
     *
776
     * @param string $targetName Name of Target to execute
777
     *
778
     * @throws BuildException
779
     */
780 638
    public function executeTarget($targetName)
781
    {
782
        // complain about executing void
783 638
        if (null === $targetName) {
0 ignored issues
show
introduced by
The condition null === $targetName is always false.
Loading history...
784
            throw new BuildException('No target specified');
785
        }
786
787
        // invoke topological sort of the target tree and run all targets
788
        // until targetName occurs.
789 638
        $sortedTargets = $this->topoSort($targetName);
790
791 638
        $curIndex = (int) 0;
792 638
        $curTarget = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $curTarget is dead and can be removed.
Loading history...
793 638
        $thrownException = null;
794 638
        $buildException = null;
795
        do {
796
            try {
797 638
                $curTarget = $sortedTargets[$curIndex++];
798 638
                $curTarget->performTasks();
799 162
            } catch (BuildException $exc) {
800 162
                if (!($this->keepGoingMode)) {
801 162
                    throw $exc;
802
                }
803
                $thrownException = $exc;
804
            }
805 527
            if (null != $thrownException) {
806
                if ($thrownException instanceof BuildException) {
807
                    $this->log(
808
                        "Target '" . $curTarget->getName()
809
                        . "' failed with message '"
810
                        . $thrownException->getMessage() . "'.",
811
                        Project::MSG_ERR
812
                    );
813
                    // only the first build exception is reported
814
                    if (null === $buildException) {
815
                        $buildException = $thrownException;
816
                    }
817
                } else {
818
                    $this->log(
819
                        "Target '" . $curTarget->getName()
820
                        . "' failed with message '"
821
                        . $thrownException->getMessage() . "'." . PHP_EOL
822
                        . $thrownException->getTraceAsString(),
823
                        Project::MSG_ERR
824
                    );
825
                    if (null === $buildException) {
826
                        $buildException = new BuildException($thrownException);
827
                    }
828
                }
829
            }
830 527
        } while ($curTarget->getName() !== $targetName);
831
832 527
        if (null !== $buildException) {
833
            throw $buildException;
834
        }
835 527
    }
836
837
    /**
838
     * Helper function.
839
     *
840
     * @param File $rootDir
841
     *
842
     * @throws IOException
843
     */
844 449
    public function resolveFile(string $fileName, File $rootDir = null): File
845
    {
846 449
        if (null === $rootDir) {
847 443
            return $this->fileUtils->resolveFile($this->basedir, $fileName);
848
        }
849
850 88
        return $this->fileUtils->resolveFile($rootDir, $fileName);
851
    }
852
853
    /**
854
     * Return the bool equivalent of a string, which is considered
855
     * <code>true</code> if either <code>"on"</code>, <code>"true"</code>,
856
     * or <code>"yes"</code> is found, ignoring case.
857
     *
858
     * @param string $s the string to convert to a bool value
859
     *
860
     * @return <code>true</code> if the given string is <code>"on"</code>,
0 ignored issues
show
Documentation Bug introduced by
The doc comment <code>true</code> at position 0 could not be parsed: Unknown type name '<' at position 0 in <code>true</code>.
Loading history...
861
     *                           <code>"true"</code> or <code>"yes"</code>, or
862
     *                           <code>false</code> otherwise
863
     */
864 7
    public static function toBoolean($s)
865
    {
866
        return
867 7
            0 === strcasecmp($s, 'on')
868 7
            || 0 === strcasecmp($s, 'true')
869 5
            || 0 === strcasecmp($s, 'yes')
870
            // FIXME next condition should be removed if the bool behavior for properties will be solved
871 7
            || 0 === strcasecmp($s, 1)
872
        ;
873
    }
874
875
    /**
876
     * Topologically sort a set of Targets.
877
     *
878
     * @param string $rootTarget is the (String) name of the root Target. The sort is
879
     *                           created in such a way that the sequence of Targets until the root
880
     *                           target is the minimum possible such sequence.
881
     *
882
     * @throws Exception
883
     * @throws BuildException
884
     *
885
     * @return Target[] targets in sorted order
886
     */
887 638
    public function topoSort($rootTarget)
888
    {
889 638
        $rootTarget = (string) $rootTarget;
890 638
        $ret = [];
891 638
        $state = [];
892 638
        $visiting = [];
893
894
        // We first run a DFS based sort using the root as the starting node.
895
        // This creates the minimum sequence of Targets to the root node.
896
        // We then do a sort on any remaining unVISITED targets.
897
        // This is unnecessary for doing our build, but it catches
898
        // circular dependencies or missing Targets on the entire
899
        // dependency tree, not just on the Targets that depend on the
900
        // build Target.
901
902 638
        $this->tsort($rootTarget, $state, $visiting, $ret);
903
904 638
        $retHuman = '';
905 638
        for ($i = 0, $_i = count($ret); $i < $_i; ++$i) {
906 638
            $retHuman .= (string) $ret[$i] . ' ';
907
        }
908 638
        $this->log("Build sequence for target '{$rootTarget}' is: {$retHuman}", Project::MSG_VERBOSE);
909
910 638
        $keys = array_keys($this->targets);
911 638
        while ($keys) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $keys of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
912 638
            $curTargetName = (string) array_shift($keys);
913 638
            if (!isset($state[$curTargetName])) {
914 638
                $st = null;
915
            } else {
916 638
                $st = (string) $state[$curTargetName];
917
            }
918
919 638
            if (null === $st) {
920 638
                $this->tsort($curTargetName, $state, $visiting, $ret);
921 638
            } elseif ('VISITING' === $st) {
922
                throw new Exception("Unexpected node in visiting state: {$curTargetName}");
923
            }
924
        }
925
926 638
        $retHuman = '';
927 638
        for ($i = 0, $_i = count($ret); $i < $_i; ++$i) {
928 638
            $retHuman .= (string) $ret[$i] . ' ';
929
        }
930 638
        $this->log("Complete build sequence is: {$retHuman}", Project::MSG_VERBOSE);
931
932 638
        return $ret;
933
    }
934
935
    /**
936
     * Adds a reference to an object. This method is called when the parser
937
     * detects a id="foo" attribute. It passes the id as $name and a reference
938
     * to the object assigned to this id as $value.
939
     *
940
     * @param string $name
941
     * @param object $object
942
     */
943 840
    public function addReference($name, $object)
944
    {
945 840
        $ref = $this->references[$name] ?? null;
946 840
        if ($ref === $object) {
947
            return;
948
        }
949 840
        if (null !== $ref && !$ref instanceof UnknownElement) {
950 36
            $this->log("Overriding previous definition of reference to {$name}", Project::MSG_VERBOSE);
951
        }
952 840
        $refName = (is_scalar($object) || $object instanceof PropertyValue) ? (string) $object : get_class($object);
953 840
        $this->log("Adding reference: {$name} -> " . $refName, Project::MSG_DEBUG);
954 840
        $this->references[$name] = $object;
955 840
    }
956
957
    /**
958
     * Returns the references array.
959
     *
960
     * @return array
961
     */
962 31
    public function getReferences()
963
    {
964 31
        return $this->references;
965
    }
966
967
    /**
968
     * Returns a specific reference.
969
     *
970
     * @param string $key the reference id/key
971
     *
972
     * @return object Reference or null if not defined
973
     */
974 841
    public function getReference($key)
975
    {
976 841
        return $this->references[$key] ?? null; // just to be explicit
977
    }
978
979
    /**
980
     * Does the project know this reference?
981
     *
982
     * @param string $key the reference id/key
983
     */
984 5
    public function hasReference(string $key): bool
985
    {
986 5
        return isset($this->references[$key]);
987
    }
988
989
    /**
990
     * Abstracting and simplifyling Logger calls for project messages.
991
     *
992
     * @param string $msg
993
     * @param int    $level
994
     */
995 840
    public function log($msg, $level = Project::MSG_INFO)
996
    {
997 840
        $this->logObject($this, $msg, $level);
998 840
    }
999
1000
    /**
1001
     * @param string $msg
1002
     * @param int    $level
1003
     * @param mixed  $obj
1004
     */
1005 843
    public function logObject($obj, $msg, $level, Exception $t = null)
1006
    {
1007 843
        $this->fireMessageLogged($obj, $msg, $level, $t);
1008
1009
        // Checking whether the strict-mode is On, then consider all the warnings
1010
        // as errors.
1011 843
        if (($this->strictMode) && (Project::MSG_WARN == $level)) {
1012
            throw new BuildException('Build contains warnings, considered as errors in strict mode', null);
1013
        }
1014 843
    }
1015
1016 827
    public function addBuildListener(BuildListener $listener)
1017
    {
1018 827
        $this->listeners[] = $listener;
1019 827
    }
1020
1021 9
    public function removeBuildListener(BuildListener $listener)
1022
    {
1023 9
        $newarray = [];
1024 9
        for ($i = 0, $size = count($this->listeners); $i < $size; ++$i) {
1025 9
            if ($this->listeners[$i] !== $listener) {
1026 9
                $newarray[] = $this->listeners[$i];
1027
            }
1028
        }
1029 9
        $this->listeners = $newarray;
1030 9
    }
1031
1032
    /**
1033
     * @return array
1034
     */
1035 33
    public function getBuildListeners()
1036
    {
1037 33
        return $this->listeners;
1038
    }
1039
1040
    public function fireBuildStarted()
1041
    {
1042
        $event = new BuildEvent($this);
1043
        foreach ($this->listeners as $listener) {
1044
            $listener->buildStarted($event);
1045
        }
1046
1047
        $this->log((string) $event, Project::MSG_DEBUG);
1048
    }
1049
1050
    /**
1051
     * @param Exception $exception
1052
     */
1053
    public function fireBuildFinished($exception)
1054
    {
1055
        $event = new BuildEvent($this);
1056
        $event->setException($exception);
1057
        foreach ($this->listeners as $listener) {
1058
            $listener->buildFinished($event);
1059
        }
1060
1061
        $this->log((string) $event, Project::MSG_DEBUG);
1062
    }
1063
1064
    /**
1065
     * @param $target
1066
     */
1067 638
    public function fireTargetStarted($target)
1068
    {
1069 638
        $event = new BuildEvent($target);
1070 638
        foreach ($this->listeners as $listener) {
1071 638
            $listener->targetStarted($event);
1072
        }
1073
1074 638
        $this->log((string) $event, Project::MSG_DEBUG);
1075 638
    }
1076
1077
    /**
1078
     * @param $target
1079
     * @param $exception
1080
     */
1081 638
    public function fireTargetFinished($target, $exception)
1082
    {
1083 638
        $event = new BuildEvent($target);
1084 638
        $event->setException($exception);
1085 638
        foreach ($this->listeners as $listener) {
1086 638
            $listener->targetFinished($event);
1087
        }
1088
1089 638
        $this->log((string) $event, Project::MSG_DEBUG);
1090 638
    }
1091
1092
    /**
1093
     * @param $task
1094
     */
1095 691
    public function fireTaskStarted($task)
1096
    {
1097 691
        $event = new BuildEvent($task);
1098 691
        foreach ($this->listeners as $listener) {
1099 691
            $listener->taskStarted($event);
1100
        }
1101
1102 691
        $this->log((string) $event, Project::MSG_DEBUG);
1103 691
    }
1104
1105
    /**
1106
     * @param $task
1107
     * @param $exception
1108
     */
1109 691
    public function fireTaskFinished($task, $exception)
1110
    {
1111 691
        $event = new BuildEvent($task);
1112 691
        $event->setException($exception);
1113 691
        foreach ($this->listeners as $listener) {
1114 691
            $listener->taskFinished($event);
1115
        }
1116
1117 691
        $this->log((string) $event, Project::MSG_DEBUG);
1118 691
    }
1119
1120
    /**
1121
     * @param $event
1122
     * @param $message
1123
     * @param $priority
1124
     */
1125 843
    public function fireMessageLoggedEvent(BuildEvent $event, $message, $priority)
1126
    {
1127 843
        $event->setMessage($message, $priority);
1128 843
        foreach ($this->listeners as $listener) {
1129 827
            $listener->messageLogged($event);
1130
        }
1131 843
    }
1132
1133
    /**
1134
     * @param string    $message
1135
     * @param int       $priority
1136
     * @param Exception $t
1137
     * @param mixed     $object
1138
     *
1139
     * @throws Exception
1140
     */
1141 843
    public function fireMessageLogged($object, $message, $priority, Exception $t = null)
1142
    {
1143 843
        $event = new BuildEvent($object);
1144 843
        if (null !== $t) {
1145
            $event->setException($t);
1146
        }
1147 843
        $this->fireMessageLoggedEvent($event, $message, $priority);
1148 843
    }
1149
1150
    /**
1151
     * Sets a property unless it is already defined as a user property
1152
     * (in which case the method returns silently).
1153
     *
1154
     * @param string $name  The name of the property.
1155
     *                      Must not be
1156
     *                      <code>null</code>.
1157
     * @param string $value The property value. Must not be <code>null</code>.
1158
     */
1159 838
    private function setPropertyInternal($name, $value)
1160
    {
1161 838
        PropertyHelper::getPropertyHelper($this)->setProperty(null, $name, $value, false);
1162 838
    }
1163
1164
    // one step in a recursive DFS traversal of the target dependency tree.
1165
    // - The array "state" contains the state (VISITED or VISITING or null)
1166
    //   of all the target names.
1167
    // - The stack "visiting" contains a stack of target names that are
1168
    //   currently on the DFS stack. (NB: the target names in "visiting" are
1169
    //    exactly the target names in "state" that are in the VISITING state.)
1170
    // 1. Set the current target to the VISITING state, and push it onto
1171
    //    the "visiting" stack.
1172
    // 2. Throw a BuildException if any child of the current node is
1173
    //    in the VISITING state (implies there is a cycle.) It uses the
1174
    //    "visiting" Stack to construct the cycle.
1175
    // 3. If any children have not been VISITED, tsort() the child.
1176
    // 4. Add the current target to the Vector "ret" after the children
1177
    //    have been visited. Move the current target to the VISITED state.
1178
    //    "ret" now contains the sorted sequence of Targets up to the current
1179
    //    Target.
1180
1181
    /**
1182
     * @param $root
1183
     * @param $state
1184
     * @param $visiting
1185
     * @param $ret
1186
     *
1187
     * @throws BuildException
1188
     * @throws Exception
1189
     */
1190 638
    private function tsort($root, &$state, &$visiting, &$ret)
1191
    {
1192 638
        $state[$root] = 'VISITING';
1193 638
        $visiting[] = $root;
1194
1195 638
        if (!isset($this->targets[$root]) || !($this->targets[$root] instanceof Target)) {
1196 3
            $target = null;
1197
        } else {
1198 638
            $target = $this->targets[$root];
1199
        }
1200
1201
        // make sure we exist
1202 638
        if (null === $target) {
1203 3
            $sb = "Target '{$root}' does not exist in this project.";
1204 3
            array_pop($visiting);
1205 3
            if (!empty($visiting)) {
1206
                $parent = (string) $visiting[count($visiting) - 1];
1207
                $sb .= " It is a dependency of target '{$parent}'.";
1208
            }
1209 3
            if ($suggestion = $this->findSuggestion($root)) {
1210 2
                $sb .= sprintf(" Did you mean '%s'?", $suggestion);
1211
            }
1212
1213 3
            throw new BuildException($sb);
1214
        }
1215
1216 638
        $deps = $target->getDependencies();
1217
1218 638
        while ($deps) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $deps of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1219 142
            $cur = (string) array_shift($deps);
1220 142
            if (!isset($state[$cur])) {
1221 78
                $m = null;
1222
            } else {
1223 124
                $m = (string) $state[$cur];
1224
            }
1225 142
            if (null === $m) {
1226
                // not been visited
1227 78
                $this->tsort($cur, $state, $visiting, $ret);
1228 124
            } elseif ('VISITING' == $m) {
1229
                // currently visiting this node, so have a cycle
1230
                throw $this->makeCircularException($cur, $visiting);
1231
            }
1232
        }
1233
1234 638
        $p = (string) array_pop($visiting);
1235 638
        if ($root !== $p) {
1236
            throw new Exception("Unexpected internal error: expected to pop {$root} but got {$p}");
1237
        }
1238
1239 638
        $state[$root] = 'VISITED';
1240 638
        $ret[] = $target;
1241 638
    }
1242
1243
    /**
1244
     * @param string $end
1245
     * @param array  $stk
1246
     *
1247
     * @return BuildException
1248
     */
1249
    private function makeCircularException($end, $stk)
1250
    {
1251
        $sb = "Circular dependency: {$end}";
1252
        do {
1253
            $c = (string) array_pop($stk);
1254
            $sb .= ' <- ' . $c;
1255
        } while ($c != $end);
1256
1257
        return new BuildException($sb);
1258
    }
1259
1260
    /**
1261
     * Finds the Target with the most similar name to function's argument.
1262
     *
1263
     * Will return null if buildfile has no targets.
1264
     *
1265
     * @see https://www.php.net/manual/en/function.levenshtein.php
1266
     *
1267
     * @param string $unknownTarget Target name
1268
     *
1269
     * @return Target
1270
     */
1271 3
    private function findSuggestion(string $unknownTarget): ?Target
1272
    {
1273 3
        return array_reduce($this->targets, function (?Target $carry, Target $current) use ($unknownTarget): ?Target {
1274
            // Omit target with empty name (there's always one)
1275 3
            if (empty(strval($current))) {
1276 3
                return $carry;
1277
            }
1278
            // $carry is null the first time
1279 2
            if (is_null($carry)) {
1280 2
                return $current;
1281
            }
1282
1283 2
            return levenshtein($unknownTarget, $carry) < levenshtein($unknownTarget, $current) ? $carry : $current;
1284 3
        });
1285
    }
1286
}
1287