View Javadoc
1   package org.apache.maven.shared.release.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.File;
23  import java.io.FileWriter;
24  import java.io.IOException;
25  import java.util.Arrays;
26  import java.util.List;
27  import java.util.Properties;
28  
29  import org.apache.commons.cli.CommandLine;
30  import org.apache.commons.cli.OptionBuilder;
31  import org.apache.commons.cli.Options;
32  import org.apache.commons.cli.PosixParser;
33  import org.apache.maven.settings.io.xpp3.SettingsXpp3Writer;
34  import org.apache.maven.shared.invoker.DefaultInvocationRequest;
35  import org.apache.maven.shared.invoker.DefaultInvoker;
36  import org.apache.maven.shared.invoker.InvocationOutputHandler;
37  import org.apache.maven.shared.invoker.InvocationRequest;
38  import org.apache.maven.shared.invoker.InvocationResult;
39  import org.apache.maven.shared.invoker.Invoker;
40  import org.apache.maven.shared.invoker.InvokerLogger;
41  import org.apache.maven.shared.invoker.MavenInvocationException;
42  import org.apache.maven.shared.release.ReleaseResult;
43  import org.apache.maven.shared.release.env.ReleaseEnvironment;
44  import org.codehaus.plexus.component.annotations.Component;
45  import org.codehaus.plexus.logging.Logger;
46  import org.codehaus.plexus.util.cli.CommandLineUtils;
47  
48  /**
49   * Fork Maven using the maven-invoker shared library.
50   */
51  @Component( role = MavenExecutor.class, hint = "invoker" )
52  @SuppressWarnings( "static-access" )
53  public class InvokerMavenExecutor
54      extends AbstractMavenExecutor
55  {
56  
57      private static final Options OPTIONS = new Options();
58  
59      private static final char SET_SYSTEM_PROPERTY = 'D';
60  
61      private static final char OFFLINE = 'o';
62  
63      private static final char REACTOR = 'r';
64  
65      private static final char QUIET = 'q';
66  
67      private static final char DEBUG = 'X';
68  
69      private static final char ERRORS = 'e';
70  
71      private static final char NON_RECURSIVE = 'N';
72  
73      private static final char UPDATE_SNAPSHOTS = 'U';
74  
75      private static final char ACTIVATE_PROFILES = 'P';
76  
77      private static final char CHECKSUM_FAILURE_POLICY = 'C';
78  
79      private static final char CHECKSUM_WARNING_POLICY = 'c';
80  
81      private static final char ALTERNATE_USER_SETTINGS = 's';
82  
83      private static final String ALTERNATE_GLOBAL_SETTINGS = "gs";
84  
85      private static final String FAIL_FAST = "ff";
86  
87      private static final String FAIL_AT_END = "fae";
88  
89      private static final String FAIL_NEVER = "fn";
90      
91      private static final String ALTERNATE_POM_FILE = "f";
92      
93      private static final String THREADS = "T";
94  
95      private static final String BATCH_MODE = "B";
96      
97      /** Constant <code>ALTERNATE_USER_TOOLCHAINS='t'</code> */
98      public static final char ALTERNATE_USER_TOOLCHAINS = 't';
99      
100     static
101     {
102         OPTIONS.addOption(
103             OptionBuilder.withLongOpt( "define" ).hasArg().withDescription( "Define a system property" ).create(
104                 SET_SYSTEM_PROPERTY ) );
105 
106         OPTIONS.addOption( OptionBuilder.withLongOpt( "offline" ).withDescription( "Work offline" ).create( OFFLINE ) );
107 
108         OPTIONS.addOption(
109             OptionBuilder.withLongOpt( "quiet" ).withDescription( "Quiet output - only show errors" ).create( QUIET ) );
110 
111         OPTIONS.addOption(
112             OptionBuilder.withLongOpt( "debug" ).withDescription( "Produce execution debug output" ).create( DEBUG ) );
113 
114         OPTIONS.addOption(
115             OptionBuilder.withLongOpt( "errors" ).withDescription( "Produce execution error messages" ).create(
116                 ERRORS ) );
117 
118         OPTIONS.addOption( OptionBuilder.withLongOpt( "reactor" ).withDescription(
119             "Execute goals for project found in the reactor" ).create( REACTOR ) );
120 
121         OPTIONS.addOption(
122             OptionBuilder.withLongOpt( "non-recursive" ).withDescription( "Do not recurse into sub-projects" ).create(
123                 NON_RECURSIVE ) );
124 
125         OPTIONS.addOption( OptionBuilder.withLongOpt( "update-snapshots" ).withDescription(
126             "Forces a check for updated releases and snapshots on remote repositories" ).create( UPDATE_SNAPSHOTS ) );
127 
128         OPTIONS.addOption( OptionBuilder.withLongOpt( "activate-profiles" ).withDescription(
129             "Comma-delimited list of profiles to activate" ).hasArg().create( ACTIVATE_PROFILES ) );
130 
131         OPTIONS.addOption( OptionBuilder.withLongOpt( "strict-checksums" ).withDescription(
132             "Fail the build if checksums don't match" ).create( CHECKSUM_FAILURE_POLICY ) );
133 
134         OPTIONS.addOption(
135             OptionBuilder.withLongOpt( "lax-checksums" ).withDescription( "Warn if checksums don't match" ).create(
136                 CHECKSUM_WARNING_POLICY ) );
137 
138         OPTIONS.addOption( OptionBuilder.withLongOpt( "settings" ).withDescription(
139             "Alternate path for the user settings file" ).hasArg().create( ALTERNATE_USER_SETTINGS ) );
140 
141         OPTIONS.addOption( OptionBuilder.withLongOpt( "global-settings" ).withDescription(
142             " Alternate path for the global settings file" ).hasArg().create( ALTERNATE_GLOBAL_SETTINGS ) );
143 
144         OPTIONS.addOption( OptionBuilder.withLongOpt( "fail-fast" ).withDescription(
145             "Stop at first failure in reactorized builds" ).create( FAIL_FAST ) );
146 
147         OPTIONS.addOption( OptionBuilder.withLongOpt( "fail-at-end" ).withDescription(
148             "Only fail the build afterwards; allow all non-impacted builds to continue" ).create( FAIL_AT_END ) );
149 
150         OPTIONS.addOption( OptionBuilder.withLongOpt( "fail-never" ).withDescription(
151             "NEVER fail the build, regardless of project result" ).create( FAIL_NEVER ) );
152         
153         OPTIONS.addOption( OptionBuilder.withLongOpt( "file" ).withDescription( 
154             "Force the use of an alternate POM file." ).hasArg().create( ALTERNATE_POM_FILE ) );
155         
156         OPTIONS.addOption( OptionBuilder.withLongOpt( "threads" ).withDescription( 
157             "Thread count, for instance 2.0C where C is core multiplied" ).hasArg().create( THREADS ) );
158         
159         OPTIONS.addOption( OptionBuilder.withLongOpt( "batch-mode" ).withDescription( 
160             "Run in non-interactive (batch) mode" ).create( BATCH_MODE ) );
161         
162         OPTIONS.addOption( OptionBuilder.withLongOpt( "toolchains" ).withDescription( 
163             "Alternate path for the user toolchains file" ).hasArg().create( ALTERNATE_USER_TOOLCHAINS ) );
164     }
165 
166     // TODO: Configuring an invocation request from a command line could as well be part of the Invoker API
167     /**
168      * <p>setupRequest.</p>
169      *
170      * @param req a {@link org.apache.maven.shared.invoker.InvocationRequest} object
171      * @param bridge a {@link org.apache.maven.shared.invoker.InvokerLogger} object
172      * @param additionalArguments a {@link java.lang.String} object
173      * @throws org.apache.maven.shared.release.exec.MavenExecutorException if any.
174      */
175     protected void setupRequest( InvocationRequest req,
176                                  InvokerLogger bridge,
177                                String additionalArguments )
178         throws MavenExecutorException
179     {
180         try
181         {
182             String[] args = CommandLineUtils.translateCommandline( additionalArguments );
183             CommandLine cli = new PosixParser().parse( OPTIONS, args );
184 
185             if ( cli.hasOption( SET_SYSTEM_PROPERTY ) )
186             {
187                 String[] properties = cli.getOptionValues( SET_SYSTEM_PROPERTY );
188                 Properties props = new Properties();
189                 for ( int i = 0; i < properties.length; i++ )
190                 {
191                     String property = properties[i];
192                     String name, value;
193                     int sep = property.indexOf( "=" );
194                     if ( sep <= 0 )
195                     {
196                         name = property.trim();
197                         value = "true";
198                     }
199                     else
200                     {
201                         name = property.substring( 0, sep ).trim();
202                         value = property.substring( sep + 1 ).trim();
203                     }
204                     props.setProperty( name, value );
205                 }
206 
207                 req.setProperties( props );
208             }
209 
210             if ( cli.hasOption( OFFLINE ) )
211             {
212                 req.setOffline( true );
213             }
214 
215             if ( cli.hasOption( QUIET ) )
216             {
217                 // TODO: setQuiet() currently not supported by InvocationRequest
218                 req.setDebug( false );
219             }
220             else if ( cli.hasOption( DEBUG ) )
221             {
222                 req.setDebug( true );
223             }
224             else if ( cli.hasOption( ERRORS ) )
225             {
226                 req.setShowErrors( true );
227             }
228 
229             if ( cli.hasOption( REACTOR ) )
230             {
231                 req.setRecursive( true );
232             }
233             else if ( cli.hasOption( NON_RECURSIVE ) )
234             {
235                 req.setRecursive( false );
236             }
237 
238             if ( cli.hasOption( UPDATE_SNAPSHOTS ) )
239             {
240                 req.setUpdateSnapshots( true );
241             }
242 
243             if ( cli.hasOption( ACTIVATE_PROFILES ) )
244             {
245                 String[] profiles = cli.getOptionValues( ACTIVATE_PROFILES );
246                 
247                 if ( profiles != null )
248                 {
249                     req.setProfiles( Arrays.asList( profiles ) );
250                 }
251             }
252 
253             if ( cli.hasOption( CHECKSUM_FAILURE_POLICY ) )
254             {
255                 req.setGlobalChecksumPolicy( InvocationRequest.CHECKSUM_POLICY_FAIL );
256             }
257             else if ( cli.hasOption( CHECKSUM_WARNING_POLICY ) )
258             {
259                 req.setGlobalChecksumPolicy( InvocationRequest.CHECKSUM_POLICY_WARN );
260             }
261 
262             if ( cli.hasOption( ALTERNATE_USER_SETTINGS ) )
263             {
264                 req.setUserSettingsFile( new File( cli.getOptionValue( ALTERNATE_USER_SETTINGS ) ) );
265             }
266             
267             if ( cli.hasOption( ALTERNATE_GLOBAL_SETTINGS ) )
268             {
269                 req.setGlobalSettingsFile( new File( cli.getOptionValue( ALTERNATE_GLOBAL_SETTINGS ) ) );
270             }
271 
272             if ( cli.hasOption( FAIL_AT_END ) )
273             {
274                 req.setFailureBehavior( InvocationRequest.REACTOR_FAIL_AT_END );
275             }
276             else if ( cli.hasOption( FAIL_FAST ) )
277             {
278                 req.setFailureBehavior( InvocationRequest.REACTOR_FAIL_FAST );
279             }
280             if ( cli.hasOption( FAIL_NEVER ) )
281             {
282                 req.setFailureBehavior( InvocationRequest.REACTOR_FAIL_NEVER );
283             }
284             if ( cli.hasOption( ALTERNATE_POM_FILE ) )
285             {
286                 if ( req.getPomFileName() != null )
287                 {
288                     getLogger().info( "pomFileName is already set, ignoring the -f argument" );
289                 }
290                 else
291                 {
292                     req.setPomFileName( cli.getOptionValue( ALTERNATE_POM_FILE ) );
293                 }
294             }
295             
296             if ( cli.hasOption( THREADS ) )
297             {
298                 req.setThreads( cli.getOptionValue( THREADS ) );
299             }
300             
301             if ( cli.hasOption( BATCH_MODE ) )
302             {
303                 req.setInteractive( false );
304             }
305             
306             if ( cli.hasOption( ALTERNATE_USER_TOOLCHAINS ) )
307             {
308                 req.setToolchainsFile( new File( cli.getOptionValue( ALTERNATE_USER_TOOLCHAINS ) ) );
309             }
310             
311         }
312         catch ( Exception e )
313         {
314             throw new MavenExecutorException( "Failed to re-parse additional arguments for Maven invocation.", e );
315         }
316     }
317 
318     @Override
319     public void executeGoals( File workingDirectory, List<String> goals, ReleaseEnvironment releaseEnvironment,
320                               boolean interactive, String additionalArguments, String pomFileName,
321                               ReleaseResult result )
322         throws MavenExecutorException
323     {
324         InvocationOutputHandler handler = getOutputHandler();
325         InvokerLogger bridge = getInvokerLogger();
326 
327         File mavenPath = null;
328         // if null we use the current one
329         if ( releaseEnvironment.getMavenHome() != null )
330         {
331             mavenPath = releaseEnvironment.getMavenHome();
332         }
333         else
334         {
335             String mavenHome = System.getProperty( "maven.home" );
336             if ( mavenHome == null )
337             {
338                 mavenHome = System.getenv( "MAVEN_HOME" );
339             }
340             if ( mavenHome == null )
341             {
342                 mavenHome = System.getenv( "M2_HOME" );
343             }
344             mavenPath = mavenHome == null ? null : new File( mavenHome );
345         }
346         Invoker invoker =
347             new DefaultInvoker().setMavenHome( mavenPath ).setLogger( bridge )
348                 .setOutputHandler( handler ).setErrorHandler( handler );
349 
350         InvocationRequest req =
351             new DefaultInvocationRequest().setDebug( getLogger().isDebugEnabled() )
352                 .setBaseDirectory( workingDirectory ).setInteractive( interactive );
353 
354         if ( pomFileName != null )
355         {
356             req.setPomFileName( pomFileName );
357         }
358 
359         File settingsFile = null;
360         if ( releaseEnvironment.getSettings() != null )
361         {
362             // Have to serialize to a file as if Maven is embedded, there may not actually be a settings.xml on disk
363             try
364             {
365                 settingsFile = File.createTempFile( "release-settings", ".xml" );
366                 SettingsXpp3Writer writer = getSettingsWriter();
367                 
368                 try ( FileWriter fileWriter = new FileWriter( settingsFile ) )
369                 {
370                     writer.write( fileWriter, encryptSettings( releaseEnvironment.getSettings() ) );
371                 }
372                 req.setUserSettingsFile( settingsFile );
373             }
374             catch ( IOException e )
375             {
376                 throw new MavenExecutorException( "Could not create temporary file for release settings.xml", e );
377             }
378         }
379         try
380         {
381             File localRepoDir = releaseEnvironment.getLocalRepositoryDirectory();
382             if ( localRepoDir != null )
383             {
384                 req.setLocalRepositoryDirectory( localRepoDir );
385             }
386 
387             setupRequest( req, bridge, additionalArguments );
388 
389             req.setGoals( goals );
390 
391             try
392             {
393                 InvocationResult invocationResult = invoker.execute( req );
394 
395                 if ( invocationResult.getExecutionException() != null )
396                 {
397                     throw new MavenExecutorException( "Error executing Maven.",
398                                                       invocationResult.getExecutionException() );
399                 }
400                 if ( invocationResult.getExitCode() != 0 )
401                 {
402                     throw new MavenExecutorException(
403                         "Maven execution failed, exit code: \'" + invocationResult.getExitCode() + "\'",
404                         invocationResult.getExitCode() );
405                 }
406             }
407             catch ( MavenInvocationException e )
408             {
409                 throw new MavenExecutorException( "Failed to invoke Maven build.", e );
410             }
411         }
412         finally
413         {
414             if ( settingsFile != null && settingsFile.exists() && !settingsFile.delete() )
415             {
416                 settingsFile.deleteOnExit();
417             }
418         }
419     }
420 
421     /**
422      * <p>getInvokerLogger.</p>
423      *
424      * @return a {@link org.apache.maven.shared.invoker.InvokerLogger} object
425      */
426     protected InvokerLogger getInvokerLogger()
427     {
428         return new LoggerBridge( getLogger() );
429     }
430 
431     /**
432      * <p>getOutputHandler.</p>
433      *
434      * @return a {@link org.apache.maven.shared.invoker.InvocationOutputHandler} object
435      */
436     protected InvocationOutputHandler getOutputHandler()
437     {
438         return new Handler( getLogger() );
439     }
440 
441     private static final class Handler
442         implements InvocationOutputHandler
443     {
444         private Logger logger;
445 
446         Handler( Logger logger )
447         {
448             this.logger = logger;
449         }
450 
451         public void consumeLine( String line )
452         {
453             logger.info( line );
454         }
455     }
456 
457     private static final class LoggerBridge
458         implements InvokerLogger
459     {
460 
461         private Logger logger;
462 
463         LoggerBridge( Logger logger )
464         {
465             this.logger = logger;
466         }
467 
468         @Override
469         public void debug( String message, Throwable error )
470         {
471             logger.debug( message, error );
472         }
473 
474         @Override
475         public void debug( String message )
476         {
477             logger.debug( message );
478         }
479 
480         @Override
481         public void error( String message, Throwable error )
482         {
483             logger.error( message, error );
484         }
485 
486         @Override
487         public void error( String message )
488         {
489             logger.error( message );
490         }
491 
492         @Override
493         public void fatalError( String message, Throwable error )
494         {
495             logger.fatalError( message, error );
496         }
497 
498         @Override
499         public void fatalError( String message )
500         {
501             logger.fatalError( message );
502         }
503 
504         @Override
505         public int getThreshold()
506         {
507             return logger.getThreshold();
508         }
509 
510         @Override
511         public void info( String message, Throwable error )
512         {
513             logger.info( message, error );
514         }
515 
516         @Override
517         public void info( String message )
518         {
519             logger.info( message );
520         }
521 
522         @Override
523         public boolean isDebugEnabled()
524         {
525             return logger.isDebugEnabled();
526         }
527 
528         @Override
529         public boolean isErrorEnabled()
530         {
531             return logger.isErrorEnabled();
532         }
533 
534         @Override
535         public boolean isFatalErrorEnabled()
536         {
537             return logger.isFatalErrorEnabled();
538         }
539 
540         @Override
541         public boolean isInfoEnabled()
542         {
543             return logger.isInfoEnabled();
544         }
545 
546         @Override
547         public boolean isWarnEnabled()
548         {
549             return logger.isWarnEnabled();
550         }
551 
552         @Override
553         public void setThreshold( int level )
554         {
555             // NOTE:
556             // logger.setThreadhold( level )
557             // is not supported in plexus-container-default:1.0-alpha-9 as used in Maven 2.x
558         }
559 
560         @Override
561         public void warn( String message, Throwable error )
562         {
563             logger.warn( message, error );
564         }
565 
566         @Override
567         public void warn( String message )
568         {
569             logger.warn( message );
570         }
571     }
572 
573 }