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