Phing::printUsage()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 34
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 31
c 1
b 0
f 0
dl 0
loc 34
ccs 4
cts 4
cp 1
rs 9.424
cc 1
nc 1
nop 0
crap 1
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\Exception\ConfigurationException;
26
use Phing\Exception\ExitStatusException;
27
use Phing\Input\ConsoleInputHandler;
28
use Phing\Io\File;
29
use Phing\Io\FileOutputStream;
30
use Phing\Io\FileParserFactory;
31
use Phing\Io\FileReader;
32
use Phing\Io\FileSystem;
33
use Phing\Io\FileUtils;
34
use Phing\Io\IOException;
35
use Phing\Io\OutputStream;
36
use Phing\Io\PrintStream;
37
use Phing\Listener\BuildLogger;
38
use Phing\Listener\DefaultLogger;
39
use Phing\Listener\SilentLogger;
40
use Phing\Listener\StreamRequiredBuildLogger;
41
use Phing\Parser\ProjectConfigurator;
42
use Phing\Util\DefaultClock;
43
use Phing\Util\Diagnostics;
44
use Phing\Util\Properties;
45
use Phing\Util\SizeHelper;
46
use Phing\Util\StringHelper;
47
use SebastianBergmann\Version;
48
use Symfony\Component\Console\Output\ConsoleOutput;
49
use Throwable;
50
51
use function array_filter;
52
use function array_map;
53
use function array_reduce;
54
use function gmdate;
55
use function implode;
56
use function method_exists;
57
use function sprintf;
58
use function strlen;
59
use function strval;
60
use function trim;
61
62
use const DATE_RFC7231;
63
use const PHP_EOL;
64
65
/**
66
 * Entry point into Phing.  This class handles the full lifecycle of a build -- from
67
 * parsing & handling commandline arguments to assembling the project to shutting down
68
 * and cleaning up in the end.
69
 *
70
 * If you are invoking Phing from an external application, this is still
71
 * the class to use.  Your application can invoke the start() method, passing
72
 * any commandline arguments or additional properties.
73
 *
74
 * @author Andreas Aderhold <[email protected]>
75
 * @author Hans Lellelid <[email protected]>
76
 */
