View Javadoc
1   package org.codehaus.mojo.exec;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *     http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.BufferedOutputStream;
23  import java.io.File;
24  import java.io.FileOutputStream;
25  import java.io.FileWriter;
26  import java.io.IOException;
27  import java.io.OutputStream;
28  import java.io.PrintWriter;
29  import java.net.URL;
30  import java.nio.file.Path;
31  import java.util.ArrayList;
32  import java.util.Arrays;
33  import java.util.Collection;
34  import java.util.HashMap;
35  import java.util.Iterator;
36  import java.util.List;
37  import java.util.Map;
38  import java.util.Properties;
39  import java.util.Set;
40  import java.util.TreeSet;
41  import java.util.function.Consumer;
42  import java.util.jar.JarEntry;
43  import java.util.jar.JarOutputStream;
44  import java.util.jar.Manifest;
45  
46  import org.apache.commons.exec.CommandLine;
47  import org.apache.commons.exec.ExecuteException;
48  import org.apache.commons.exec.ExecuteResultHandler;
49  import org.apache.commons.exec.ExecuteWatchdog;
50  import org.apache.commons.exec.Executor;
51  import org.apache.commons.exec.OS;
52  import org.apache.commons.exec.ProcessDestroyer;
53  import org.apache.commons.exec.PumpStreamHandler;
54  import org.apache.commons.exec.ShutdownHookProcessDestroyer;
55  import org.apache.maven.artifact.Artifact;
56  import org.apache.maven.artifact.resolver.filter.AndArtifactFilter;
57  import org.apache.maven.artifact.resolver.filter.IncludesArtifactFilter;
58  import org.apache.maven.execution.MavenSession;
59  import org.apache.maven.plugin.MojoExecutionException;
60  import org.apache.maven.plugins.annotations.Mojo;
61  import org.apache.maven.plugins.annotations.Parameter;
62  import org.apache.maven.plugins.annotations.ResolutionScope;
63  import org.apache.maven.project.MavenProject;
64  import org.apache.maven.toolchain.Toolchain;
65  import org.apache.maven.toolchain.ToolchainManager;
66  import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
67  import org.codehaus.plexus.util.IOUtil;
68  import org.codehaus.plexus.util.StringUtils;
69  import org.codehaus.plexus.util.cli.CommandLineException;
70  import org.codehaus.plexus.util.cli.CommandLineUtils;
71  import org.codehaus.plexus.util.cli.Commandline;
72  import org.codehaus.plexus.util.cli.DefaultConsumer;
73  import org.codehaus.plexus.util.cli.StreamConsumer;
74  
75  /**
76   * A Plugin for executing external programs.
77   *
78   * @author Jerome Lacoste (jerome@coffeebreaks.org)
79   * @version $Id$
80   * @since 1.0
81   */
82  @Mojo( name = "exec", threadSafe = true, requiresDependencyResolution = ResolutionScope.TEST )
83  public class ExecMojo
84      extends AbstractExecMojo
85  {
86      /**
87       * <p>
88       * The executable. Can be a full path or the name of the executable. In the latter case, the executable must be in
89       * the PATH for the execution to work. Omit when using <code>executableDependency</code>.
90       * </p>
91       * <p>
92       * The plugin will search for the executable in the following order:
93       * <ol>
94       * <li>relative to the root of the project</li>
95       * <li>as toolchain executable</li>
96       * <li>relative to the working directory (Windows only)</li>
97       * <li>relative to the directories specified in the system property PATH (Windows Only)</li>
98       * </ol>
99       * Otherwise use the executable as is.
100      * </p>
101      *
102      * @since 1.0
103      */
104     @Parameter( property = "exec.executable" )
105     private String executable;
106 
107     /**
108      * <p>
109      * Timeout in full milliseconds, default is {@code 0}.
110      * <p>
111      * <p>
112      * When set to a value larger than zero, the executable is forcefully
113      * terminated if it did not finish within this time, and the build will
114      * fail.
115      * </p>
116      *
117      * @since 3.0.0
118      */
119     @Parameter( property = "exec.timeout", defaultValue = "0" )
120     private int timeout;
121 
122     /**
123      * <p>
124      * The toolchain. If omitted, <code>"jdk"</code> is assumed.
125      * </p>
126      */
127     @Parameter( property = "exec.toolchain", defaultValue = "jdk" )
128     private String toolchain;
129 
130     /**
131      * The current working directory. Optional. If not specified, basedir will be used.
132      *
133      * @since 1.0
134      */
135     @Parameter( property = "exec.workingdir" )
136     private File workingDirectory;
137 
138     /**
139      * Program standard and error output will be redirected to the file specified by this optional field. If not
140      * specified the standard Maven logging is used. <br/>
141      * <strong>Note:</strong> Be aware that <code>System.out</code> and <code>System.err</code> use buffering, so don't
142      * rely on the order!
143      *
144      * @since 1.1-beta-2
145      * @see java.lang.System#err
146      * @see java.lang.System#in
147      */
148     @Parameter( property = "exec.outputFile" )
149     private File outputFile;
150 
151     /**
152      * Program standard input, output and error streams will be inherited from the maven process.
153      * This allow tighter control of the streams and the console.
154      *
155      * @since 3.0.1
156      * @see ProcessBuilder#inheritIO()
157      */
158     @Parameter( property = "exec.inheritIo" )
159     private boolean inheritIo;
160 
161     /**
162      * When enabled, program standard and error output will be redirected to the
163      * Maven logger as <i>Info</i> and <i>Error</i> level logs, respectively. If not enabled the
164      * traditional behavior of program output being directed to standard System.out
165      * and System.err is used.<br>
166      * <br>
167      * NOTE: When enabled, to log the program standard out as Maven <i>Debug</i> level instead of
168      * <i>Info</i> level use {@code exec.quietLogs=true}. <br>
169      * <br>
170      * This option can be extremely helpful when combined with multithreaded builds
171      * for two reasons:<br>
172      * <ul>
173      * <li>Program output is suffixed with the owning thread name, making it easier
174      * to trace execution of a specific projects build thread.</li>
175      * <li>Program output will not get jumbled with other maven log messages.</li>
176      * </ul>
177      *
178      * For Example, if using {@code exec:exec} to run a script to echo a count from
179      * 1 to 100 as:
180      *
181      * <pre>
182      * for i in {1..100}
183      * do
184      *   echo "${project.artifactId} - $i"
185      * done
186      * </pre>
187      *
188      * When this script is run multi-threaded on two modules, {@code module1} and
189      * {@code module2}, you might get output such as:
190      *
191      * <pre>
192      * [BuilderThread 1] [INFO] --- exec-maven-plugin:1.6.0:exec (test) @ module1 ---
193      * [BuilderThread 2] [INFO] --- exec-maven-plugin:1.6.0:exec (test) @ module2 ---
194      * ...
195      * module2 - 98
196      * modu
197      * module1 - 97
198      * module1 -
199      * le2 - 9899
200      * ...
201      * </pre>
202      *
203      * With this flag enabled, the output will instead come something similar to:
204      *
205      * <pre>
206      * ...
207      * [Exec Stream Pumper] [INFO] [BuilderThread 2] module2 - 98
208      * [Exec Stream Pumper] [INFO] [BuilderThread 1] module1 - 97
209      * [Exec Stream Pumper] [INFO] [BuilderThread 1] module1 - 98
210      * [Exec Stream Pumper] [INFO] [BuilderThread 2] module2 - 99
211      * ...
212      * </pre>
213      *
214      * NOTE 1: To show the thread in the Maven log, configure the Maven
215      * installations <i>conf/logging/simplelogger.properties</i> option:
216      * {@code org.slf4j.simpleLogger.showThreadName=true}<br>
217      *
218      * NOTE 2: This option is ignored when {@code exec.outputFile} is specified.
219      *
220      * @since 3.0.0
221      * @see java.lang.System#err
222      * @see java.lang.System#in
223      */
224     @Parameter( property = "exec.useMavenLogger", defaultValue = "false" )
225     private boolean useMavenLogger;
226 
227     /**
228      * When combined with {@code exec.useMavenLogger=true}, prints all executed
229      * program output at debug level instead of the default info level to the Maven
230      * logger.
231      *
232      * @since 3.0.0
233      */
234     @Parameter( property = "exec.quietLogs", defaultValue = "false" )
235     private boolean quietLogs;
236 
237     /**
238      * <p>
239      * A list of arguments passed to the {@code executable}, which should be of type <code>&lt;argument&gt;</code> or
240      * <code>&lt;classpath&gt;</code>. Can be overridden by using the <code>exec.args</code> environment variable.
241      * </p>
242      *
243      * @since 1.0
244      */
245     @Parameter
246     private List<?> arguments; // TODO: Change ? into something more meaningful
247 
248     /**
249      * @since 1.0
250      */
251     @Parameter( readonly = true, required = true, defaultValue = "${basedir}" )
252     private File basedir;
253 
254     /**
255      * @since 3.0.0
256      */
257     @Parameter( readonly = true, required = true, defaultValue = "${project.build.directory}" )
258     private File buildDirectory;
259 
260     /**
261      * <p>Environment variables to pass to the executed program. For example if you want to set the LANG var:
262      * <code>&lt;environmentVariables&gt;
263      *     &lt;LANG&gt;en_US&lt;/LANG&gt;
264      * &lt;/environmentVariables&gt;
265      * </code>
266      * </p>
267      *
268      * @since 1.1-beta-2
269      */
270     @Parameter
271     private Map<String, String> environmentVariables = new HashMap<String, String>();
272 
273     /**
274      * Environment script to be merged with <i>environmentVariables</i> This script is platform specifics, on Unix its
275      * must be Bourne shell format. Use this feature if you have a need to create environment variable dynamically such
276      * as invoking Visual Studio environment script file
277      *
278      * @since 1.4.0
279      */
280     @Parameter
281     private File environmentScript = null;
282 
283     /**
284      * The current build session instance. This is used for toolchain manager API calls.
285      */
286     @Parameter( defaultValue = "${session}", readonly = true )
287     private MavenSession session;
288 
289     /**
290      * Exit codes to be resolved as successful execution for non-compliant applications (applications not returning 0
291      * for success).
292      *
293      * @since 1.1.1
294      */
295     @Parameter
296     private int[] successCodes;
297 
298     /**
299      * If set to true the classpath and the main class will be written to a MANIFEST.MF file and wrapped into a jar.
300      * Instead of '-classpath/-cp CLASSPATH mainClass' the exec plugin executes '-jar maven-exec.jar'.
301      *
302      * @since 1.1.2
303      */
304     @Parameter( property = "exec.longClasspath", defaultValue = "false" )
305     private boolean longClasspath;
306 
307     /**
308      * If set to true the modulepath and the main class will be written as an @arg file
309      * Instead of '--module-path/-p MODULEPATH ' the exec plugin executes '@modulepath'.
310      *
311      * @since 1.1.2
312      */
313     @Parameter( property = "exec.longModulepath", defaultValue = "true" )
314     private boolean longModulepath;
315 
316     /**
317      * If set to true the child process executes asynchronously and build execution continues in parallel.
318      */
319     @Parameter( property = "exec.async", defaultValue = "false" )
320     private boolean async;
321 
322     /**
323      * If set to true, the asynchronous child process is destroyed upon JVM shutdown. If set to false, asynchronous
324      * child process continues execution after JVM shutdown. Applies only to asynchronous processes; ignored for
325      * synchronous processes.
326      */
327     @Parameter( property = "exec.asyncDestroyOnShutdown", defaultValue = "true" )
328     private boolean asyncDestroyOnShutdown = true;
329 
330     public static final String CLASSPATH_TOKEN = "%classpath";
331 
332     public static final String MODULEPATH_TOKEN = "%modulepath";
333 
334     /**
335      * priority in the execute method will be to use System properties arguments over the pom specification.
336      *
337      * @throws MojoExecutionException if a failure happens
338      */
339     public void execute()
340         throws MojoExecutionException
341     {
342         if ( executable == null )
343         {
344             if (executableDependency == null)
345             {
346                 throw new MojoExecutionException( "The parameter 'executable' is missing or invalid" );
347             }
348 
349             executable = findExecutableArtifact().getFile().getAbsolutePath();
350             getLog().debug( "using executable dependency " + executable);
351         }
352 
353         if ( isSkip() )
354         {
355             getLog().info( "skipping execute as per configuration" );
356             return;
357         }
358 
359         if ( basedir == null )
360         {
361             throw new IllegalStateException( "basedir is null. Should not be possible." );
362         }
363 
364         try
365         {
366 
367             handleWorkingDirectory();
368 
369             String argsProp = getSystemProperty( "exec.args" );
370 
371             List<String> commandArguments = new ArrayList<String>();
372 
373             if ( hasCommandlineArgs() )
374             {
375                 handleCommandLineArgs( commandArguments );
376             }
377             else if ( !StringUtils.isEmpty( argsProp ) )
378             {
379                 handleSystemPropertyArguments( argsProp, commandArguments );
380             }
381             else
382             {
383                 if ( arguments != null )
384                 {
385                     handleArguments( commandArguments );
386                 }
387             }
388 
389             Map<String, String> enviro = handleSystemEnvVariables();
390 
391             CommandLine commandLine = getExecutablePath( enviro, workingDirectory );
392 
393             String[] args = commandArguments.toArray( new String[commandArguments.size()] );
394 
395             commandLine.addArguments( args, false );
396 
397             Executor exec = new ExtendedExecutor( inheritIo );
398             if ( this.timeout > 0 )
399             {
400                 exec.setWatchdog( new ExecuteWatchdog( this.timeout ) );
401             }
402             exec.setWorkingDirectory( workingDirectory );
403             fillSuccessCodes( exec );
404 
405             if ( OS.isFamilyOpenVms() && inheritIo )
406             {
407                 getLog().warn("The inheritIo flag is not supported on OpenVMS, execution will proceed without stream inheritance.");
408             }
409             getLog().debug( "Executing command line: " + commandLine );
410 
411             try
412             {
413                 int resultCode;
414                 if ( outputFile != null )
415                 {
416                     if ( !outputFile.getParentFile().exists() && !outputFile.getParentFile().mkdirs() )
417                     {
418                         getLog().warn( "Could not create non existing parent directories for log file: " + outputFile );
419                     }
420 
421                     FileOutputStream outputStream = null;
422                     try
423                     {
424                         outputStream = new FileOutputStream( outputFile );
425 
426                         resultCode = executeCommandLine( exec, commandLine, enviro, outputStream );
427                     }
428                     finally
429                     {
430                         IOUtil.close( outputStream );
431                     }
432                 }
433                 else if (useMavenLogger)
434                 {
435                     getLog().debug("Will redirect program output to Maven logger");
436                     final String parentThreadName = Thread.currentThread().getName();
437                     final String logSuffix = "[" + parentThreadName + "] ";
438                     Consumer<String> mavenOutRedirect = new Consumer<String>()
439                     {
440 
441                         @Override
442                         public void accept(String logMessage)
443                         {
444                             if (quietLogs)
445                             {
446                                 getLog().debug(logSuffix + logMessage);
447                             }
448                             else
449                             {
450                                 getLog().info(logSuffix + logMessage);
451                             }
452                         }
453                     };
454                     Consumer<String> mavenErrRedirect = new Consumer<String>()
455                     {
456 
457                         @Override
458                         public void accept(String logMessage)
459                         {
460                             getLog().error(logSuffix + logMessage);
461                         }
462                     };
463 
464                     try (OutputStream out = new LineRedirectOutputStream(mavenOutRedirect);
465                             OutputStream err = new LineRedirectOutputStream(mavenErrRedirect)) {
466                         resultCode = executeCommandLine(exec, commandLine, enviro, out, err);
467                     }
468                 }
469                 else
470                 {
471                     resultCode = executeCommandLine( exec, commandLine, enviro, System.out, System.err );
472                 }
473 
474                 if ( isResultCodeAFailure( resultCode ) )
475                 {
476                     String message = "Result of " + commandLine.toString() + " execution is: '" + resultCode + "'.";
477                     getLog().error( message );
478                     throw new MojoExecutionException( message );
479                 }
480             }
481             catch ( ExecuteException e )
482             {
483                 if ( exec.getWatchdog() != null && exec.getWatchdog().killedProcess() )
484                 {
485                     final String message = "Timeout. Process runs longer than " + this.timeout + " ms.";
486                     getLog().error( message );
487                     throw new MojoExecutionException( message, e );
488                 }
489                 else
490                 {
491                     getLog().error( "Command execution failed.", e );
492                     throw new MojoExecutionException( "Command execution failed.", e );
493                 }
494             }
495             catch ( IOException e )
496             {
497                 getLog().error( "Command execution failed.", e );
498                 throw new MojoExecutionException( "Command execution failed.", e );
499             }
500 
501             registerSourceRoots();
502         }
503         catch ( IOException e )
504         {
505             throw new MojoExecutionException( "I/O Error", e );
506         }
507     }
508 
509     private Map<String, String> handleSystemEnvVariables()
510         throws MojoExecutionException
511     {
512 
513         Map<String, String> enviro = new HashMap<String, String>();
514         try
515         {
516             Properties systemEnvVars = CommandLineUtils.getSystemEnvVars();
517             for ( Map.Entry<?, ?> entry : systemEnvVars.entrySet() )
518             {
519                 enviro.put( (String) entry.getKey(), (String) entry.getValue() );
520             }
521         }
522         catch ( IOException x )
523         {
524             getLog().error( "Could not assign default system enviroment variables.", x );
525         }
526 
527         if ( environmentVariables != null )
528         {
529             enviro.putAll( environmentVariables );
530         }
531 
532         if ( this.environmentScript != null )
533         {
534             getLog().info( "Pick up external environment script: " + this.environmentScript );
535             Map<String, String> envVarsFromScript = this.createEnvs( this.environmentScript );
536             if ( envVarsFromScript != null )
537             {
538                 enviro.putAll( envVarsFromScript );
539             }
540         }
541 
542         if ( this.getLog().isDebugEnabled() )
543         {
544             Set<String> keys = new TreeSet<String>();
545             keys.addAll( enviro.keySet() );
546             for ( String key : keys )
547             {
548                 this.getLog().debug( "env: " + key + "=" + enviro.get( key ) );
549             }
550         }
551 
552         return enviro;
553     }
554 
555     /**
556      * This is a convenient method to make the execute method a little bit more readable. It will define the
557      * workingDirectory to be the baseDir in case of workingDirectory is null. If the workingDirectory does not exist it
558      * will created.
559      *
560      * @throws MojoExecutionException
561      */
562     private void handleWorkingDirectory()
563         throws MojoExecutionException
564     {
565         if ( workingDirectory == null )
566         {
567             workingDirectory = basedir;
568         }
569 
570         if ( !workingDirectory.exists() )
571         {
572             getLog().debug( "Making working directory '" + workingDirectory.getAbsolutePath() + "'." );
573             if ( !workingDirectory.mkdirs() )
574             {
575                 throw new MojoExecutionException( "Could not make working directory: '"
576                     + workingDirectory.getAbsolutePath() + "'" );
577             }
578         }
579     }
580 
581     private void handleSystemPropertyArguments( String argsProp, List<String> commandArguments )
582         throws MojoExecutionException
583     {
584         getLog().debug( "got arguments from system properties: " + argsProp );
585 
586         try
587         {
588             String[] args = CommandLineUtils.translateCommandline( argsProp );
589             commandArguments.addAll( Arrays.asList( args ) );
590         }
591         catch ( Exception e )
592         {
593             throw new MojoExecutionException( "Couldn't parse systemproperty 'exec.args'" );
594         }
595     }
596 
597     private void handleCommandLineArgs( List<String> commandArguments )
598         throws MojoExecutionException, IOException
599     {
600         String[] args = parseCommandlineArgs();
601         for ( int i = 0; i < args.length; i++ )
602         {
603             if ( isLongClassPathArgument( args[i] ) )
604             {
605                 // it is assumed that starting from -cp or -classpath the arguments
606                 // are: -classpath/-cp %classpath mainClass
607                 // the arguments are replaced with: -jar $TMP/maven-exec.jar
608                 // NOTE: the jar will contain the classpath and the main class
609                 commandArguments.add( "-jar" );
610                 File tmpFile = createJar( computePath( null ), args[i + 2] );
611                 commandArguments.add( tmpFile.getAbsolutePath() );
612                 i += 2;
613             }
614             else if ( args[i].contains( CLASSPATH_TOKEN ) )
615             {
616                 commandArguments.add( args[i].replace( CLASSPATH_TOKEN, computeClasspathString( null ) ) );
617             }
618             else
619             {
620                 commandArguments.add( args[i] );
621             }
622         }
623     }
624 
625     private void handleArguments( List<String> commandArguments )
626         throws MojoExecutionException, IOException
627     {
628         String specialArg = null;
629 
630         for ( int i = 0; i < arguments.size(); i++ )
631         {
632             Object argument = arguments.get( i );
633 
634             if ( specialArg != null )
635             {
636                 if ( isLongClassPathArgument( specialArg ) && argument instanceof Classpath )
637                 {
638                     // it is assumed that starting from -cp or -classpath the arguments
639                     // are: -classpath/-cp %classpath mainClass
640                     // the arguments are replaced with: -jar $TMP/maven-exec.jar
641                     // NOTE: the jar will contain the classpath and the main class
642                     commandArguments.add( "-jar" );
643 
644                     File tmpFile = createJar( computePath( (Classpath) argument ),
645                                               (String) arguments.get( ++i ) );
646                     commandArguments.add( tmpFile.getAbsolutePath() );
647                 }
648                 else if ( isLongModulePathArgument( specialArg ) && argument instanceof Modulepath )
649                 {
650                     String filePath = new File( buildDirectory, "modulepath" ).getAbsolutePath();
651 
652                     StringBuilder modulePath = new StringBuilder();
653                     modulePath.append( '"' );
654 
655                     for ( Iterator<String> it = computePath( (Modulepath) argument ).iterator(); it.hasNext(); )
656                     {
657                         modulePath.append( it.next().replace( "\\", "\\\\" ) );
658                         if ( it.hasNext() )
659                         {
660                             modulePath.append( File.pathSeparatorChar );
661                         }
662                     }
663 
664                     modulePath.append( '"' );
665 
666                     createArgFile( filePath, Arrays.asList( "-p", modulePath.toString() ) );
667                     commandArguments.add( '@' + filePath );
668                 }
669                 else
670                 {
671                     commandArguments.add( specialArg );
672                 }
673 
674                 specialArg = null;
675 
676                 continue;
677             }
678 
679             if ( argument instanceof Classpath )
680             {
681                 Classpath specifiedClasspath = (Classpath) argument;
682                 commandArguments.add( computeClasspathString( specifiedClasspath ) );
683             }
684             else if ( argument instanceof Modulepath )
685             {
686                 Modulepath specifiedModulepath = (Modulepath) argument;
687                 commandArguments.add( computeClasspathString( specifiedModulepath ) );
688             }
689             else if ( (argument instanceof String) && (isLongModulePathArgument( (String) argument ) || isLongClassPathArgument( (String) argument )) )
690             {
691                 specialArg = (String) argument;
692             }
693             else if (argument == null)
694             {
695                 commandArguments.add( "" );
696             }
697             else
698             {
699                 commandArguments.add( (String) argument );
700             }
701         }
702     }
703 
704     private void fillSuccessCodes( Executor exec )
705     {
706         if ( successCodes != null && successCodes.length > 0 )
707         {
708             exec.setExitValues( successCodes );
709         }
710     }
711 
712     boolean isResultCodeAFailure( int result )
713     {
714         if ( successCodes == null || successCodes.length == 0 )
715         {
716             return result != 0;
717         }
718         for ( int successCode : successCodes )
719         {
720             if ( successCode == result )
721             {
722                 return false;
723             }
724         }
725         return true;
726     }
727 
728     private boolean isLongClassPathArgument( String arg )
729     {
730         return longClasspath && ( "-classpath".equals( arg ) || "-cp".equals( arg ) );
731     }
732 
733     private boolean isLongModulePathArgument( String arg )
734     {
735         return longModulepath && ( "--module-path".equals( arg ) || "-p".equals( arg ) );
736     }
737 
738     /**
739      * Compute the classpath from the specified Classpath. The computed classpath is based on the classpathScope. The
740      * plugin cannot know from maven the phase it is executed in. So we have to depend on the user to tell us he wants
741      * the scope in which the plugin is expected to be executed.
742      *
743      * @param specifiedClasspath Non null when the user restricted the dependencies, <code>null</code> otherwise (the
744      *            default classpath will be used)
745      * @return a platform specific String representation of the classpath
746      */
747     private String computeClasspathString( AbstractPath specifiedClasspath )
748     {
749         List<String> resultList = computePath( specifiedClasspath );
750         StringBuffer theClasspath = new StringBuffer();
751 
752         for ( String str : resultList )
753         {
754             addToClasspath( theClasspath, str );
755         }
756 
757         return theClasspath.toString();
758     }
759 
760     /**
761      * Compute the classpath from the specified Classpath. The computed classpath is based on the classpathScope. The
762      * plugin cannot know from maven the phase it is executed in. So we have to depend on the user to tell us he wants
763      * the scope in which the plugin is expected to be executed.
764      *
765      * @param specifiedClasspath Non null when the user restricted the dependencies, <code>null</code> otherwise (the
766      *            default classpath will be used)
767      * @return a list of class path elements
768      */
769     private List<String> computePath( AbstractPath specifiedClasspath )
770     {
771         List<Artifact> artifacts = new ArrayList<>();
772         List<Path> theClasspathFiles = new ArrayList<>();
773         List<String> resultList = new ArrayList<>();
774 
775         collectProjectArtifactsAndClasspath( artifacts, theClasspathFiles );
776 
777         if ( ( specifiedClasspath != null ) && ( specifiedClasspath.getDependencies() != null ) )
778         {
779             artifacts = filterArtifacts( artifacts, specifiedClasspath.getDependencies() );
780         }
781 
782         for ( Path f : theClasspathFiles )
783         {
784             resultList.add( f.toAbsolutePath().toString() );
785         }
786 
787         for ( Artifact artifact : artifacts )
788         {
789             getLog().debug( "dealing with " + artifact );
790             resultList.add( artifact.getFile().getAbsolutePath() );
791         }
792 
793         return resultList;
794     }
795 
796     private static void addToClasspath( StringBuffer theClasspath, String toAdd )
797     {
798         if ( theClasspath.length() > 0 )
799         {
800             theClasspath.append( File.pathSeparator );
801         }
802         theClasspath.append( toAdd );
803     }
804 
805     private List<Artifact> filterArtifacts( List<Artifact> artifacts, Collection<String> dependencies )
806     {
807         AndArtifactFilter filter = new AndArtifactFilter();
808 
809         filter.add( new IncludesArtifactFilter( new ArrayList<String>( dependencies ) ) ); // gosh
810 
811         List<Artifact> filteredArtifacts = new ArrayList<Artifact>();
812         for ( Artifact artifact : artifacts )
813         {
814             if ( filter.include( artifact ) )
815             {
816                 getLog().debug( "filtering in " + artifact );
817                 filteredArtifacts.add( artifact );
818             }
819         }
820         return filteredArtifacts;
821     }
822 
823     private ProcessDestroyer processDestroyer;
824 
825     CommandLine getExecutablePath( Map<String, String> enviro, File dir )
826     {
827         File execFile = new File( executable );
828         String exec = null;
829         if ( execFile.isFile() )
830         {
831             getLog().debug( "Toolchains are ignored, 'executable' parameter is set to " + executable );
832             exec = execFile.getAbsolutePath();
833         }
834 
835         if ( exec == null )
836         {
837             Toolchain tc = getToolchain();
838 
839             // if the file doesn't exist & toolchain is null, the exec is probably in the PATH...
840             // we should probably also test for isFile and canExecute, but the second one is only
841             // available in SDK 6.
842             if ( tc != null )
843             {
844                 getLog().info( "Toolchain in exec-maven-plugin: " + tc );
845                 exec = tc.findTool( executable );
846             }
847             else
848             {
849                 if ( OS.isFamilyWindows() )
850                 {
851                     List<String> paths = this.getExecutablePaths( enviro );
852                     paths.add( 0, dir.getAbsolutePath() );
853 
854                     exec = findExecutable( executable, paths );
855                 }
856             }
857         }
858 
859         if ( exec == null )
860         {
861             exec = executable;
862         }
863 
864         CommandLine toRet;
865         if ( OS.isFamilyWindows() && !hasNativeExtension( exec ) && hasExecutableExtension( exec ) )
866         {
867             // run the windows batch script in isolation and exit at the end
868             final String comSpec = System.getenv( "ComSpec" );
869             toRet = new CommandLine( comSpec == null ? "cmd" : comSpec );
870             toRet.addArgument( "/c" );
871             toRet.addArgument( exec );
872         }
873         else
874         {
875             toRet = new CommandLine( exec );
876         }
877 
878         return toRet;
879     }
880 
881     static String findExecutable( final String executable, final List<String> paths )
882     {
883         File f = null;
884         search: for ( final String path : paths )
885         {
886             f = new File( path, executable );
887             if ( !OS.isFamilyWindows() && f.isFile() )
888                 break;
889             else
890                 for ( final String extension : getExecutableExtensions() )
891                 {
892                     f = new File( path, executable + extension );
893                     if ( f.isFile() )
894                         break search;
895                 }
896         }
897 
898         if ( f == null || !f.exists() )
899             return null;
900 
901         return f.getAbsolutePath();
902     }
903 
904     private static boolean hasNativeExtension( final String exec )
905     {
906         final String lowerCase = exec.toLowerCase();
907         return lowerCase.endsWith( ".exe" ) || lowerCase.endsWith( ".com" );
908     }
909 
910     private static boolean hasExecutableExtension( final String exec )
911     {
912         final String lowerCase = exec.toLowerCase();
913         for ( final String ext : getExecutableExtensions() )
914             if ( lowerCase.endsWith( ext ) )
915                 return true;
916 
917         return false;
918     }
919 
920     private static List<String> getExecutableExtensions()
921     {
922         final String pathExt = System.getenv( "PATHEXT" );
923         return pathExt == null ? Arrays.asList( ".bat", ".cmd" )
924                         : Arrays.asList( StringUtils.split( pathExt.toLowerCase(), File.pathSeparator ) );
925     }
926 
927     private List<String> getExecutablePaths( Map<String, String> enviro )
928     {
929         List<String> paths = new ArrayList<String>();
930         paths.add( "" );
931 
932         String path = enviro.get( "PATH" );
933         if ( path != null )
934         {
935             paths.addAll( Arrays.asList( StringUtils.split( path, File.pathSeparator ) ) );
936         }
937 
938         return paths;
939     }
940 
941     protected int executeCommandLine( Executor exec, CommandLine commandLine, Map<String, String> enviro,
942                                       OutputStream out, OutputStream err )
943                                           throws ExecuteException, IOException
944     {
945         // note: don't use BufferedOutputStream here since it delays the outputs MEXEC-138
946         PumpStreamHandler psh = new PumpStreamHandler( out, err, System.in );
947         return executeCommandLine( exec, commandLine, enviro, psh );
948     }
949 
950     protected int executeCommandLine( Executor exec, CommandLine commandLine, Map<String, String> enviro,
951                                       FileOutputStream outputFile )
952                                           throws ExecuteException, IOException
953     {
954         BufferedOutputStream bos = new BufferedOutputStream( outputFile );
955         PumpStreamHandler psh = new PumpStreamHandler( bos );
956         return executeCommandLine( exec, commandLine, enviro, psh );
957     }
958 
959     protected int executeCommandLine( Executor exec, final CommandLine commandLine, Map<String, String> enviro,
960                                       final PumpStreamHandler psh )
961                                           throws ExecuteException, IOException
962     {
963         exec.setStreamHandler( psh );
964 
965         int result;
966         try
967         {
968             psh.start();
969             if ( async )
970             {
971                 if ( asyncDestroyOnShutdown )
972                 {
973                     exec.setProcessDestroyer( getProcessDestroyer() );
974                 }
975 
976                 exec.execute( commandLine, enviro, new ExecuteResultHandler()
977                 {
978                     public void onProcessFailed( ExecuteException e )
979                     {
980                         getLog().error( "Async process failed for: " + commandLine, e );
981                     }
982 
983                     public void onProcessComplete( int exitValue )
984                     {
985                         getLog().info( "Async process complete, exit value = " + exitValue + " for: " + commandLine );
986                         try
987                         {
988                             psh.stop();
989                         }
990                         catch ( IOException e )
991                         {
992                             getLog().error( "Error stopping async process stream handler for: " + commandLine, e );
993                         }
994                     }
995                 } );
996                 result = 0;
997             }
998             else
999             {
1000                 result = exec.execute( commandLine, enviro );
1001             }
1002         }
1003         finally
1004         {
1005             if ( !async )
1006             {
1007                 psh.stop();
1008             }
1009         }
1010         return result;
1011     }
1012 
1013     //
1014     // methods used for tests purposes - allow mocking and simulate automatic setters
1015     //
1016 
1017     void setExecutable( String executable )
1018     {
1019         this.executable = executable;
1020     }
1021 
1022     String getExecutable()
1023     {
1024         return executable;
1025     }
1026 
1027     void setWorkingDirectory( String workingDir )
1028     {
1029         setWorkingDirectory( new File( workingDir ) );
1030     }
1031 
1032     void setWorkingDirectory( File workingDir )
1033     {
1034         this.workingDirectory = workingDir;
1035     }
1036 
1037     void setArguments( List<?> arguments )
1038     {
1039         this.arguments = arguments;
1040     }
1041 
1042     void setBasedir( File basedir )
1043     {
1044         this.basedir = basedir;
1045     }
1046 
1047     void setProject( MavenProject project )
1048     {
1049         this.project = project;
1050     }
1051 
1052     protected String getSystemProperty( String key )
1053     {
1054         return System.getProperty( key );
1055     }
1056 
1057     public void setSuccessCodes( Integer... list )
1058     {
1059         this.successCodes = new int[list.length];
1060         for ( int index = 0; index < list.length; index++ )
1061         {
1062             successCodes[index] = list[index];
1063         }
1064     }
1065 
1066     public int[] getSuccessCodes()
1067     {
1068         return successCodes;
1069     }
1070 
1071     private Toolchain getToolchain()
1072     {
1073         Toolchain tc = null;
1074 
1075         try
1076         {
1077             if ( session != null ) // session is null in tests..
1078             {
1079                 ToolchainManager toolchainManager =
1080                     (ToolchainManager) session.getContainer().lookup( ToolchainManager.ROLE );
1081 
1082                 if ( toolchainManager != null )
1083                 {
1084                     tc = toolchainManager.getToolchainFromBuildContext( toolchain, session );
1085                 }
1086             }
1087         }
1088         catch ( ComponentLookupException componentLookupException )
1089         {
1090             // just ignore, could happen in pre-2.0.9 builds..
1091         }
1092         return tc;
1093     }
1094 
1095     /**
1096      * Create a jar with just a manifest containing a Main-Class entry for SurefireBooter and a Class-Path entry for all
1097      * classpath elements. Copied from surefire (ForkConfiguration#createJar())
1098      *
1099      * @param classPath List&lt;String> of all classpath elements.
1100      * @return
1101      * @throws IOException
1102      */
1103     private File createJar( List<String> classPath, String mainClass )
1104         throws IOException
1105     {
1106         File file = File.createTempFile( "maven-exec", ".jar" );
1107         file.deleteOnExit();
1108         FileOutputStream fos = new FileOutputStream( file );
1109         JarOutputStream jos = new JarOutputStream( fos );
1110         jos.setLevel( JarOutputStream.STORED );
1111         JarEntry je = new JarEntry( "META-INF/MANIFEST.MF" );
1112         jos.putNextEntry( je );
1113 
1114         Manifest man = new Manifest();
1115 
1116         // we can't use StringUtils.join here since we need to add a '/' to
1117         // the end of directory entries - otherwise the jvm will ignore them.
1118         StringBuilder cp = new StringBuilder();
1119         for ( String el : classPath )
1120         {
1121             // NOTE: if File points to a directory, this entry MUST end in '/'.
1122             cp.append( new URL( new File( el ).toURI().toASCIIString() ).toExternalForm() + " " );
1123         }
1124 
1125         man.getMainAttributes().putValue( "Manifest-Version", "1.0" );
1126         man.getMainAttributes().putValue( "Class-Path", cp.toString().trim() );
1127         man.getMainAttributes().putValue( "Main-Class", mainClass );
1128 
1129         man.write( jos );
1130         jos.close();
1131 
1132         return file;
1133     }
1134 
1135     private void createArgFile( String filePath, List<String> lines )
1136         throws IOException
1137     {
1138         final String EOL = System.getProperty( "line.separator", "\\n" );
1139 
1140         FileWriter writer = null;
1141         try
1142         {
1143             writer = new FileWriter( filePath );
1144             for ( String line : lines )
1145             {
1146                 writer.append( line ).append( EOL );
1147             }
1148 
1149         }
1150         finally
1151         {
1152             IOUtil.close( writer );
1153         }
1154     }
1155 
1156     protected Map<String, String> createEnvs( File envScriptFile )
1157         throws MojoExecutionException
1158     {
1159         Map<String, String> results = null;
1160 
1161         File tmpEnvExecFile = null;
1162         try
1163         {
1164             tmpEnvExecFile = this.createEnvWrapperFile( envScriptFile );
1165 
1166             Commandline cl = new Commandline();// commons-exec instead?
1167             cl.setExecutable( tmpEnvExecFile.getAbsolutePath() );
1168             if ( !OS.isFamilyWindows() )
1169             {
1170                 cl.setExecutable( "sh" );
1171                 cl.createArg().setFile( tmpEnvExecFile );
1172             }
1173 
1174             // pickup the initial env vars so that the env script can used if necessary
1175             if ( environmentVariables != null )
1176             {
1177                 for ( Map.Entry<String, String> item : environmentVariables.entrySet() )
1178                 {
1179                     cl.addEnvironment( item.getKey(), item.getValue() );
1180                 }
1181             }
1182 
1183             EnvStreamConsumer stdout = new EnvStreamConsumer();
1184             StreamConsumer stderr = new DefaultConsumer();
1185 
1186             CommandLineUtils.executeCommandLine( cl, stdout, stderr );
1187 
1188             if(!stdout.getUnparsedLines().isEmpty())
1189             {
1190                 getLog().warn( "The following lines could not be parsed into environment variables :" );
1191                 for ( String line : stdout.getUnparsedLines() )
1192                 {
1193                     getLog().warn( line );
1194                 }
1195             }
1196 
1197             results = stdout.getParsedEnv();
1198         }
1199         catch ( CommandLineException e )
1200         {
1201             throw new MojoExecutionException( e.getMessage() );
1202         }
1203         catch ( IOException e )
1204         {
1205             throw new MojoExecutionException( e.getMessage() );
1206         }
1207         finally
1208         {
1209             if ( tmpEnvExecFile != null )
1210             {
1211                 tmpEnvExecFile.delete();
1212             }
1213         }
1214 
1215         return results;
1216 
1217     }
1218 
1219     protected File createEnvWrapperFile( File envScript )
1220         throws IOException
1221     {
1222         PrintWriter writer = null;
1223         File tmpFile = null;
1224         try
1225         {
1226 
1227             if ( OS.isFamilyWindows() )
1228             {
1229                 tmpFile = File.createTempFile( "env", ".bat" );
1230                 writer = new PrintWriter( tmpFile );
1231                 writer.append( "@echo off" ).println();
1232                 writer.append( "call \"" ).append( envScript.getCanonicalPath() ).append( "\"" ).println();
1233                 writer.append( "echo " + EnvStreamConsumer.START_PARSING_INDICATOR ).println();
1234                 writer.append( "set" ).println();
1235                 writer.flush();
1236             }
1237             else
1238             {
1239                 tmpFile = File.createTempFile( "env", ".sh" );
1240                 // tmpFile.setExecutable( true );//java 6 only
1241                 writer = new PrintWriter( tmpFile );
1242                 writer.append( "#! /bin/sh" ).println();
1243                 writer.append( ". " ).append( envScript.getCanonicalPath() ).println(); // works on all unix??
1244                 writer.append( "echo " + EnvStreamConsumer.START_PARSING_INDICATOR ).println();
1245                 writer.append( "env" ).println();
1246                 writer.flush();
1247             }
1248         }
1249         finally
1250         {
1251             IOUtil.close( writer );
1252         }
1253 
1254         return tmpFile;
1255 
1256     }
1257 
1258     protected ProcessDestroyer getProcessDestroyer()
1259     {
1260         if ( processDestroyer == null )
1261         {
1262             processDestroyer = new ShutdownHookProcessDestroyer();
1263         }
1264         return processDestroyer;
1265     }
1266 }