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 21require_once 'phing/Task.php'; 22include_once 'phing/types/FileList.php'; 23include_once 'phing/types/FileSet.php'; 24 25 26/** 27 * Executes a command on the (filtered) file list/set. 28 * (Loosely based on the "Ant Apply" task - http://ant.apache.org/manual/Tasks/apply.html) 29 * 30 * @author Utsav Handa <handautsav at hotmail dot com> 31 * @package phing.tasks.system 32 * 33 * @todo Add support for mapper, targetfile expressions 34 */ 35class ApplyTask extends Task { 36 37 38 /** 39 * Configuration(s) 40 * 41 */ 42 //[TBA]const TARGETFILE_ID = '__TARGETFILE__'; 43 const SOURCEFILE_ID = '__SOURCEFILE__'; 44 45 46 /** 47 * File Set/List of files. 48 * @var array 49 */ 50 protected $filesets = array(); 51 protected $filelists = array(); 52 53 54 /** 55 * Commandline managing object 56 * @var commandline 57 */ 58 protected $commandline; 59 60 /** 61 * Working directory 62 * @var phingfile 63 */ 64 protected $dir; 65 protected $currentdirectory; 66 67 68 /** 69 * Command to be executed 70 * @var string 71 */ 72 protected $realCommand; 73 74 75 /** 76 * Escape (shell) command using 'escapeshellcmd' before execution 77 * @var boolean 78 */ 79 protected $escape = false; 80 81 82 /** 83 * Where to direct output 84 * @var phingfile 85 */ 86 protected $output; 87 88 89 /** 90 * Where to direct error 91 * @var phingfile 92 */ 93 protected $error; 94 95 /** 96 * Whether output should be appended to or overwrite an existing file 97 * @var boolean 98 */ 99 protected $appendoutput = false; 100 101 /** 102 * Runs the command only once, appending all files as arguments 103 * else command will be executed once for every file. 104 * @var boolean 105 */ 106 protected $parallel = false; 107 108 109 /** 110 * Whether source file name should be added to the end of command automatically 111 * @var boolean 112 */ 113 protected $addsourcefile = true; 114 115 116 /** 117 * Whether to spawn the command execution as a background process 118 * @var boolean 119 */ 120 protected $spawn = false; 121 122 123 /** 124 * Property name to set with return value 125 * @var string 126 */ 127 protected $returnProperty; 128 129 130 /** 131 * Property name to set with output value 132 * @var string 133 */ 134 protected $outputProperty; 135 136 137 /** 138 * Whether the filenames should be passed on the command line as relative pathnames (relative to the base directory of the corresponding fileset/list) 139 * @var boolean 140 */ 141 protected $relative = false; 142 143 144 /** 145 * Operating system information 146 * @var string 147 */ 148 protected $os; 149 protected $currentos; 150 protected $osvariant; 151 152 153 /** 154 * Logging level for status messages 155 * @var integer 156 */ 157 protected $loglevel = null; 158 159 160 /** 161 * Fail on command that exits with a returncode other than zero 162 * @var boolean 163 * 164 */ 165 protected $failonerror = false; 166 167 168 /** 169 * Whether to use PHP's passthru() function instead of exec() 170 * @var boolean 171 */ 172 protected $passthru = false; 173 174 175 /** 176 * Whether to use forward-slash as file-separator on the file names 177 * @var boolean 178 */ 179 protected $forwardslash = false; 180 181 182 /** 183 * Limit the amount of parallelism by passing at most this many sourcefiles at once 184 * (Set it to <= 0 for unlimited) 185 * @var integer 186 */ 187 protected $maxparallel = 0; 188 189 190 191 /** 192 * Supports embedded <filelist> element. 193 * 194 * @return FileList 195 */ 196 public function createFileList() { 197 $num = array_push($this->filelists, new FileList()); 198 return $this->filelists[$num-1]; 199 } 200 201 202 /** 203 * Nested creator, adds a set of files (nested <fileset> attribute). 204 * This is for when you don't care what order files get appended. 205 * 206 * @return FileSet 207 */ 208 public function createFileSet() { 209 $num = array_push($this->filesets, new FileSet()); 210 return $this->filesets[$num-1]; 211 } 212 213 214 /** 215 * Sets the command executable information 216 * 217 * @param string $executable Executable path 218 * 219 * @return void 220 */ 221 public function setExecutable($executable) { 222 $this->commandline->setExecutable( (string) $executable); 223 } 224 225 226 /** 227 * Specify the working directory for the command execution. 228 * 229 * @param PhingFile $dir Set the working directory as specified 230 * 231 * @return void 232 */ 233 public function setDir(PhingFile $dir) { 234 $this->dir = $dir; 235 } 236 237 238 /** 239 * Escape command using 'escapeshellcmd' before execution 240 * 241 * @param boolean $escape Escape command before execution 242 * 243 * @return void 244 */ 245 public function setEscape($escape) { 246 $this->escape = (bool) $escape; 247 } 248 249 250 /** 251 * File to which output should be written 252 * 253 * @param PhingFile $outputfile Output log file 254 * 255 * @return void 256 */ 257 public function setOutput(PhingFile $outputfile) { 258 $this->output = $outputfile; 259 } 260 261 262 /** 263 * File to which output should be written 264 * 265 * @param PhingFile $outputfile Output log file 266 * 267 * @return void 268 */ 269 public function setAppend($append) { 270 $this->appendoutput = (bool) $append; 271 } 272 273 274 /** 275 * Run the command only once, appending all files as arguments 276 * 277 * @param Boolean $parallel Identifier for files as arguments appending 278 * 279 * @return void 280 */ 281 public function setParallel($parallel) { 282 $this->parallel = (bool) $parallel; 283 } 284 285 286 /** 287 * To add the source filename at the end of command of automatically 288 * 289 * @param Boolean $addsourcefile Identifier for adding source file at the end of command 290 * 291 * @return void 292 */ 293 public function setAddsourcefile($addsourcefile) { 294 $this->addsourcefile = (bool) $addsourcefile; 295 } 296 297 298 /** 299 * File to which error output should be written 300 * 301 * @param PhingFile $errorfile Error log file 302 * 303 * @return void 304 */ 305 public function setError(PhingFile $errorfile) { 306 $this->error = $errorfile; 307 } 308 309 310 /** 311 * Whether to spawn the command and run as background process 312 * 313 * @param boolean $spawn If the command is to be run as a background process 314 * 315 * @return void 316 */ 317 public function setSpawn($spawn) { 318 $this->spawn = (bool) $spawn; 319 } 320 321 322 /** 323 * The name of property to set to return value 324 * 325 * @param string $propertyname Property name 326 * 327 * @return void 328 */ 329 public function setReturnProperty($propertyname) { 330 $this->returnProperty = (string) $propertyname; 331 } 332 333 334 /** 335 * The name of property to set to output value 336 * 337 * @param string $propertyname Property name 338 * 339 * @return void 340 */ 341 public function setOutputProperty($propertyname) { 342 $this->outputProperty = (string) $propertyname; 343 } 344 345 346 /** 347 * Whether the filenames should be passed on the command line as relative 348 * pathnames (relative to the base directory of the corresponding fileset/list) 349 * 350 * @param boolean $escape Escape command before execution 351 * 352 * @return void 353 */ 354 public function setRelative($relative) { 355 $this->relative = (bool) $relative; 356 } 357 358 359 /** 360 * Specify OS (or muliple OS) that must match in order to execute this command. 361 * 362 * @param string $os Operating system string (e.g. "Linux") 363 * 364 * @return void 365 */ 366 public function setOs($os) { 367 $this->os = (string) $os; 368 } 369 370 371 /** 372 * Whether to use PHP's passthru() function instead of exec() 373 * 374 * @param boolean $passthru If passthru shall be used 375 * 376 * @return void 377 */ 378 public function setPassthru($passthru) { 379 $this->passthru = (bool) $passthru; 380 } 381 382 383 /** 384 * Fail on command exits with a returncode other than zero 385 * 386 * @param boolean $failonerror Indicator to fail on error 387 * 388 * @return void 389 */ 390 public function setFailonerror($failonerror) { 391 $this->failonerror = (bool) $failonerror; 392 } 393 public function setCheckreturn($failonerror) { 394 $this->setFailonerror($failonerror); 395 } 396 397 398 /** 399 * Whether to use forward-slash as file-separator on the file names 400 * 401 * @param boolean $forwardslash Indicator to use forward-slash 402 * 403 * @return void 404 */ 405 public function setForwardslash($forwardslash) { 406 $this->forwardslash = (bool) $forwardslash; 407 } 408 409 /** 410 * Limit the amount of parallelism by passing at most this many sourcefiles at once 411 * 412 * @param boolean $forwardslash Indicator to use forward-slash 413 * 414 * @return void 415 */ 416 public function setMaxparallel($max) { 417 $this->maxparallel = (int) $max; 418 } 419 420 421 /** [TBA] 422 * Supports embedded <targetfile> element. 423 * 424 * @return void 425 */ 426 /**public function createTargetfile() { 427 return $this->commandline->addArguments( array(self::TARGETFILE_ID) ); 428 }*/ 429 430 431 /** 432 * Supports embedded <srcfile> element. 433 * 434 * @return void 435 */ 436 public function createSrcfile() { 437 return $this->commandline->addArguments( array(self::SOURCEFILE_ID) ); 438 } 439 440 441 /** 442 * Supports embedded <arg> element. 443 * 444 * @return void 445 */ 446 public function createArg() { 447 return $this->commandline->createArgument(); 448 } 449 450 451 452 /**********************************************************************************/ 453 /**************************** T A S K M E T H O D S ******************************/ 454 /**********************************************************************************/ 455 456 457 /** 458 * Class Initialization 459 * @return void 460 */ 461 public function init() { 462 463 $this->commandline = new Commandline(); 464 $this->loglevel = Project::MSG_INFO; //VERBOSE; 465 } 466 467 468 /** 469 * Do work 470 * @throws BuildException 471 */ 472 public function main() { 473 474 475 // Log 476 $this->log('Started ', $this->loglevel); 477 478 479 // Initialize // 480 $this->initialize(); 481 482 483 // Validate O.S. applicability 484 if ($this->validateOS()) { 485 486 487 // Build the command // 488 $this->buildCommand(); 489 490 491 // Process // 492 // - FileLists 493 foreach($this->filelists as $fl) { 494 $this->process( $fl->getFiles($this->project), $fl->getDir($this->project) ); 495 } 496 unset($this->filelists); 497 498 // - FileSets 499 foreach($this->filesets as $fs) { 500 $this->process( $fs->getDirectoryScanner($this->project)->getIncludedFiles(), $fs->getDir($this->project) ); 501 } 502 unset($this->filesets); 503 504 505 } 506 507 508 /// Cleanup // 509 $this->cleanup(); 510 511 512 // Log 513 $this->log('End ', $this->loglevel); 514 515 516 } 517 518 519 520 /**********************************************************************************/ 521 /********************** T A S K C O R E M E T H O D S ***************************/ 522 /**********************************************************************************/ 523 524 525 /** 526 * Checks whether the current O.S. should be supported 527 * 528 * @return boolean False if the exec command shall not be run 529 */ 530 protected function validateOS() { 531 532 533 // Log 534 $this->log('Validating Operating System information ', $this->loglevel); 535 536 537 // Checking whether'os' information is specified 538 if (empty($this->os)) { 539 540 // Log 541 $this->log("Operating system information not specified. Skipped checking. ", $this->loglevel); 542 543 return true; 544 } 545 546 547 // Validating the operating system information 548 $matched = (strpos(strtolower($this->os), strtolower($this->currentos)) !== false) ? true : false; 549 550 // Log 551 $this->log("Operating system '".$this->currentos."' " .($matched ? '' : 'not '). "found in " . $this->os, $this->loglevel); 552 553 554 return $matched; 555 } 556 557 558 559 560 /** 561 * Initializes the task operations, i.e. 562 * - Required information validation 563 * - Working directory 564 * 565 * @param none 566 * 567 * @return void 568 */ 569 private function initialize() { 570 571 // Log 572 $this->log('Initializing started ', $this->loglevel); 573 574 575 ///// Validating the required parameters ///// 576 577 // Executable 578 if ($this->commandline->getExecutable() === null) { 579 return $this->throwBuildException('Please provide "executable" information'); 580 } 581 582 583 // Retrieving the current working directory 584 $this->currentdirectory = getcwd(); 585 586 587 // Directory (in which the command should be executed) 588 if ($this->dir !== null) { 589 590 // Try expanding (any) symbolic links 591 if (! $this->dir->getCanonicalFile()->isDirectory()) { 592 return $this->throwBuildException("'" . $this->dir . "' is not a valid directory"); 593 } 594 595 // Change working directory 596 $dirchangestatus = @chdir($this->dir->getPath()); 597 598 // Log 599 $this->log('Working directory change '.($dirchangestatus ? 'successful' : 'failed') .' to ' . $this->dir->getPath(), $this->loglevel); 600 601 } 602 603 604 ///// Preparing the task environment ///// 605 606 // Getting current operationg system 607 $this->currentos = Phing::getProperty('os.name'); 608 609 // Log 610 $this->log('Operating System identified : ' . $this->currentos, $this->loglevel); 611 612 613 // Getting the O.S. type identifier 614 // Validating the 'filesystem' for determining the OS type [UNIX, WINNT and WIN32] 615 // (Another usage could be with 'os.name' for determination) 616 if ('WIN' == strtoupper(substr(Phing::getProperty('host.fstype'), 0, 3))) { 617 $this->osvariant = 'WIN'; // Probable Windows flavour 618 } else { 619 $this->osvariant = 'LIN'; // Probable GNU/Linux flavour 620 } 621 622 // Log 623 $this->log('Operating System variant identified : ' . $this->osvariant, $this->loglevel); 624 625 626 627 // Log 628 $this->log('Initializing completed ', $this->loglevel); 629 630 631 return; 632 } 633 634 635 636 /** 637 * Builds the full command to execute and stores it in $realCommand. 638 * 639 * @param none 640 * 641 * @return void 642 */ 643 private function buildCommand() { 644 645 // Log 646 $this->log('Command building started ', $this->loglevel); 647 648 // Building the executable 649 $this->realCommand = Commandline::toString($this->commandline->getCommandline(), $this->escape); 650 651 652 // Adding the source filename at the end of command, validating the existing 653 // sourcefile position explicit mentioning 654 if ( ($this->addsourcefile === true) && (strpos($this->realCommand, self::SOURCEFILE_ID) === false) ) { 655 $this->realCommand .= ' ' . self::SOURCEFILE_ID; 656 } 657 658 659 // Setting command output redirection with content appending 660 if ($this->output !== null) { 661 662 $this->realCommand .= ' 1>'; 663 $this->realCommand .= ($this->appendoutput ? '>' : ''); // Append output 664 $this->realCommand .= ' ' . escapeshellarg($this->output->getPath()); 665 666 } elseif ($this->spawn) { // Validating the 'spawn' configuration, and redirecting the output to 'null' 667 668 // Validating the O.S. variant 669 if ('WIN' == $this->osvariant) { 670 $this->realCommand .= ' > NUL'; // MS Windows output nullification 671 } else { 672 $this->realCommand .= ' 1>/dev/null'; // GNU/Linux output nullification 673 } 674 675 $this->log("For process spawning, setting Output nullification ", $this->loglevel); 676 } 677 678 679 // Setting command error redirection with content appending 680 if ($this->error !== null) { 681 $this->realCommand .= ' 2>'; 682 $this->realCommand .= ($this->appendoutput ? '>' : ''); // Append error 683 $this->realCommand .= ' ' . escapeshellarg($this->error->getPath()); 684 } 685 686 687 688 // Setting the execution as a background process 689 if ($this->spawn) { 690 691 // Validating the O.S. variant 692 if ('WIN' == $this->osvariant) { 693 $this->realCommand = 'start /b ' . $this->realcommand; // MS Windows background process forking 694 } else { 695 $this->realCommand .= ' &'; // GNU/Linux background process forking 696 } 697 698 } 699 700 701 // Log 702 $this->log('Command built : ' . $this->realCommand, $this->loglevel); 703 704 705 // Log 706 $this->log('Command building completed ', $this->loglevel); 707 708 709 return; 710 } 711 712 713 714 715 716 /** 717 * Processes the files list with provided information for execution 718 * 719 * @param $files File list for processing 720 * @param #basedir Base directory of the file list 721 * 722 * @return void 723 */ 724 private function process($files, $basedir) { 725 726 // Log 727 $this->log("Processing Filelist with base directory ($basedir) ", $this->loglevel); 728 729 730 // Process each file in the list for applying the 'realcommand' 731 foreach ($files as $count => $file) { 732 733 734 // Preparing the absolute filename with relative path information 735 $absolutefilename = $this->getFilePath($file, $basedir, $this->relative); 736 737 738 // Checking whether 'parallel' information is enabled. If enabled, append all 739 // the file names as arguments, and run only once. 740 if ($this->parallel) { 741 742 // Checking whether 'maxparallel' setting describes parallelism limitation 743 // by passing at most 'maxparallel' many sourcefiles at once 744 $slicedfiles = array_splice($files, 0, (($this->maxparallel > 0) ? $this->maxparallel : count($files)));; 745 746 $absolutefilename = implode(' ', $this->getFilePath($slicedfiles, $basedir, $this->relative)); 747 } 748 749 750 751 // Checking whether the forward-slash as file-separator has been set. 752 // (Applicability: The source {and target} file names must use the forward slash as file separator) 753 if ($this->forwardslash) { 754 $absolutefilename = str_replace(DIRECTORY_SEPARATOR, '/', $absolutefilename); 755 } 756 757 758 // Preparing the command to be executed 759 $filecommand = str_replace(array(self::SOURCEFILE_ID), array($absolutefilename), $this->realCommand); 760 761 762 // Command execution 763 list($returncode, $output) = $this->executeCommand($filecommand); 764 765 766 // Process the stuff on the first command execution only 767 if (0 == $count) { 768 769 // Sets the return property 770 if ($this->returnProperty) { 771 $this->project->setProperty($this->returnProperty, $returncode); 772 } 773 774 // Sets the output property 775 if ($this->outputProperty) { 776 $this->project->setProperty($this->outputProperty, implode("\n", $output)); 777 } 778 779 780 } 781 782 783 // Validating the 'return-code' 784 if ( ($this->failonerror) && ($returncode != 0) ) { 785 return $this->throwBuildException("Task exited with code ($returncode)"); 786 } 787 788 789 // Validate the 'parallel' information for command execution. If the command has been 790 // executed with the filenames as argument, considering 'maxparallel', just break. 791 if ( ($this->parallel) && (! array_key_exists($count, $files)) ) { 792 break; 793 } 794 795 796 797 } // Each file processing loop ends 798 799 800 801 return; 802 } 803 804 805 806 /** 807 * Executes the specified command and returns the return code & output. 808 * 809 * @return array array(return code, array with output) 810 */ 811 private function executeCommand($command) { 812 813 // Var(s) 814 $output = array(); 815 $return = null; 816 817 // Validating the command executor container 818 ($this->passthru ? passthru($command, $return) : exec($command, $output, $return)); 819 820 // Log 821 $this->log('Command execution : (' . ($this->passthru ? 'passthru' : 'exec') . ') : ' . $command . " : completed with return code ($return) ", $this->loglevel); 822 823 824 return array($return, $output); 825 } 826 827 828 /** 829 * Runs cleanup tasks post execution 830 * - Restore working directory 831 * 832 * @return void 833 */ 834 private function cleanup() { 835 836 // Restore working directory 837 if ($this->dir !== null) { 838 @chdir($this->currentdirectory); 839 } 840 841 842 843 return; 844 } 845 846 847 848 /** 849 * Prepares the filename per base directory and relative path information 850 * 851 * @param $information Exception information 852 * 853 * @return mixed processed filenames 854 */ 855 function getFilePath($filename, $basedir, $relative) { 856 857 // Var(s) 858 $files = array(); 859 860 // Validating the 'file' information 861 $files = (is_array($filename)) ? $filename : array($filename); 862 863 864 // Processing the file information 865 foreach($files as $index => $file) { 866 $absolutefilename = (($relative === false) ? ($basedir . DIRECTORY_SEPARATOR) : ''); 867 $absolutefilename .= $file; 868 $files[$index] = $absolutefilename; 869 } 870 871 872 return (is_array($filename) ? $files : $files[0]); 873 } 874 875 876 877 /** 878 * Throws the exception with specified information 879 * 880 * @param $information Exception information 881 * 882 * @return void 883 */ 884 private function throwBuildException($information) { 885 throw new BuildException('ApplyTask: ' . (string) $information); 886 } 887 888 889 890 891 892} 893