77
class Phing
78
{
79
    /**
80
     * Alias for phar file.
81
     */
82
    public const PHAR_ALIAS = 'phing.phar';
83
84
    /**
85
     * The default build file name.
86
     */
87
    public const DEFAULT_BUILD_FILENAME = 'build.xml';
88
    public const DEFAULT_BUILD_CONTENT = <<<'XML'
89
        <?xml version="1.0" encoding="UTF-8" ?>
90
91
        <project name="" description="" default="">
92
93
            <target name="" description="">
94
95
            </target>
96
97
        </project>
98
        XML;
99
    public const PHING_HOME = 'phing.home';
100
    public const PHP_VERSION = 'php.version';
101
    public const PHP_INTERPRETER = 'php.interpreter';
102
103
    /**
104
     * Our current message output status. Follows Project::MSG_XXX.
105
     */
106
    private static $msgOutputLevel = Project::MSG_INFO;
107
    /**
108
     * Set of properties that are passed in from commandline or invoking code.
109
     *
110
     * @var Properties
111
     */
112
    private static $definedProps;
113
    /**
114
     * Used by utility function getResourcePath().
115
     */
116
    private static $importPaths;
117
    /**
118
     * Cache of the Phing version information when it has been loaded.
119
     */
120
    private static $phingVersion;
121
    /**
122
     * Cache of the short Phing version information when it has been loaded.
123
     */
124
    private static $phingShortVersion;
125
    /**
126
     * System-wide static properties (moved from System).
127
     */
128
    private static $properties = [];
129
    /**
130
     * Static system timer.
131
     */
132
    private static $timer;
133
    /**
134
     * The current Project.
135
     */
136
    private static $currentProject;
137
    /**
138
     * Whether to capture PHP errors to buffer.
139
     */
140
    private static $phpErrorCapture = false;
141
    /**
142
     * Array of captured PHP errors.
143
     */
144
    private static $capturedPhpErrors = [];
145
    /**
146
     * @var OUtputStream stream for standard output
147
     */
148
    private static $out;
149
    /**
150
     * @var OutputStream stream for error output
151
     */
152
    private static $err;
153
    /**
154
     * @var bool whether we are using a logfile
155
     */
156
    private static $isLogFileUsed = false;
157
    /**
158
     * Array to hold original ini settings that Phing changes (and needs
159
     * to restore in restoreIni() method).
160
     *
161
     * @var array Struct of array(setting-name => setting-value)
162
     *
163
     * @see restoreIni()
164
     */
165
    private static $origIniSettings = [];
166
    /**
167
     * PhingFile that we are using for configuration.
168
     */
169
    private $buildFile;
170
    /**
171
     * The build targets.
172
     */
173
    private $targets = [];
174
    /**
175
     * Names of classes to add as listeners to project.
176
     */
177
    private $listeners = [];
178
    /**
179
     * keep going mode.
180
     *
181
     * @var bool
182
     */
183
    private $keepGoingMode = false;
184
    private $loggerClassname;
185
    /**
186
     * The class to handle input (can be only one).
187
     */
188
    private $inputHandlerClassname;
189
    /**
190
     * Whether or not log output should be reduced to the minimum.
191
     *
192
     * @var bool
193
     */
194
    private $silent = false;
195
    /**
196
     * Indicates whether phing should run in strict mode.
197
     */
198
    private $strictMode = false;
199
    /**
200
     * Indicates if this phing should be run.
201
     */
202
    private $readyToRun = false;
203
    /**
204
     * Indicates we should only parse and display the project help information.
205
     */
206
    private $projectHelp = false;
207
    /**
208
     * Whether to values in a property file should override existing values.
209
     */
210
    private $propertyFileOverride = false;
211
    /**
212
     * Whether or not output to the log is to be unadorned.
213
     */
214
    private $emacsMode = false;
215
216
    /**
217
     * What to search for, passed as arguments.
218
     *
219
     * @var ?string
220
     */
221
    private $searchForThis;
222
    private $propertyFiles = [];
223
224
    /**
225
     * Gets the stream to use for standard (non-error) output.
226
     *
227
     * @return OutputStream
228
     */
229
    public static function getOutputStream()
230
    {
231
        return self::$out;
232
    }
233
234
    /**
235
     * Gets the stream to use for error output.
236
     *
237
     * @return OutputStream
238
     */
239
    public static function getErrorStream()
240
    {
241
        return self::$err;
242
    }
243
244
    /**
245
     * Command line entry point. This method kicks off the building
246
     * of a project object and executes a build using either a given
247
     * target or the default target.
248
     *
249
     * @param array $args command line args
250
     *
251
     * @throws Exception
252
     */
253
    public static function fire($args): void
254
    {
255
        self::start($args);
256
    }
257
258
    /**
259
     * Entry point allowing for more options from other front ends.
260
     *
261
     * This method encapsulates the complete build lifecycle.
262
     *
263
     * @param array      $args                     the commandline args passed to phing shell script
264
     * @param array|null $additionalUserProperties Any additional properties to be passed to Phing (alternative front-end might implement this).
265
     *                                             These additional properties will be available using the getDefinedProperty() method and will
266
     *                                             be added to the project's "user" properties
267
     *
268
     * @throws Exception - if there is an error during build
269
     *
270
     * @see    runBuild()
271
     * @see    execute()
272
     */
273
    public static function start($args, ?array $additionalUserProperties = null)
274
    {
275
        try {
276
            $m = new self();
277
            $m->execute($args);
278
        } catch (Exception $exc) {
279
            self::handleLogfile();
280
            self::printMessage($exc);
281
            self::statusExit(1);
282
283
            return;
284
        }
285
286
        if (null !== $additionalUserProperties) {
287
            foreach ($additionalUserProperties as $key => $value) {
288
                $m::setDefinedProperty($key, $value);
289
            }
290
        }
291
292
        // expect the worst
293
        $exitCode = 1;
294
295
        try {
296
            try {
297
                $m->runBuild();
298
                $exitCode = 0;
299
            } catch (ExitStatusException $ese) {
300
                $exitCode = $ese->getCode();
301
                if (0 !== $exitCode) {
302
                    self::handleLogfile();
303
304
                    throw $ese;
305
                }
306
            }
307
        } catch (BuildException $exc) {
308
            // avoid printing output twice: self::printMessage($exc);
309
        } catch (Throwable $exc) {
310
            self::printMessage($exc);
311
        } finally {
312
            self::handleLogfile();
313
        }
314
        self::statusExit($exitCode);
315
    }
316
317
    /**
318
     * Setup/initialize Phing environment from commandline args.
319
     *
320
     * @param array $args commandline args passed to phing shell
321
     *
322
     * @throws ConfigurationException
323
     */
324
    public function execute($args): void
325
    {
326
        self::$definedProps = new Properties();
327
        $this->searchForThis = null;
328
329
        // 1) First handle any options which should always
330
        // Note: The order in which these are executed is important (if multiple of these options are specified)
331
332
        if (in_array('-help', $args) || in_array('-h', $args)) {
333
            static::printUsage();
334
335
            return;
336
        }
337
338
        if (in_array('-version', $args) || in_array('-v', $args)) {
339
            static::printVersion();
340
341
            return;
342
        }
343
344
        if (in_array('-init', $args) || in_array('-i', $args)) {
345
            $key = array_search('-init', $args) ?: array_search('-i', $args);
346
            $path = $args[$key + 1] ?? null;
347
348
            self::init($path);
349
350
            return;
351
        }
352
353
        if (in_array('-diagnostics', $args)) {
354
            Diagnostics::doReport(new PrintStream(self::$out));
355
356
            return;
357
        }
358
359
        // 2) Next pull out stand-alone args.
360
        // Note: The order in which these are executed is important (if multiple of these options are specified)
361
362
        if (
363
            false !== ($key = array_search('-quiet', $args, true))
364
            || false !== ($key = array_search(
365
                '-q',
366
                $args,
367
                true
368
            ))
369
        ) {
370
            self::$msgOutputLevel = Project::MSG_WARN;
371
            unset($args[$key]);
372
        }
373
374
        if (
375
            false !== ($key = array_search('-emacs', $args, true))
376
            || false !== ($key = array_search('-e', $args, true))
377
        ) {
378
            $this->emacsMode = true;
379
            unset($args[$key]);
380
        }
381
382
        if (false !== ($key = array_search('-verbose', $args, true))) {
383
            self::$msgOutputLevel = Project::MSG_VERBOSE;
384
            unset($args[$key]);
385
        }
386
387
        if (false !== ($key = array_search('-debug', $args, true))) {
388
            self::$msgOutputLevel = Project::MSG_DEBUG;
389
            unset($args[$key]);
390
        }
391
392
        if (
393
            false !== ($key = array_search('-silent', $args, true))
394
            || false !== ($key = array_search('-S', $args, true))
395
        ) {
396
            $this->silent = true;
397
            unset($args[$key]);
398
        }
399
400
        if (false !== ($key = array_search('-propertyfileoverride', $args, true))) {
401
            $this->propertyFileOverride = true;
402
            unset($args[$key]);
403
        }
404
405
        // 3) Finally, cycle through to parse remaining args
406
        //
407
        $keys = array_keys($args); // Use keys and iterate to max(keys) since there may be some gaps
408
        $max = $keys ? max($keys) : -1;
409
        for ($i = 0; $i <= $max; ++$i) {
410
            if (!array_key_exists($i, $args)) {
411
                // skip this argument, since it must have been removed above.
412
                continue;
413
            }
414
415
            $arg = $args[$i];
416
417
            if ('-logfile' == $arg) {
418
                try {
419
                    // see: http://phing.info/trac/ticket/65
420
                    if (!isset($args[$i + 1])) {
421
                        $msg = "You must specify a log file when using the -logfile argument\n";
422
423
                        throw new ConfigurationException($msg);
424
                    }
425
426
                    $logFile = new File($args[++$i]);
427
                    $out = new FileOutputStream($logFile); // overwrite
428
                    self::setOutputStream($out);
429
                    self::setErrorStream($out);
430
                    self::$isLogFileUsed = true;
431
                } catch (IOException $ioe) {
432
                    $msg = 'Cannot write on the specified log file. Make sure the path exists and you have write permissions.';
433
434
                    throw new ConfigurationException($msg, $ioe);
435
                }
436
            } elseif ('-buildfile' == $arg || '-file' == $arg || '-f' == $arg) {
437
                if (!isset($args[$i + 1])) {
438
                    $msg = 'You must specify a buildfile when using the -buildfile argument.';
439
440
                    throw new ConfigurationException($msg);
441
                }
442
443
                $this->buildFile = new File($args[++$i]);
444
            } elseif ('-listener' == $arg) {
445
                if (!isset($args[$i + 1])) {
446
                    $msg = 'You must specify a listener class when using the -listener argument';
447
448
                    throw new ConfigurationException($msg);
449
                }
450
451
                $this->listeners[] = $args[++$i];
452
            } elseif (StringHelper::startsWith('-D', $arg)) {
453
                // Evaluating the property information //
454
                // Checking whether arg. is not just a switch, and next arg. does not starts with switch identifier
455
                if (('-D' == $arg) && (!StringHelper::startsWith('-', $args[$i + 1]))) {
456
                    $name = $args[++$i];
457
                } else {
458
                    $name = substr($arg, 2);
459
                }
460
461
                $value = null;
462
                $posEq = strpos($name, '=');
463
                if (false !== $posEq) {
464
                    $value = substr($name, $posEq + 1);
465
                    $name = substr($name, 0, $posEq);
466
                } elseif ($i < count($args) - 1 && !StringHelper::startsWith('-D', $args[$i + 1])) {
467
                    $value = $args[++$i];
468
                }
469
                self::$definedProps->setProperty($name, $value);
470
            } elseif ('-logger' == $arg) {
471
                if (!isset($args[$i + 1])) {
472
                    $msg = 'You must specify a classname when using the -logger argument';
473
474
                    throw new ConfigurationException($msg);
475
                }
476
477
                $this->loggerClassname = $args[++$i];
478
            } elseif ('-no-strict' == $arg) {
479
                $this->strictMode = false;
480
            } elseif ('-strict' == $arg) {
481
                $this->strictMode = true;
482
            } elseif ('-inputhandler' == $arg) {
483
                if (null !== $this->inputHandlerClassname) {
484
                    throw new ConfigurationException('Only one input handler class may be specified.');
485
                }
486
                if (!isset($args[$i + 1])) {
487
                    $msg = 'You must specify a classname when using the -inputhandler argument';
488
489
                    throw new ConfigurationException($msg);
490
                }
491
492
                $this->inputHandlerClassname = $args[++$i];
493
            } elseif ('-propertyfile' === $arg) {
494
                $i = $this->handleArgPropertyFile($args, $i);
495
            } elseif ('-keep-going' === $arg || '-k' === $arg) {
496
                $this->keepGoingMode = true;
497
            } elseif ('-longtargets' == $arg) {
498
                self::$definedProps->setProperty('phing.showlongtargets', 1);
499
            } elseif ('-projecthelp' == $arg || '-targets' == $arg || '-list' == $arg || '-l' == $arg || '-p' == $arg) {
500
                // set the flag to display the targets and quit
501
                $this->projectHelp = true;
502
            } elseif ('-find' == $arg) {
503
                // eat up next arg if present, default to build.xml
504
                if ($i < count($args) - 1) {
505
                    $this->searchForThis = $args[++$i];
506
                } else {
507
                    $this->searchForThis = self::DEFAULT_BUILD_FILENAME;
508
                }
509
            } elseif ('-' == substr($arg, 0, 1)) {
510
                // we don't have any more args
511
                self::printUsage();
512
                self::$err->write(PHP_EOL);
513
514
                throw new ConfigurationException('Unknown argument: ' . $arg);
515
            } else {
516
                // if it's no other arg, it may be the target
517
                $this->targets[] = $arg;
518
            }
519
        }
520
521
        // if buildFile was not specified on the command line,
522
        if (null === $this->buildFile) {
523
            // but -find then search for it
524
            if (null !== $this->searchForThis) {
525
                $this->buildFile = $this->findBuildFile(self::getProperty('user.dir'), $this->searchForThis);
0 ignored issues
show
Bug introduced by
$this->searchForThis of type void is incompatible with the type string expected by parameter $suffix of Phing\Phing::findBuildFile(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

525
                $this->buildFile = $this->findBuildFile(self::getProperty('user.dir'), /** @scrutinizer ignore-type */ $this->searchForThis);
Loading history...
526
            } else {
527
                $this->buildFile = new File(self::DEFAULT_BUILD_FILENAME);
528
            }
529
        }
530
531
        try {
532
            // make sure buildfile (or buildfile.dist) exists
533
            if (!$this->buildFile->exists()) {
534
                $distFile = new File($this->buildFile->getAbsolutePath() . '.dist');
535
                if (!$distFile->exists()) {
536
                    throw new ConfigurationException(
537
                        'Buildfile: ' . $this->buildFile->__toString() . ' does not exist!'
538
                    );
539
                }
540
                $this->buildFile = $distFile;
541
            }
542
543
            // make sure it's not a directory
544
            if ($this->buildFile->isDirectory()) {
545
                throw new ConfigurationException('Buildfile: ' . $this->buildFile->__toString() . ' is a dir!');
546
            }
547
        } catch (IOException $e) {
548
            // something else happened, buildfile probably not readable
549
            throw new ConfigurationException('Buildfile: ' . $this->buildFile->__toString() . ' is not readable!');
550
        }
551
552
        $this->loadPropertyFiles();
553
554
        $this->readyToRun = true;
555
    }
556
557
    /**
558
     * Prints the usage of how to use this class.
559
     */
560 1
    public static function printUsage()
561
    {
562 1
        $msg = <<<'TEXT'
563
            phing [options] [target [target2 [target3] ...]]
564
            Options:
565
              -h -help                 print this message
566
              -l -list                 list available targets in this project
567
              -i -init [file]          generates an initial buildfile
568
              -v -version              print the version information and exit
569
              -q -quiet                be extra quiet
570
              -S -silent               print nothing but task outputs and build failures
571
                 -verbose              be extra verbose
572
                 -debug                print debugging information
573
              -e -emacs                produce logging information without adornments
574
                 -diagnostics          print diagnostics information
575
                 -strict               runs build in strict mode, considering a warning as error
576
                 -no-strict            runs build normally (overrides buildfile attribute)
577
                 -longtargets          show target descriptions during build
578
                 -logfile <file>       use given file for log
579
                 -logger <classname>   the class which is to perform logging
580
                 -listener <classname> add an instance of class as a project listener
581
              -f -buildfile <file>     use given buildfile
582
                 -D<property>=<value>  use value for given property
583
              -k -keep-going           execute all targets that do not depend on failed target(s)
584
                 -propertyfile <file>  load all properties from file
585
                 -propertyfileoverride values in property file override existing values
586
                 -find <file>          search for buildfile towards the root of the filesystem and use it
587
                 -inputhandler <file>  the class to use to handle user input
588
589
            Report bugs to https://github.com/phingofficial/phing/issues
590
591 1
            TEXT;
592
593 1
        self::$err->write($msg);
594
    }
595
596
    /**
597
     * Prints the current Phing version.
598
     */
599
    public static function printVersion()
600
    {
601
        self::$out->write(self::getPhingVersion() . PHP_EOL);
602
    }
603
604
    /**
605
     * Gets the current Phing version based on VERSION.TXT file. Once the information
606
     * has been loaded once, it's cached and returned from the cache on future
607
     * calls.
608
     *
609
     * @throws ConfigurationException
610
     */
611 6
    public static function getPhingVersion(): string
612
    {
613 6
        if (self::$phingVersion === null) {
614
            $versionPath = self::getResourcePath('phing/etc/VERSION.TXT');
615
            if (null === $versionPath) {
616
                $versionPath = self::getResourcePath('etc/VERSION.TXT');
617
            }
618
            if (null === $versionPath) {
619
                throw new ConfigurationException('No VERSION.TXT file found; try setting phing.home environment variable.');
620
            }
621
622
            try { // try to read file
623
                $file = new File($versionPath);
624
                $reader = new FileReader($file);
625
                $phingVersion = trim($reader->read());
626
            } catch (IOException $iox) {
627
                throw new ConfigurationException("Can't read version information file");
628
            }
629
630
            $basePath = dirname(__DIR__, 2);
631
632
            $version = new Version($phingVersion, $basePath);
633
            self::$phingShortVersion = (method_exists($version, 'asString') ? $version->asString() : $version->getVersion());
634
635
            self::$phingVersion = 'Phing ' . self::$phingShortVersion;
636
        }
637 6
        return self::$phingVersion;
638
    }
639
640
    /**
641
     * Returns the short Phing version information, if available. Once the information
642
     * has been loaded once, it's cached and returned from the cache on future
643
     * calls.
644
     *
645
     * @return the short Phing version information as a string
0 ignored issues
show
Bug introduced by
The type Phing\the was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
646
     *
647
     * @throws ConfigurationException
648
     */
649
    public static function getPhingShortVersion(): string
650
    {
651
        if (self::$phingShortVersion === null) {
652
            self::getPhingVersion();
653
        }
654
        return self::$phingShortVersion;
655
    }
656
657
    /**
658
     * Looks on include path for specified file.
659
     *
660
     * @param string $path
661
     *
662
     * @return null|string file found (null if no file found)
663
     */
664 907
    public static function getResourcePath($path): ?string
665
    {
666 907
        if (null === self::$importPaths) {
667
            self::$importPaths = self::explodeIncludePath();
668
        }
669
670 907
        $path = str_replace(['\\', '/'], DIRECTORY_SEPARATOR, $path);
671
672 907
        foreach (self::$importPaths as $prefix) {
673 907
            $testPath = $prefix . DIRECTORY_SEPARATOR . $path;
674 907
            if (file_exists($testPath)) {
675
                return $testPath;
676
            }
677
        }
678
679
        // Check for the property phing.home
680 907
        $homeDir = self::getProperty(self::PHING_HOME);
681 907
        if ($homeDir) {
682 907
            $testPath = $homeDir . DIRECTORY_SEPARATOR . $path;
683 907
            if (file_exists($testPath)) {
684 907
                return $testPath;
685
            }
686
        }
687
688
        // Check for the phing home of phar archive
689
        if (0 === strpos(self::$importPaths[0], 'phar://')) {
690
            $testPath = self::$importPaths[0] . '/../' . $path;
691
            if (file_exists($testPath)) {
692
                return $testPath;
693
            }
694
        }
695
696
        // Do one additional check based on path of current file (Phing.php)
697
        $maybeHomeDir = realpath(__DIR__ . DIRECTORY_SEPARATOR);
698
        $testPath = $maybeHomeDir . DIRECTORY_SEPARATOR . $path;
699
        if (file_exists($testPath)) {
700
            return $testPath;
701
        }
702
703
        return null;
704
    }
705
706
    /**
707
     * Explode an include path into an array.
708
     *
709
     * If no path provided, uses current include_path. Works around issues that
710
     * occur when the path includes stream schemas.
711
     *
712
     * Pulled from Zend_Loader::explodeIncludePath() in ZF1.
713
     *
714
     * @copyright Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
715
     * @license   http://framework.zend.com/license/new-bsd New BSD License
716
     *
717
     * @param null|string $path
718
     *
719
     * @return array
720
     */
721 17
    public static function explodeIncludePath($path = null)
722
    {
723 17
        if (null === $path) {
724 17
            $path = get_include_path();
725
        }
726
727 17
        if (PATH_SEPARATOR === ':') {
728
            // On *nix systems, include_paths which include paths with a stream
729
            // schema cannot be safely explode'd, so we have to be a bit more
730
            // intelligent in the approach.
731 17
            $paths = preg_split('#:(?!//)#', $path);
732
        } else {
733
            $paths = explode(PATH_SEPARATOR, $path);
734
        }
735
736 17
        return $paths;
737
    }
738
739
    /**
740
     * Returns property value for a System property.
741
     * System properties are "global" properties like application.startdir,
742
     * and user.dir.  Many of these correspond to similar properties in Java
743
     * or Ant.
744
     *
745
     * @param string $propName
746
     *
747
     * @return string value of found property (or null, if none found)
748
     */
749 925
    public static function getProperty($propName)
750
    {
751
        // some properties are detemined on each access
752
        // some are cached, see below
753
754
        // default is the cached value:
755 925
        $val = self::$properties[$propName] ?? null;
756
757
        // special exceptions
758
        switch ($propName) {
759 925
            case 'user.dir':
760 153
                $val = getcwd();
761
762 153
                break;
763
        }
764
765 925
        return $val;
766
    }
767
768
    /**
769
     * Creates generic buildfile.
770
     *
771
     * @param string $path
772
     */
773
    public static function init($path)
774
    {
775
        if ($buildfilePath = self::initPath($path)) {
776
            self::initWrite($buildfilePath);
777
        }
778
    }
779
780
    /**
781
     * Sets the stream to use for standard (non-error) output.
782
     *
783
     * @param OutputStream $stream the stream to use for standard output
784
     */
785 1
    public static function setOutputStream(OutputStream $stream)
786
    {
787 1
        self::$out = $stream;
788
    }
789
790
    /**
791
     * Sets the stream to use for error output.
792
     *
793
     * @param OutputStream $stream the stream to use for error output
794
     */
795 1
    public static function setErrorStream(OutputStream $stream): void
796
    {
797 1
        self::$err = $stream;
798
    }
799
800
    /**
801
     * Making output level a static property so that this property
802
     * can be accessed by other parts of the system, enabling
803
     * us to display more information -- e.g. backtraces -- for "debug" level.
804
     *
805
     * @return int
806
     */
807 9
    public static function getMsgOutputLevel()
808
    {
809 9
        return self::$msgOutputLevel;
810
    }
811
812
    /**
813
     * Prints the message of the Exception if it's not null.
814
     */
815
    public static function printMessage(Throwable $t): void
816
    {
817
        if (null === self::$err) { // Make sure our error output is initialized
818
            self::initializeOutputStreams();
819
        }
820
        if (self::getMsgOutputLevel() >= Project::MSG_VERBOSE) {
821
            self::$err->write((string) $t . PHP_EOL);
822
        } else {
823
            self::$err->write($t->getMessage() . PHP_EOL);
824
        }
825
    }
826
827
    /**
828
     * Performs any shutdown routines, such as stopping timers.
829
     *
830
     * @throws IOException
831
     */
832 1
    public static function shutdown(): void
833
    {
834 1
        FileSystem::getFileSystem()::deleteFilesOnExit();
835 1
        self::$msgOutputLevel = Project::MSG_INFO;
836 1
        self::restoreIni();
837 1
        self::getTimer()->stop();
838
    }
839
840
    /**
841
     * Returns reference to DefaultClock object.
842
     */
843 2
    public static function getTimer(): DefaultClock
844
    {
845 2
        if (null === self::$timer) {
846
            self::$timer = new DefaultClock();
847
        }
848
849 2
        return self::$timer;
850
    }
851
852
    /**
853
     * This sets a property that was set via command line or otherwise passed into Phing.
854
     *
855
     * @param string $name
856
     * @param mixed  $value
857
     *
858
     * @return mixed value of found property (or null, if none found)
859
     */
860
    public static function setDefinedProperty($name, $value)
861
    {
862
        return self::$definedProps->setProperty($name, $value);
863
    }
864
865
    /**
866
     * Executes the build.
867
     *
868
     * @throws IOException
869
     * @throws Throwable
870
     */
871
    public function runBuild(): void
872
    {
873
        if (!$this->readyToRun) {
874
            return;
875
        }
876
877
        $project = new Project();
878
879
        self::setCurrentProject($project);
880
        set_error_handler(['Phing\Phing', 'handlePhpError']);
881
882
        $error = null;
883
884
        try {
885
            $this->addBuildListeners($project);
886
            $this->addInputHandler($project);
887
888
            // set this right away, so that it can be used in logging.
889
            $project->setUserProperty('phing.file', $this->buildFile->getAbsolutePath());
890
            $project->setUserProperty('phing.dir', dirname($this->buildFile->getAbsolutePath()));
891
            $project->setUserProperty('phing.version', static::getPhingVersion());
892
            $project->fireBuildStarted();
893
            $project->init();
894
            $project->setKeepGoingMode($this->keepGoingMode);
895
896
            $e = self::$definedProps->keys();
897
            while (count($e)) {
898
                $arg = (string) array_shift($e);
899
                $value = (string) self::$definedProps->getProperty($arg);
900
                $project->setUserProperty($arg, $value);
901
            }
902
            unset($e);
903
904
            // first use the Configurator to create the project object
905
            // from the given build file.
906
907
            ProjectConfigurator::configureProject($project, $this->buildFile);
908
909
            // Set the project mode
910
            $project->setStrictMode(StringHelper::booleanValue($this->strictMode));
911
912
            // make sure that minimum required phing version is satisfied
913
            $this->comparePhingVersion($project->getPhingVersion());
914
915
            if ($this->projectHelp) {
916
                $this->printDescription($project);
917
                $this->printTargets($project);
918
919
                return;
920
            }
921
922
            // make sure that we have a target to execute
923
            if (0 === count($this->targets)) {
924
                $this->targets[] = $project->getDefaultTarget();
925
            }
926
927
            $project->executeTargets($this->targets);
928
        } catch (Throwable $t) {
929
            $error = $t;
930
931
            throw $t;
932
        } finally {
933
            if (!$this->projectHelp) {
934
                try {
935
                    $project->fireBuildFinished($error);
936
                } catch (Exception $e) {
937
                    self::$err->write('Caught an exception while logging the end of the build.  Exception was:' . PHP_EOL);
938
                    self::$err->write($e->getTraceAsString());
939
                    if (null !== $error) {
940
                        self::$err->write('There has been an error prior to that:' . PHP_EOL);
941
                        self::$err->write($error->getTraceAsString());
942
                    }
943
944
                    throw new BuildException($error);
945
                }
946
            } elseif (null !== $error) {
947
                $project->log($error->getMessage(), Project::MSG_ERR);
948
            }
949
950
            restore_error_handler();
951
            self::unsetCurrentProject();
952
        }
953
    }
954
955
    /**
956
     * Import a class, supporting the following conventions:
957
     * - PEAR style (@see http://pear.php.net/manual/en/standards.naming.php)
958
     * - PSR-0 (@see https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md).
959
     *
960
     * @param string $classname Name of class
961
     * @param mixed  $classpath String or object supporting __toString()
962
     *
963
     * @throws BuildException - if cannot find the specified file
964
     *
965
     * @return string the unqualified classname (which can be instantiated)
966
     */
967 910
    public static function import($classname, $classpath = null)
968
    {
969
        // first check to see that the class specified hasn't already been included.
970 910
        if (class_exists($classname)) {
971 908
            return $classname;
972
        }
973
974 7
        $filename = strtr($classname, ['_' => DIRECTORY_SEPARATOR, '\\' => DIRECTORY_SEPARATOR]) . '.php';
975
976 7
        Phing::importFile($filename, $classpath);
977
978 5
        return $classname;
979
    }
980
981
    /**
982
     * Import a PHP file.
983
     *
984
     * This used to be named __import, however PHP has reserved all method names
985
     * with a double underscore prefix for future use.
986
     *
987
     * @param string $path      Path to the PHP file
988
     * @param mixed  $classpath String or object supporting __toString()
989
     *
990
     * @throws ConfigurationException
991
     */
992 10
    public static function importFile($path, $classpath = null)
993
    {
994 10
        if ($classpath) {
995
            // Apparently casting to (string) no longer invokes __toString() automatically.
996 2
            if (is_object($classpath)) {
997
                $classpath = $classpath->__toString();
998
            }
999
1000
            // classpaths are currently additive, but we also don't want to just
1001
            // indiscriminantly prepand/append stuff to the include_path.  This means
1002
            // we need to parse current incldue_path, and prepend any
1003
            // specified classpath locations that are not already in the include_path.
1004
            //
1005
            // NOTE:  the reason why we do it this way instead of just changing include_path
1006
            // and then changing it back, is that in many cases applications (e.g. Propel) will
1007
            // include/require class files from within method calls.  This means that not all
1008
            // necessary files will be included in this import() call, and hence we can't
1009
            // change the include_path back without breaking those apps.  While this method could
1010
            // be more expensive than switching & switching back (not sure, but maybe), it makes it
1011
            // possible to write far less expensive run-time applications (e.g. using Propel), which is
1012
            // really where speed matters more.
1013
1014 2
            $curr_parts = Phing::explodeIncludePath();
1015 2
            $add_parts = Phing::explodeIncludePath($classpath);
1016 2
            $new_parts = array_diff($add_parts, $curr_parts);
1017 2
            if ($new_parts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $new_parts 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...
1018 1
                set_include_path(implode(PATH_SEPARATOR, array_merge($new_parts, $curr_parts)));
1019
            }
1020
        }
1021
1022 10
        $ret = include_once $path;
1023
1024 8
        if (false === $ret) {
1025
            $msg = "Error importing {$path}";
1026
            if (self::getMsgOutputLevel() >= Project::MSG_DEBUG) {
1027
                $x = new Exception('for-path-trace-only');
1028
                $msg .= $x->getTraceAsString();
1029
            }
1030
1031
            throw new ConfigurationException($msg);
1032
        }
1033
    }
1034
1035
    /**
1036
     * Print the project description, if any.
1037
     *
1038
     * @throws IOException
1039
     */
1040
    public function printDescription(Project $project): void
1041
    {
1042
        if (null !== $project->getDescription()) {
1043
            $project->log($project->getDescription());
1044
        }
1045
    }
1046
1047
    /**
1048
     * Print out a list of all targets in the current buildfile.
1049
     */
1050 1
    public function printTargets(Project $project)
1051
    {
1052 1
        $visibleTargets = array_filter($project->getTargets(), function (Target $target) {
1053 1
            return !$target->isHidden() && !empty($target->getName());
1054 1
        });
1055 1
        $padding = array_reduce($visibleTargets, function (int $carry, Target $target) {
1056
            return max(strlen($target->getName()), $carry);
1057 1
        }, 0);
1058 1
        $categories = [
1059 1
            'Default target:' => array_filter($visibleTargets, function (Target $target) use ($project) {
1060
                return trim(strval($target)) === $project->getDefaultTarget();
1061 1
            }),
1062 1
            'Main targets:' => array_filter($visibleTargets, function (Target $target) {
1063
                return !empty($target->getDescription());
1064 1
            }),
1065 1
            'Subtargets:' => array_filter($visibleTargets, function (Target $target) {
1066
                return empty($target->getDescription());
1067 1
            }),
1068 1
        ];
1069 1
        foreach ($categories as $title => $targets) {
1070 1
            $targetList = $this->generateTargetList($title, $targets, $padding);
1071 1
            $project->log($targetList, Project::MSG_WARN);
1072
        }
1073
    }
1074
1075
    /**
1076
     * Unsets the current Project.
1077
     */
1078 1
    public static function unsetCurrentProject(): void
1079
    {
1080 1
        self::$currentProject = null;
1081
    }
1082
1083
    /**
1084
     * Error handler for PHP errors encountered during the build.
1085
     * This uses the logging for the currently configured project.
1086
     *
1087
     * @param $level
1088
     * @param string $message
1089
     * @param $file
1090
     * @param $line
1091
     */
1092
    public static function handlePhpError($level, $message, $file, $line)
1093
    {
1094
        // don't want to print suppressed errors
1095
        if (!(error_reporting() & $level)) {
1096
            return true;
1097
        }
1098
        if (self::$phpErrorCapture) {
1099
            self::$capturedPhpErrors[] = [
1100
                'message' => $message,
1101
                'level' => $level,
1102
                'line' => $line,
1103
                'file' => $file,
1104
            ];
1105
        } else {
1106
            $message = '[PHP Error] ' . $message;
1107
            $message .= ' [line ' . $line . ' of ' . $file . ']';
1108
1109
            switch ($level) {
1110
                case E_USER_DEPRECATED:
1111
                case E_DEPRECATED:
1112
                case E_NOTICE:
1113
                case E_USER_NOTICE:
1114
                    self::log($message, Project::MSG_VERBOSE);
1115
1116
                    break;
1117
1118
                case E_WARNING:
1119
                case E_USER_WARNING:
1120
                    self::log($message, Project::MSG_WARN);
1121
1122
                    break;
1123
1124
                case E_ERROR:
1125
                case E_USER_ERROR:
1126
                default:
1127
                    self::log($message, Project::MSG_ERR);
1128
            } // switch
1129
        } // if phpErrorCapture
1130
    }
1131
1132
    /**
1133
     * A static convenience method to send a log to the current (last-setup) Project.
1134
     * If there is no currently-configured Project, then this will do nothing.
1135
     *
1136
     * @param string $message
1137
     * @param int    $priority project::MSG_INFO, etc
1138
     */
1139 1
    public static function log($message, $priority = Project::MSG_INFO): void
1140
    {
1141 1
        $p = self::getCurrentProject();
1142 1
        if ($p) {
0 ignored issues
show
introduced by
$p is of type Phing\Project, thus it always evaluated to true.
Loading history...
1143 1
            $p->log($message, $priority);
1144
        }
1145
    }
1146
1147
    /**
1148
     * Gets the current Project.
1149
     *
1150
     * @return Project current Project or NULL if none is set yet/still
1151
     */
1152 8
    public static function getCurrentProject()
1153
    {
1154 8
        return self::$currentProject;
1155
    }
1156
1157
    /**
1158
     * Sets the current Project.
1159
     *
1160
     * @param Project $p
1161
     */
1162 1
    public static function setCurrentProject($p): void
1163
    {
1164 1
        self::$currentProject = $p;
1165
    }
1166
1167
    /**
1168
     * Begins capturing PHP errors to a buffer.
1169
     * While errors are being captured, they are not logged.
1170
     */
1171
    public static function startPhpErrorCapture(): void
1172
    {
1173
        self::$phpErrorCapture = true;
1174
        self::$capturedPhpErrors = [];
1175
    }
1176
1177
    /**
1178
     * Stops capturing PHP errors to a buffer.
1179
     * The errors will once again be logged after calling this method.
1180
     */
1181
    public static function stopPhpErrorCapture(): void
1182
    {
1183
        self::$phpErrorCapture = false;
1184
    }
1185
1186
    /**
1187
     * Clears the captured errors without affecting the starting/stopping of the capture.
1188
     */
1189
    public static function clearCapturedPhpErrors(): void
1190
    {
1191
        self::$capturedPhpErrors = [];
1192
    }
1193
1194
    /**
1195
     * Gets any PHP errors that were captured to buffer.
1196
     *
1197
     * @return array array('message' => message, 'line' => line number, 'file' => file name, 'level' => error level)
1198
     */
1199
    public static function getCapturedPhpErrors()
1200
    {
1201
        return self::$capturedPhpErrors;
1202
    }
1203
1204
    /**
1205
     * This gets a property that was set via command line or otherwise passed into Phing.
1206
     * "Defined" in this case means "externally defined".  The reason this method exists is to
1207
     * provide a public means of accessing commandline properties for (e.g.) logger or listener
1208
     * scripts.  E.g. to specify which logfile to use, PearLogger needs to be able to access
1209
     * the pear.log.name property.
1210
     *
1211
     * @param string $name
1212
     *
1213
     * @return string value of found property (or null, if none found)
1214
     */
1215
    public static function getDefinedProperty($name)
1216
    {
1217
        return self::$definedProps->getProperty($name);
1218
    }
1219
1220
    /**
1221
     * Retuns reference to all properties.
1222
     */
1223 907
    public static function &getProperties()
1224
    {
1225 907
        return self::$properties;
1226
    }
1227
1228
    /**
1229
     * Start up Phing.
1230
     * Sets up the Phing environment but does not initiate the build process.
1231
     *
1232
     * @throws exception - If the Phing environment cannot be initialized
1233
     */
1234 1
    public static function startup(): void
1235
    {
1236
        // setup STDOUT and STDERR defaults
1237 1
        self::initializeOutputStreams();
1238
1239
        // some init stuff
1240 1
        self::getTimer()->start();
1241
1242 1
        self::setSystemConstants();
1243 1
        self::setIncludePaths();
1244 1
        self::setIni();
1245
    }
1246
1247
    /**
1248
     * @param $propName
1249
     * @param $propValue
1250
     *
1251
     * @return string
1252
     */
1253 8
    public static function setProperty($propName, $propValue)
1254
    {
1255 8
        $propName = (string) $propName;
1256 8
        $oldValue = self::getProperty($propName);
1257 8
        self::$properties[$propName] = $propValue;
1258
1259 8
        return $oldValue;
1260
    }
1261
1262
    /**
1263
     * Returns buildfile's path.
1264
     *
1265
     * @param $path
1266
     *
1267
     * @throws ConfigurationException
1268
     *
1269
     * @return string
1270
     */
1271
    protected static function initPath($path)
1272
    {
1273
        // Fallback
1274
        if (empty($path)) {
1275
            $defaultDir = self::getProperty('application.startdir');
1276
            $path = $defaultDir . DIRECTORY_SEPARATOR . self::DEFAULT_BUILD_FILENAME;
1277
        }
1278
1279
        // Adding filename if necessary
1280
        if (is_dir($path)) {
1281
            $path .= DIRECTORY_SEPARATOR . self::DEFAULT_BUILD_FILENAME;
1282
        }
1283
1284
        // Check if path is available
1285
        $dirname = dirname($path);
1286
        if (is_dir($dirname) && !is_file($path)) {
1287
            return $path;
1288
        }
1289
1290
        // Path is valid, but buildfile already exists
1291
        if (is_file($path)) {
1292
            throw new ConfigurationException('Buildfile already exists.');
1293
        }
1294
1295
        throw new ConfigurationException('Invalid path for sample buildfile.');
1296
    }
1297
1298
    /**
1299
     * Writes sample buildfile.
1300
     *
1301
     * If $buildfilePath does not exist, the buildfile is created.
1302
     *
1303
     * @param $buildfilePath buildfile's location
0 ignored issues
show
Documentation Bug introduced by
The doc comment buildfile's at position 0 could not be parsed: Unknown type name 'buildfile's' at position 0 in buildfile's.
Loading history...
1304
     *
1305
     * @throws ConfigurationException
1306
     */
1307
    protected static function initWrite($buildfilePath): void
1308
    {
1309
        // Overwriting protection
1310
        if (file_exists($buildfilePath)) {
1311
            throw new ConfigurationException('Cannot overwrite existing file.');
1312
        }
1313
1314
        file_put_contents($buildfilePath, self::DEFAULT_BUILD_CONTENT);
1315
    }
1316
1317
    /**
1318
     * This operation is expected to call `exit($int)`, which
1319
     * is what the base version does.
1320
     * However, it is possible to do something else.
1321
     *
1322
     * @param int $exitCode code to exit with
1323
     */
1324
    protected static function statusExit($exitCode): void
1325
    {
1326
        Phing::shutdown();
1327
1328
        exit($exitCode);
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
1329
    }
1330
1331
    /**
1332
     * Handle the -propertyfile argument.
1333
     *
1334
     * @throws ConfigurationException
1335
     * @throws IOException
1336
     */
1337
    private function handleArgPropertyFile(array $args, int $pos): int
1338
    {
1339
        if (!isset($args[$pos + 1])) {
1340
            throw new ConfigurationException('You must specify a filename when using the -propertyfile argument');
1341
        }
1342
1343
        $this->propertyFiles[] = $args[++$pos];
1344
1345
        return $pos;
1346
    }
1347
1348
    /**
1349
     * Search parent directories for the build file.
1350
     *
1351
     * Takes the given target as a suffix to append to each
1352
     * parent directory in search of a build file.  Once the
1353
     * root of the file-system has been reached an exception
1354
     * is thrown.
1355
     *
1356
     * @param string $start  start file path
1357
     * @param string $suffix suffix filename to look for in parents
1358
     *
1359
     * @throws ConfigurationException
1360
     *
1361
     * @return File A handle to the build file
1362
     */
1363
    private function findBuildFile($start, $suffix)
1364
    {
1365
        if (self::getMsgOutputLevel() >= Project::MSG_INFO) {
1366
            self::$out->write('Searching for ' . $suffix . ' ...' . PHP_EOL);
1367
        }
1368
1369
        $parent = new File((new File($start))->getAbsolutePath());
1370
        $file = new File($parent, $suffix);
1371
1372
        // check if the target file exists in the current directory
1373
        while (!$file->exists()) {
1374
            // change to parent directory
1375
            $parent = $parent->getParentFile();
1376
1377
            // if parent is null, then we are at the root of the fs,
1378
            // complain that we can't find the build file.
1379
            if (null === $parent) {
1380
                throw new ConfigurationException('Could not locate a build file!');
1381
            }
1382
            // refresh our file handle
1383
            $file = new File($parent, $suffix);
1384
        }
1385
1386
        return $file;
1387
    }
1388
1389
    /**
1390
     * @throws IOException
1391
     */
1392
    private function loadPropertyFiles(): void
1393
    {
1394
        foreach ($this->propertyFiles as $filename) {
1395
            $fileParserFactory = new FileParserFactory();
1396
            $fileParser = $fileParserFactory->createParser(pathinfo($filename, PATHINFO_EXTENSION));
1397
            $p = new Properties(null, $fileParser);
1398
1399
            try {
1400
                $p->load(new File($filename));
1401
            } catch (IOException $e) {
1402
                self::$out->write('Could not load property file ' . $filename . ': ' . $e->getMessage());
1403
            }
1404
            foreach ($p->getProperties() as $prop => $value) {
1405
                self::$definedProps->setProperty($prop, $value);
1406
            }
1407
        }
1408
    }
1409
1410
    /**
1411
     * Close logfiles, if we have been writing to them.
1412
     *
1413
     * @since Phing 2.3.0
1414
     */
1415
    private static function handleLogfile(): void
1416
    {
1417
        if (self::$isLogFileUsed) {
1418
            self::$err->close();
1419
            self::$out->close();
1420
        }
1421
    }
1422
1423
    /**
1424
     * Sets the stdout and stderr streams if they are not already set.
1425
     */
1426 1
    private static function initializeOutputStreams()
1427
    {
1428 1
        if (null === self::$out) {
1429
            if (!defined('STDOUT')) {
1430
                self::$out = new OutputStream(fopen('php://stdout', 'w'));
1431
            } else {
1432
                self::$out = new OutputStream(STDOUT);
1433
            }
1434
        }
1435 1
        if (null === self::$err) {
1436
            if (!defined('STDERR')) {
1437
                self::$err = new OutputStream(fopen('php://stderr', 'w'));
1438
            } else {
1439
                self::$err = new OutputStream(STDERR);
1440
            }
1441
        }
1442
    }
1443
1444
    /**
1445
     * Restores [most] PHP INI values to their pre-Phing state.
1446
     *
1447
     * Currently the following settings are not restored:
1448
     *  - max_execution_time (because getting current time limit is not possible)
1449
     *  - memory_limit (which may have been increased by Phing)
1450
     */
1451 1
    private static function restoreIni(): void
1452
    {
1453 1
        foreach (self::$origIniSettings as $settingName => $settingValue) {
1454
            switch ($settingName) {
1455 1
                case 'error_reporting':
1456 1
                    error_reporting($settingValue);
1457
1458 1
                    break;
1459
1460
                default:
1461 1
                    ini_set($settingName, $settingValue);
1462
            }
1463
        }
1464
    }
1465
1466
    /**
1467
     * Bind any registered build listeners to this project.
1468
     *
1469
     * This means adding the logger and any build listeners that were specified
1470
     * with -listener arg.
1471
     *
1472
     * @throws BuildException
1473
     * @throws ConfigurationException
1474
     */
1475
    private function addBuildListeners(Project $project)
1476
    {
1477
        // Add the default listener
1478
        $project->addBuildListener($this->createLogger());
1479
1480
        foreach ($this->listeners as $listenerClassname) {
1481
            try {
1482
                $clz = Phing::import($listenerClassname);
1483
            } catch (Exception $e) {
1484
                $msg = 'Unable to instantiate specified listener '
1485
                    . 'class ' . $listenerClassname . ' : '
1486
                    . $e->getMessage();
1487
1488
                throw new ConfigurationException($msg);
1489
            }
1490
1491
            $listener = new $clz();
1492
1493
            if ($listener instanceof StreamRequiredBuildLogger) {
1494
                throw new ConfigurationException('Unable to add ' . $listenerClassname . ' as a listener, since it requires explicit error/output streams. (You can specify it as a -logger.)');
1495
            }
1496
            $project->addBuildListener($listener);
1497
        }
1498
    }
1499
1500
    /**
1501
     * Creates the default build logger for sending build events to the log.
1502
     *
1503
     * @throws BuildException
1504
     *
1505
     * @return BuildLogger The created Logger
1506
     */
1507
    private function createLogger()
1508
    {
1509
        if ($this->silent) {
1510
            $logger = new SilentLogger();
1511
            self::$msgOutputLevel = Project::MSG_WARN;
1512
        } elseif (null !== $this->loggerClassname) {
1513
            self::import($this->loggerClassname);
1514
            // get class name part
1515
            $classname = self::import($this->loggerClassname);
1516
            $logger = new $classname();
1517
            if (!($logger instanceof BuildLogger)) {
1518
                throw new BuildException($classname . ' does not implement the BuildLogger interface.');
1519
            }
1520
        } else {
1521
            $logger = new DefaultLogger();
1522
        }
1523
        $logger->setMessageOutputLevel(self::$msgOutputLevel);
1524
        $logger->setOutputStream(self::$out);
1525
        $logger->setErrorStream(self::$err);
1526
        $logger->setEmacsMode($this->emacsMode);
1527
1528
        return $logger;
1529
    }
1530
1531
    /**
1532
     * Creates the InputHandler and adds it to the project.
1533
     *
1534
     * @param Project $project the project instance
1535
     *
1536
     * @throws ConfigurationException
1537
     */
1538
    private function addInputHandler(Project $project): void
1539
    {
1540
        if (null === $this->inputHandlerClassname) {
1541
            $handler = new ConsoleInputHandler(STDIN, new ConsoleOutput());
1542
        } else {
1543
            try {
1544
                $clz = Phing::import($this->inputHandlerClassname);
1545
                $handler = new $clz();
1546
                if (null !== $project && method_exists($handler, 'setProject')) {
1547
                    $handler->setProject($project);
1548
                }
1549
            } catch (Exception $e) {
1550
                $msg = 'Unable to instantiate specified input handler '
1551
                    . 'class ' . $this->inputHandlerClassname . ' : '
1552
                    . $e->getMessage();
1553
1554
                throw new ConfigurationException($msg);
1555
            }
1556
        }
1557
        $project->setInputHandler($handler);
1558
    }
1559
1560
    /**
1561
     * @param string $version
1562
     *
1563
     * @throws BuildException
1564
     * @throws ConfigurationException
1565
     */
1566
    private function comparePhingVersion($version): void
1567
    {
1568
        $current = self::getPhingShortVersion();
1569
1570
        // make sure that version checks are not applied to trunk
1571
        if ('dev' === $current) {
1572
            return;
1573
        }
1574
1575
        if (-1 == version_compare($current, $version)) {
1576
            throw new BuildException(
1577
                sprintf('Incompatible Phing version (%s). Version "%s" required.', $current, $version)
1578
            );
1579
        }
1580
    }
1581
1582
    /**
1583
     * Returns a formatted list of target names with an optional description.
1584
     *
1585
     * @param string   $title   Title for this list
1586
     * @param Target[] $targets Targets in this list
1587
     * @param int      $padding Padding for name column
1588
     */
1589 1
    private function generateTargetList(string $title, array $targets, int $padding): string
1590
    {
1591 1
        usort($targets, function (Target $a, Target $b) {
1592
            return $a->getName() <=> $b->getName();
1593 1
        });
1594
1595 1
        $header = <<<HEADER
1596 1
            {$title}
1597
            -------------------------------------------------------------------------------
1598
1599 1
            HEADER;
1600
1601 1
        $getDetails = function (Target $target) use ($padding): string {
1602
            $details = [];
1603
            if (!empty($target->getDescription())) {
1604
                $details[] = $target->getDescription();
1605
            }
1606
            if (!empty($target->getDependencies())) {
1607
                $details[] = ' - depends on: ' . implode(', ', $target->getDependencies());
1608
            }
1609
            if (!empty($target->getIf())) {
1610
                $details[] = ' - if property: ' . $target->getIf();
1611
            }
1612
            if (!empty($target->getUnless())) {
1613
                $details[] = ' - unless property: ' . $target->getUnless();
1614
            }
1615
            $detailsToString = function (?string $name, ?string $detail) use ($padding): string {
1616
                return sprintf(" %-{$padding}s  %s", $name, $detail);
1617
            };
1618
1619
            return implode(PHP_EOL, array_map($detailsToString, [$target->getName()], $details));
1620 1
        };
1621
1622 1
        return $header . implode(PHP_EOL, array_map($getDetails, $targets)) . PHP_EOL;
1623
    }
1624
1625
    /**
1626
     * Set System constants which can be retrieved by calling Phing::getProperty($propName).
1627
     */
1628 1
    private static function setSystemConstants(): void
1629
    {
1630
        /*
1631
         * PHP_OS returns on
1632
         *   WindowsNT4.0sp6  => WINNT
1633
         *   Windows2000      => WINNT
1634
         *   Windows ME       => WIN32
1635
         *   Windows 98SE     => WIN32
1636
         *   FreeBSD 4.5p7    => FreeBSD
1637
         *   Redhat Linux     => Linux
1638
         *   Mac OS X         => Darwin
1639
         */
1640 1
        self::setProperty('host.os', PHP_OS);
1641
1642
        // this is used by some tasks too
1643 1
        self::setProperty('os.name', PHP_OS);
1644
1645
        // it's still possible this won't be defined,
1646
        // e.g. if Phing is being included in another app w/o
1647
        // using the phing.php script.
1648 1
        if (!defined('PHP_CLASSPATH')) {
1649
            define('PHP_CLASSPATH', get_include_path());
1650
        }
1651
1652 1
        self::setProperty('php.classpath', PHP_CLASSPATH);
1653
1654
        // try to determine the host filesystem and set system property
1655
        // used by Fileself::getFileSystem to instantiate the correct
1656
        // abstraction layer
1657
1658 1
        if (PHP_OS_FAMILY === 'Windows') {
1659
            self::setProperty('host.fstype', 'WINDOWS');
1660
            self::setProperty('user.home', getenv('HOMEDRIVE') . getenv('HOMEPATH'));
1661
        } else {
1662 1
            self::setProperty('host.fstype', 'UNIX');
1663 1
            self::setProperty('user.home', getenv('HOME'));
1664
        }
1665 1
        self::setProperty(self::PHP_INTERPRETER, PHP_BINARY);
1666 1
        self::setProperty('file.separator', FileUtils::getSeparator());
1667 1
        self::setProperty('line.separator', PHP_EOL);
1668 1
        self::setProperty('path.separator', FileUtils::getPathSeparator());
1669 1
        self::setProperty(self::PHP_VERSION, PHP_VERSION);
1670 1
        self::setProperty('php.tmpdir', sys_get_temp_dir());
1671 1
        self::setProperty('application.startdir', getcwd());
1672 1
        self::setProperty('phing.startTime', gmdate(DATE_RFC7231));
1673
1674
        // try to detect machine dependent information
1675 1
        $sysInfo = [];
1676 1
        if (function_exists('posix_uname') && 0 !== stripos(PHP_OS, 'WIN')) {
1677 1
            $sysInfo = posix_uname();
1678
        } else {
1679
            $sysInfo['nodename'] = php_uname('n');
1680
            $sysInfo['machine'] = php_uname('m');
1681
            //this is a not so ideal substition, but maybe better than nothing
1682
            $sysInfo['domain'] = $_SERVER['SERVER_NAME'] ?? 'unknown';
1683
            $sysInfo['release'] = php_uname('r');
1684
            $sysInfo['version'] = php_uname('v');
1685
        }
1686
1687 1
        self::setProperty('host.name', $sysInfo['nodename'] ?? 'unknown');
1688 1
        self::setProperty('host.arch', $sysInfo['machine'] ?? 'unknown');
1689 1
        self::setProperty('host.domain', $sysInfo['domain'] ?? 'unknown');
1690 1
        self::setProperty('host.os.release', $sysInfo['release'] ?? 'unknown');
1691 1
        self::setProperty('host.os.version', $sysInfo['version'] ?? 'unknown');
1692 1
        unset($sysInfo);
1693
    }
1694
1695
    /**
1696
     * Sets the include path to PHP_CLASSPATH constant (if this has been defined).
1697
     *
1698
     * @throws ConfigurationException - if the include_path could not be set (for some bizarre reason)
1699
     */
1700 1
    private static function setIncludePaths(): void
1701
    {
1702 1
        if (defined('PHP_CLASSPATH')) {
1703 1
            $result = set_include_path(PHP_CLASSPATH);
1704 1
            if (false === $result) {
1705
                throw new ConfigurationException('Could not set PHP include_path.');
1706
            }
1707 1
            self::$origIniSettings['include_path'] = $result; // save original value for setting back later
1708
        }
1709
    }
1710
1711
    /**
1712
     * Sets PHP INI values that Phing needs.
1713
     */
1714 1
    private static function setIni(): void
1715
    {
1716 1
        self::$origIniSettings['error_reporting'] = error_reporting(E_ALL);
1717
1718
        // We won't bother storing original max_execution_time, since 1) the value in
1719
        // php.ini may be wrong (and there's no way to get the current value) and
1720
        // 2) it would mean something very strange to set it to a value less than time script
1721
        // has already been running, which would be the likely change.
1722
1723 1
        set_time_limit(0);
1724
1725 1
        self::$origIniSettings['short_open_tag'] = ini_set('short_open_tag', 'off');
1726 1
        self::$origIniSettings['default_charset'] = ini_set('default_charset', 'iso-8859-1');
1727
1728 1
        $mem_limit = (int) SizeHelper::fromHumanToBytes(ini_get('memory_limit'));
1729 1
        if ($mem_limit < (32 * 1024 * 1024) && $mem_limit > -1) {
1730
            // We do *not* need to save the original value here, since we don't plan to restore
1731
            // this after shutdown (we don't trust the effectiveness of PHP's garbage collection).
1732
            ini_set('memory_limit', '32M'); // nore: this may need to be higher for many projects
1733
        }
1734
    }
1735
}
1736