root/branches/2.3/classes/phing/Phing.php

Revision 385, 40.9 kB (checked in by mrook, 5 months ago)

#265 - fix use of max() with empty array

  • Property svn:keywords set to author date id revision
Line 
1 <?php
2 /*
3  * $Id$
4  *
5  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
6  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
7  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
8  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
9  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
10  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
11  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
12  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
13  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
14  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
15  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
16  *
17  * This software consists of voluntary contributions made by many individuals
18  * and is licensed under the LGPL. For more information please see
19  * <http://phing.info>.
20  */
21
22 require_once 'phing/Project.php';
23 require_once 'phing/ProjectComponent.php';
24 require_once 'phing/Target.php';
25 require_once 'phing/Task.php';
26
27 include_once 'phing/BuildException.php';
28 include_once 'phing/ConfigurationException.php';
29 include_once 'phing/BuildEvent.php';
30
31 include_once 'phing/parser/Location.php';
32 include_once 'phing/parser/ExpatParser.php';
33 include_once 'phing/parser/AbstractHandler.php';
34 include_once 'phing/parser/ProjectConfigurator.php';
35 include_once 'phing/parser/RootHandler.php';
36 include_once 'phing/parser/ProjectHandler.php';
37 include_once 'phing/parser/TaskHandler.php';
38 include_once 'phing/parser/TargetHandler.php';
39 include_once 'phing/parser/DataTypeHandler.php';
40 include_once 'phing/parser/NestedElementHandler.php';
41
42 include_once 'phing/system/util/Properties.php';
43 include_once 'phing/util/StringHelper.php';
44 include_once 'phing/system/io/PhingFile.php';
45 include_once 'phing/system/io/OutputStream.php';
46 include_once 'phing/system/io/FileOutputStream.php';
47 include_once 'phing/system/io/FileReader.php';
48 include_once 'phing/system/util/Register.php';
49
50 /**
51  * Entry point into Phing.  This class handles the full lifecycle of a build -- from
52  * parsing & handling commandline arguments to assembling the project to shutting down
53  * and cleaning up in the end.
54  *
55  * If you are invoking Phing from an external application, this is still
56  * the class to use.  Your applicaiton can invoke the start() method, passing
57  * any commandline arguments or additional properties.
58  *
59  * @author    Andreas Aderhold <andi@binarycloud.com>
60  * @author    Hans Lellelid <hans@xmpl.org>
61  * @version   $Revision: 1.51 $
62  * @package   phing
63  */
64 class Phing {
65
66     /** The default build file name */
67     const DEFAULT_BUILD_FILENAME = "build.xml";
68
69     /** Our current message output status. Follows Project::MSG_XXX */
70     private static $msgOutputLevel = Project::MSG_INFO;
71
72     /** PhingFile that we are using for configuration */
73     private $buildFile = null;
74
75     /** The build targets */
76     private $targets = array();
77
78     /**
79      * Set of properties that are passed in from commandline or invoking code.
80      * @var Properties
81      */
82     private static $definedProps;
83
84     /** Names of classes to add as listeners to project */
85     private $listeners = array();
86
87     private $loggerClassname = null;
88
89     /** The class to handle input (can be only one). */
90     private $inputHandlerClassname;
91
92     /** Indicates if this phing should be run */
93     private $readyToRun = false;
94
95     /** Indicates we should only parse and display the project help information */
96     private $projectHelp = false;
97
98     /** Used by utility function getResourcePath() */
99     private static $importPaths;
100
101     /** System-wide static properties (moved from System) */
102     private static $properties = array();
103
104     /** Static system timer. */
105     private static $timer;
106
107     /** The current Project */
108     private static $currentProject;
109
110     /** Whether to capture PHP errors to buffer. */
111     private static $phpErrorCapture = false;
112
113     /** Array of captured PHP errors */
114     private static $capturedPhpErrors = array();
115
116     /**
117      * @var OUtputStream Stream for standard output.
118      */
119     private static $out;
120
121     /**
122      * @var OutputStream Stream for error output.
123      */
124     private static $err;
125
126     /**
127      * @var boolean Whether we are using a logfile.
128      */
129     private static $isLogFileUsed = false;
130
131     /**
132      * Array to hold original ini settings that Phing changes (and needs
133      * to restore in restoreIni() method).
134      *
135      * @var array Struct of array(setting-name => setting-value)
136      * @see restoreIni()
137      */
138     private static $origIniSettings = array();
139
140     /**
141      * Entry point allowing for more options from other front ends.
142      *
143      * This method encapsulates the complete build lifecycle.
144      *
145      * @param array $args The commandline args passed to phing shell script.
146      * @param array $additionalUserProperties   Any additional properties to be passed to Phing (alternative front-end might implement this).
147      *                                          These additional properties will be available using the getDefinedProperty() method and will
148      *                                          be added to the project's "user" properties
149      * @see execute()
150      * @see runBuild()
151      * @throws Exception - if there is an error during build
152      */
153     public static function start($args, array $additionalUserProperties = null) {
154
155         try {
156             $m = new Phing();
157             $m->execute($args);
158         } catch (Exception $exc) {
159             self::handleLogfile();
160             throw $exc;
161         }
162
163         if ($additionalUserProperties !== null) {
164             foreach($additionalUserProperties as $key => $value) {
165                 $m->setDefinedProperty($key, $value);
166             }
167         }
168
169         try {
170             $m->runBuild();
171         } catch(Exception $exc) {
172             self::handleLogfile();
173             throw $exc;
174         }
175
176         // everything fine, shutdown
177         self::handleLogfile();
178     }
179
180     /**
181      * Prints the message of the Exception if it's not null.
182      * @param Exception $t
183      */
184     public static function printMessage(Exception $t) {
185         if (self::$err === null) { // Make sure our error output is initialized
186             self::initializeOutputStreams();
187         }
188         if (self::getMsgOutputLevel() >= Project::MSG_VERBOSE) {
189             self::$err->write($t->__toString() . PHP_EOL);
190         } else {
191             self::$err->write($t->getMessage() . PHP_EOL);
192         }
193     }
194
195     /**
196      * Sets the stdout and stderr streams if they are not already set.
197      */
198     private static function initializeOutputStreams() {
199         if (self::$out === null) {
200             self::$out = new OutputStream(fopen("php://stdout", "w"));
201         }
202         if (self::$err === null) {
203             self::$err = new OutputStream(fopen("php://stderr", "w"));
204         }
205     }
206
207     /**
208      * Sets the stream to use for standard (non-error) output.
209      * @param OutputStream $stream The stream to use for standard output.
210      */
211     public static function setOutputStream(OutputStream $stream) {
212         self::$out = $stream;
213     }
214
215     /**
216      * Gets the stream to use for standard (non-error) output.
217      * @return OutputStream
218      */
219     public static function getOutputStream() {
220         return self::$out;
221     }
222
223     /**
224      * Sets the stream to use for error output.
225      * @param OutputStream $stream The stream to use for error output.
226      */
227     public static function setErrorStream(OutputStream $stream) {
228         self::$err = $stream;
229     }
230
231     /**
232      * Gets the stream to use for error output.
233      * @return OutputStream
234      */
235     public static function getErrorStream() {
236         return self::$err;
237     }
238
239     /**
240      * Close logfiles, if we have been writing to them.
241      *
242      * @since Phing 2.3.0
243      */
244     private static function handleLogfile() {
245         if (self::$isLogFileUsed) {
246             self::$err->close();
247             self::$out->close();
248         }
249     }
250
251     /**
252      * Making output level a static property so that this property
253      * can be accessed by other parts of the system, enabling
254      * us to display more information -- e.g. backtraces -- for "debug" level.
255      * @return int
256      */
257     public static function getMsgOutputLevel() {
258         return self::$msgOutputLevel;
259     }
260
261     /**
262      * Command line entry point. This method kicks off the building
263      * of a project object and executes a build using either a given
264      * target or the default target.
265      *
266      * @param array $args Command line args.
267      * @return void
268      */
269     public static function fire($args) {
270         self::start($args, null);
271     }
272
273     /**
274      * Setup/initialize Phing environment from commandline args.
275      * @param array $args commandline args passed to phing shell.
276      * @return void
277      */
278     public function execute($args) {
279
280         self::$definedProps = new Properties();
281         $this->searchForThis = null;
282
283         // 1) First handle any options which should always
284         // Note: The order in which these are executed is important (if multiple of these options are specified)
285
286         if (in_array('-help', $args) || in_array('-h', $args)) {
287             $this->printUsage();
288             return;
289         }
290
291         if (in_array('-version', $args) || in_array('-v', $args)) {
292             $this->printVersion();
293             return;
294         }
295
296         // 2) Next pull out stand-alone args.
297         // Note: The order in which these are executed is important (if multiple of these options are specified)
298
299         if (false !== ($key = array_search('-quiet', $args, true))) {
300             self::$msgOutputLevel = Project::MSG_WARN;
301             unset($args[$key]);
302         }
303
304         if (false !== ($key = array_search('-verbose', $args, true))) {
305             self::$msgOutputLevel = Project::MSG_VERBOSE;
306             unset($args[$key]);
307         }
308
309         if (false !== ($key = array_search('-debug', $args, true))) {
310             self::$msgOutputLevel = Project::MSG_DEBUG;
311             unset($args[$key]);
312         }
313
314         // 3) Finally, cycle through to parse remaining args
315         //
316         $keys = array_keys($args); // Use keys and iterate to max(keys) since there may be some gaps       
317         $max = $keys ? max($keys) : -1;
318         for($i=0; $i <= $max; $i++) {
319
320             if (!array_key_exists($i, $args)) {
321                 // skip this argument, since it must have been removed above.
322                 continue;
323             }
324
325             $arg = $args[$i];
326
327             if ($arg == "-logfile") {
328                 try {
329                     // see: http://phing.info/trac/ticket/65
330                     if (!isset($args[$i+1])) {
331                         $msg = "You must specify a log file when using the -logfile argument\n";
332                         throw new ConfigurationException($msg);
333                     } else {
334                         $logFile = new PhingFile($args[++$i]);
335                         $out = new FileOutputStream($logFile); // overwrite
336                         self::setOutputStream($out);
337                         self::setErrorStream($out);
338                         self::$isLogFileUsed = true;
339                     }
340                 } catch (IOException $ioe) {
341                     $msg = "Cannot write on the specified log file. Make sure the path exists and you have write permissions.";
342                     throw new ConfigurationException($msg, $ioe);
343                 }
344             } elseif ($arg == "-buildfile" || $arg == "-file" || $arg == "-f") {
345                 if (!isset($args[$i+1])) {
346                     $msg = "You must specify a buildfile when using the -buildfile argument.";
347                     throw new ConfigurationException($msg);
348                 } else {
349                     $this->buildFile = new PhingFile($args[++$i]);
350                 }
351             } elseif ($arg == "-listener") {
352                 if (!isset($args[$i+1])) {
353                     $msg = "You must specify a listener class when using the -listener argument";
354                     throw new ConfigurationException($msg);
355                 } else {
356                     $this->listeners[] = $args[++$i];
357                 }
358             } elseif (StringHelper::startsWith("-D", $arg)) {
359                 $name = substr($arg, 2);
360                 $value = null;
361                 $posEq = strpos($name, "=");
362                 if ($posEq !== false) {
363                     $value = substr($name, $posEq+1);
364                     $name  = substr($name, 0, $posEq);
365                 } elseif ($i < count($args)-1) {
366                     $value = $args[++$i];
367                 }
368                 self::$definedProps->setProperty($name, $value);
369             } elseif ($arg == "-logger") {
370                 if (!isset($args[$i+1])) {
371                     $msg = "You must specify a classname when using the -logger argument";
372                     throw new ConfigurationException($msg);
373                 } else {
374                     $this->loggerClassname = $args[++$i];
375                 }
376             } elseif ($arg == "-inputhandler") {
377                 if ($this->inputHandlerClassname !== null) {
378                     throw new ConfigurationException("Only one input handler class may be specified.");
379                 }
380                 if (!isset($args[$i+1])) {
381                     $msg = "You must specify a classname when using the -inputhandler argument";
382                     throw new ConfigurationException($msg);
383                 } else {
384                     $this->inputHandlerClassname = $args[++$i];
385                 }
386             } elseif ($arg == "-projecthelp" || $arg == "-targets" || $arg == "-list" || $arg == "-l" || $arg == "-p") {
387                 // set the flag to display the targets and quit
388                 $this->projectHelp = true;
389             } elseif ($arg == "-find") {
390                 // eat up next arg if present, default to build.xml
391                 if ($i < count($args)-1) {
392                     $this->searchForThis = $args[++$i];
393                 } else {
394                     $this->searchForThis = self::DEFAULT_BUILD_FILENAME;
395                 }
396             } elseif (substr($arg,0,1) == "-") {
397                 // we don't have any more args
398                 self::$err->write("Unknown argument: $arg" . PHP_EOL);
399                 self::printUsage();
400                 return;
401             } else {
402                 // if it's no other arg, it may be the target
403                 array_push($this->targets, $arg);
404             }
405         }
406
407         // if buildFile was not specified on the command line,
408         if ($this->buildFile === null) {
409             // but -find then search for it
410             if ($this->searchForThis !== null) {
411                 $this->buildFile = $this->_findBuildFile(self::getProperty("user.dir"), $this->searchForThis);
412             } else {
413                 $this->buildFile = new PhingFile(self::DEFAULT_BUILD_FILENAME);
414             }
415         }
416         // make sure buildfile exists
417         if (!$this->buildFile->exists()) {
418             throw new ConfigurationException("Buildfile: " . $this->buildFile->__toString() . " does not exist!");
419         }
420
421         // make sure it's not a directory
422         if ($this->buildFile->isDirectory()) {
423             throw new ConfigurationException("Buildfile: " . $this->buildFile->__toString() . " is a dir!");
424         }
425
426         $this->readyToRun = true;
427     }
428
429     /**
430      * Helper to get the parent file for a given file.
431      *
432      * @param PhingFile $file
433      * @return PhingFile Parent file or null if none
434      */
435     private function _getParentFile(PhingFile $file) {
436         $filename = $file->getAbsolutePath();
437         $file     = new PhingFile($filename);
438         $filename = $file->getParent();
439         return ($filename === null) ? null : new PhingFile($filename);
440     }
441
442     /**
443      * Search parent directories for the build file.
444      *
445      * Takes the given target as a suffix to append to each
446      * parent directory in search of a build file.  Once the
447      * root of the file-system has been reached an exception
448      * is thrown.
449      *
450      * @param string $start Start file path.
451      * @param string $suffix Suffix filename to look for in parents.
452      * @return PhingFile A handle to the build file
453      *
454      * @throws BuildException    Failed to locate a build file
455      */
456     private function _findBuildFile($start, $suffix) {
457         $startf = new PhingFile($start);
458         $parent = new PhingFile($startf->getAbsolutePath());
459         $file   = new PhingFile($parent, $suffix);
460
461         // check if the target file exists in the current directory
462         while (!$file->exists()) {
463             // change to parent directory
464             $parent = $this->_getParentFile($parent);
465
466             // if parent is null, then we are at the root of the fs,
467             // complain that we can't find the build file.
468             if ($parent === null) {
469                 throw new ConfigurationException("Could not locate a build file!");
470             }
471             // refresh our file handle
472             $file = new PhingFile($parent, $suffix);
473         }
474         return $file;
475     }
476
477     /**
478      * Executes the build.
479      * @return void
480      */
481     function runBuild() {
482
483         if (!$this->readyToRun) {
484             return;
485         }
486
487         $project = new Project();
488
489         self::setCurrentProject($project);
490         set_error_handler(array('Phing', 'handlePhpError'));
491
492         $error = null;
493
494         $this->addBuildListeners($project);
495         $this->addInputHandler($project);
496
497         // set this right away, so that it can be used in logging.
498         $project->setUserProperty("phing.file", $this->buildFile->getAbsolutePath());
499
500         try {
501             $project->fireBuildStarted();
502             $project->init();
503         } catch (Exception $exc) {
504             $project->fireBuildFinished($exc);
505             throw $exc;
506         }
507
508         $project->setUserProperty("phing.version", $this->getPhingVersion());
509
510         $e = self::$definedProps->keys();
511         while (count($e)) {
512             $arg   = (string) array_shift($e);
513             $value = (string) self::$definedProps->getProperty($arg);
514             $project->setUserProperty($arg, $value);
515         }
516         unset($e);
517
518         $project->setUserProperty("phing.file", $this->buildFile->getAbsolutePath());
519
520         // first use the Configurator to create the project object
521         // from the given build file.
522
523         try {
524             ProjectConfigurator::configureProject($project, $this->buildFile);
525         } catch (Exception $exc) {
526             $project->fireBuildFinished($exc);
527             restore_error_handler();
528             self::unsetCurrentProject();
529             throw $exc;
530         }
531
532         // make sure that we have a target to execute
533         if (count($this->targets) === 0) {
534             $this->targets[] = $project->getDefaultTarget();
535         }
536
537         // execute targets if help param was not given
538         if (!$this->projectHelp) {
539
540             try {
541                 $project->executeTargets($this->targets);
542             } catch (Exception $exc) {
543                 $project->fireBuildFinished($exc);
544                 restore_error_handler();
545                 self::unsetCurrentProject();
546                 throw $exc;
547             }
548         }
549         // if help is requested print it
550         if ($this->projectHelp) {
551             try {
552