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