View Javadoc

1   package org.apache.maven.plugin;
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.util.ArrayList;
24  import java.util.HashSet;
25  import java.util.Iterator;
26  import java.util.LinkedHashMap;
27  import java.util.List;
28  import java.util.Map;
29  import java.util.Set;
30  
31  import org.apache.maven.execution.MavenSession;
32  import org.apache.maven.toolchain.Toolchain;
33  import org.apache.maven.toolchain.ToolchainManager;
34  import org.codehaus.plexus.compiler.Compiler;
35  import org.codehaus.plexus.compiler.CompilerConfiguration;
36  import org.codehaus.plexus.compiler.CompilerError;
37  import org.codehaus.plexus.compiler.CompilerException;
38  import org.codehaus.plexus.compiler.CompilerOutputStyle;
39  import org.codehaus.plexus.compiler.manager.CompilerManager;
40  import org.codehaus.plexus.compiler.manager.NoSuchCompilerException;
41  import org.codehaus.plexus.compiler.util.scan.InclusionScanException;
42  import org.codehaus.plexus.compiler.util.scan.SourceInclusionScanner;
43  import org.codehaus.plexus.compiler.util.scan.mapping.SingleTargetSourceMapping;
44  import org.codehaus.plexus.compiler.util.scan.mapping.SourceMapping;
45  import org.codehaus.plexus.compiler.util.scan.mapping.SuffixMapping;
46  import org.codehaus.plexus.util.ReaderFactory;
47  import org.codehaus.plexus.util.StringUtils;
48  
49  /**
50   * TODO: At least one step could be optimized, currently the plugin will do two
51   * scans of all the source code if the compiler has to have the entire set of
52   * sources. This is currently the case for at least the C# compiler and most
53   * likely all the other .NET compilers too.
54   *
55   * @author others
56   * @author <a href="mailto:trygvis@inamo.no">Trygve Laugst&oslash;l</a>
57   * @version $Id: AbstractCompilerMojo.html 816611 2012-05-08 13:04:26Z hboutemy $
58   * @since 2.0
59   */
60  public abstract class AbstractCompilerMojo
61      extends AbstractMojo
62  {
63      // ----------------------------------------------------------------------
64      // Configurables
65      // ----------------------------------------------------------------------
66  
67      /**
68       * Indicates whether the build will continue even if there are compilation errors; defaults to true.
69       *
70       * @parameter expression="${maven.compiler.failOnError}" default-value="true"
71       * @since 2.0.2
72       */
73      private boolean failOnError = true;
74  
75      /**
76       * Set to true to include debugging information in the compiled class files.
77       *
78       * @parameter expression="${maven.compiler.debug}" default-value="true"
79       */
80      private boolean debug = true;
81  
82      /**
83       * Set to true to show messages about what the compiler is doing.
84       *
85       * @parameter expression="${maven.compiler.verbose}" default-value="false"
86       */
87      private boolean verbose;
88  
89      /**
90       * Sets whether to show source locations where deprecated APIs are used.
91       *
92       * @parameter expression="${maven.compiler.showDeprecation}" default-value="false"
93       */
94      private boolean showDeprecation;
95  
96      /**
97       * Set to true to optimize the compiled code using the compiler's optimization methods.
98       *
99       * @parameter expression="${maven.compiler.optimize}" default-value="false"
100      */
101     private boolean optimize;
102 
103     /**
104      * Set to true to show compilation warnings.
105      *
106      * @parameter expression="${maven.compiler.showWarnings}" default-value="false"
107      */
108     private boolean showWarnings;
109 
110     /**
111      * The -source argument for the Java compiler.
112      *
113      * @parameter expression="${maven.compiler.source}"
114      */
115     protected String source;
116 
117     /**
118      * The -target argument for the Java compiler.
119      *
120      * @parameter expression="${maven.compiler.target}"
121      */
122     protected String target;
123 
124     /**
125      * The -encoding argument for the Java compiler.
126      *
127      * @parameter expression="${encoding}" default-value="${project.build.sourceEncoding}"
128      */
129     private String encoding;
130 
131     /**
132      * Sets the granularity in milliseconds of the last modification
133      * date for testing whether a source needs recompilation.
134      *
135      * @parameter expression="${lastModGranularityMs}" default-value="0"
136      */
137     private int staleMillis;
138 
139     /**
140      * The compiler id of the compiler to use. See this
141      * <a href="non-javac-compilers.html">guide</a> for more information.
142      *
143      * @parameter expression="${maven.compiler.compilerId}" default-value="javac"
144      */
145     private String compilerId;
146 
147     /**
148      * Version of the compiler to use, ex. "1.3", "1.5", if fork is set to true.
149      *
150      * @parameter expression="${maven.compiler.compilerVersion}"
151      */
152     private String compilerVersion;
153 
154     /**
155      * Allows running the compiler in a separate process.
156      * If "false" it uses the built in compiler, while if "true" it will use an executable.
157      *
158      * @parameter expression="${maven.compiler.fork}" default-value="false"
159      */
160     private boolean fork;
161 
162     /**
163      * Initial size, in megabytes, of the memory allocation pool, ex. "64", "64m"
164      * if fork is set to true.
165      *
166      * @parameter expression="${maven.compiler.meminitial}"
167      * @since 2.0.1
168      */
169     private String meminitial;
170 
171     /**
172      * Sets the maximum size, in megabytes, of the memory allocation pool, ex. "128", "128m"
173      * if fork is set to true.
174      *
175      * @parameter expression="${maven.compiler.maxmem}"
176      * @since 2.0.1
177      */
178     private String maxmem;
179 
180     /**
181      * Sets the executable of the compiler to use when fork is true.
182      *
183      * @parameter expression="${maven.compiler.executable}"
184      */
185     private String executable;
186 
187     /**
188      * <p>
189      * Sets the arguments to be passed to the compiler (prepending a dash) if fork is set to true.
190      * </p>
191      * <p>
192      * This is because the list of valid arguments passed to a Java compiler
193      * varies based on the compiler version.
194      * </p>
195      *
196      * @parameter
197      * @since 2.0.1
198      */
199     protected Map compilerArguments;
200 
201     /**
202      * <p>
203      * Sets the unformatted argument string to be passed to the compiler if fork is set to true.
204      * </p>
205      * <p>
206      * This is because the list of valid arguments passed to a Java compiler
207      * varies based on the compiler version.
208      * </p>
209      *
210      * @parameter
211      */
212     protected String compilerArgument;
213 
214     /**
215      * Sets the name of the output file when compiling a set of
216      * sources to a single file.
217      *
218      * @parameter expression="${project.build.finalName}"
219      */
220     private String outputFileName;
221     
222     /**
223      * Keyword list to be appended to the -g  command-line switch. Legal values are none or a comma-separated list of the following keywords: lines, vars, and source.
224      * If debuglevel is not specified, by default, nothing will be appended to -g. If debug is not turned on, this attribute will be ignored.
225      *
226      * @parameter expression="${maven.compiler.debuglevel}"
227      * @since 2.1
228      */
229     private String debuglevel;    
230 
231     /** @component */
232     private ToolchainManager toolchainManager;
233     
234     // ----------------------------------------------------------------------
235     // Read-only parameters
236     // ----------------------------------------------------------------------
237 
238     /**
239      * The directory to run the compiler from if fork is true.
240      *
241      * @parameter default-value="${basedir}"
242      * @required
243      * @readonly
244      */
245     private File basedir;
246 
247     /**
248      * The target directory of the compiler if fork is true.
249      *
250      * @parameter default-value="${project.build.directory}"
251      * @required
252      * @readonly
253      */
254     private File buildDirectory;
255 
256     /**
257      * Plexus compiler manager.
258      *
259      * @component
260      */
261     private CompilerManager compilerManager;
262     
263     /**
264      * The current build session instance. This is used for
265      * toolchain manager API calls.
266      *
267      * @parameter default-value="${session}"
268      * @required
269      * @readonly
270      */
271     private MavenSession session;
272 
273     protected abstract SourceInclusionScanner getSourceInclusionScanner( int staleMillis );
274 
275     protected abstract SourceInclusionScanner getSourceInclusionScanner( String inputFileEnding );
276 
277     protected abstract List getClasspathElements();
278 
279     protected abstract List getCompileSourceRoots();
280 
281     protected abstract File getOutputDirectory();
282     
283     protected abstract String getSource();
284     
285     protected abstract String getTarget();
286     
287     protected abstract String getCompilerArgument();
288     
289     protected abstract Map getCompilerArguments();
290 
291     public void execute()
292         throws MojoExecutionException, CompilationFailureException
293     {
294         // ----------------------------------------------------------------------
295         // Look up the compiler. This is done before other code than can
296         // cause the mojo to return before the lookup is done possibly resulting
297         // in misconfigured POMs still building.
298         // ----------------------------------------------------------------------
299 
300         Compiler compiler;
301 
302         getLog().debug( "Using compiler '" + compilerId + "'." );
303 
304         try
305         {
306             compiler = compilerManager.getCompiler( compilerId );
307         }
308         catch ( NoSuchCompilerException e )
309         {
310             throw new MojoExecutionException( "No such compiler '" + e.getCompilerId() + "'." );
311         }
312         
313         //-----------toolchains start here ----------------------------------
314         //use the compilerId as identifier for toolchains as well.
315         Toolchain tc = getToolchain();
316         if ( tc != null ) 
317         {
318             getLog().info( "Toolchain in compiler-plugin: " + tc );
319             if ( executable  != null ) 
320             { 
321                 getLog().warn( "Toolchains are ignored, 'executable' parameter is set to " + executable );
322             } 
323             else 
324             {
325                 fork = true;
326                 //TODO somehow shaky dependency between compilerId and tool executable.
327                 executable = tc.findTool( compilerId );
328             }
329         }
330         // ----------------------------------------------------------------------
331         //
332         // ----------------------------------------------------------------------
333 
334         List compileSourceRoots = removeEmptyCompileSourceRoots( getCompileSourceRoots() );
335 
336         if ( compileSourceRoots.isEmpty() )
337         {
338             getLog().info( "No sources to compile" );
339 
340             return;
341         }
342 
343         if ( getLog().isDebugEnabled() )
344         {
345             getLog().debug( "Source directories: " + compileSourceRoots.toString().replace( ',', '\n' ) );
346             getLog().debug( "Classpath: " + getClasspathElements().toString().replace( ',', '\n' ) );
347             getLog().debug( "Output directory: " + getOutputDirectory() );
348         }
349 
350         // ----------------------------------------------------------------------
351         // Create the compiler configuration
352         // ----------------------------------------------------------------------
353 
354         CompilerConfiguration compilerConfiguration = new CompilerConfiguration();
355 
356         compilerConfiguration.setOutputLocation( getOutputDirectory().getAbsolutePath() );
357 
358         compilerConfiguration.setClasspathEntries( getClasspathElements() );
359 
360         compilerConfiguration.setSourceLocations( compileSourceRoots );
361 
362         compilerConfiguration.setOptimize( optimize );
363 
364         compilerConfiguration.setDebug( debug );
365 
366         if ( debug && StringUtils.isNotEmpty( debuglevel ) )
367         {
368             String[] split = StringUtils.split( debuglevel, "," );
369             for ( int i = 0; i < split.length; i++ )
370             {
371                 if ( !( split[i].equalsIgnoreCase( "none" ) || split[i].equalsIgnoreCase( "lines" )
372                     || split[i].equalsIgnoreCase( "vars" ) || split[i].equalsIgnoreCase( "source" ) ) )
373                 {
374                     throw new IllegalArgumentException( "The specified debug level: '" + split[i]
375                         + "' is unsupported. " + "Legal values are 'none', 'lines', 'vars', and 'source'." );
376                 }
377             }
378             compilerConfiguration.setDebugLevel( debuglevel );
379         }        
380         
381         compilerConfiguration.setVerbose( verbose );
382 
383         compilerConfiguration.setShowWarnings( showWarnings );
384 
385         compilerConfiguration.setShowDeprecation( showDeprecation );
386 
387         compilerConfiguration.setSourceVersion( getSource() );
388 
389         compilerConfiguration.setTargetVersion( getTarget() );
390 
391         compilerConfiguration.setSourceEncoding( encoding );
392         
393         Map effectiveCompilerArguments = getCompilerArguments();
394 
395         String effectiveCompilerArgument = getCompilerArgument();
396 
397         if ( ( effectiveCompilerArguments != null ) || ( effectiveCompilerArgument != null ) )
398         {
399             LinkedHashMap cplrArgsCopy = new LinkedHashMap();
400             if ( effectiveCompilerArguments != null )
401             {
402                 for ( Iterator i = effectiveCompilerArguments.entrySet().iterator(); i.hasNext(); )
403                 {
404                     Map.Entry me = (Map.Entry) i.next();
405                     String key = (String) me.getKey();
406                     String value = (String) me.getValue();
407                     if ( !key.startsWith( "-" ) )
408                     {
409                         key = "-" + key;
410                     }
411                     cplrArgsCopy.put( key, value );
412                 }
413             }
414             if ( !StringUtils.isEmpty( effectiveCompilerArgument ) )
415             {
416                 cplrArgsCopy.put( effectiveCompilerArgument, null );
417             }
418             compilerConfiguration.setCustomCompilerArguments( cplrArgsCopy );
419         }
420 
421         compilerConfiguration.setFork( fork );
422 
423         if ( fork )
424         {
425             if ( !StringUtils.isEmpty( meminitial ) )
426             {
427                 String value = getMemoryValue( meminitial );
428 
429                 if ( value != null )
430                 {
431                     compilerConfiguration.setMeminitial( value );
432                 }
433                 else
434                 {
435                     getLog().info( "Invalid value for meminitial '" + meminitial + "'. Ignoring this option." );
436                 }
437             }
438 
439             if ( !StringUtils.isEmpty( maxmem ) )
440             {
441                 String value = getMemoryValue( maxmem );
442 
443                 if ( value != null )
444                 {
445                     compilerConfiguration.setMaxmem( value );
446                 }
447                 else
448                 {
449                     getLog().info( "Invalid value for maxmem '" + maxmem + "'. Ignoring this option." );
450                 }
451             }
452         }
453 
454         compilerConfiguration.setExecutable( executable );
455 
456         compilerConfiguration.setWorkingDirectory( basedir );
457 
458         compilerConfiguration.setCompilerVersion( compilerVersion );
459 
460         compilerConfiguration.setBuildDirectory( buildDirectory );
461 
462         compilerConfiguration.setOutputFileName( outputFileName );
463 
464         // TODO: have an option to always compile (without need to clean)
465         Set staleSources;
466 
467         boolean canUpdateTarget;
468 
469         try
470         {
471             staleSources =
472                 computeStaleSources( compilerConfiguration, compiler, getSourceInclusionScanner( staleMillis ) );
473 
474             canUpdateTarget = compiler.canUpdateTarget( compilerConfiguration );
475 
476             if ( compiler.getCompilerOutputStyle().equals( CompilerOutputStyle.ONE_OUTPUT_FILE_FOR_ALL_INPUT_FILES ) &&
477                 !canUpdateTarget )
478             {
479                 getLog().info( "RESCANNING!" );
480                 // TODO: This second scan for source files is sub-optimal
481                 String inputFileEnding = compiler.getInputFileEnding( compilerConfiguration );
482 
483                 Set sources = computeStaleSources( compilerConfiguration, compiler,
484                                                    getSourceInclusionScanner( inputFileEnding ) );
485 
486                 compilerConfiguration.setSourceFiles( sources );
487             }
488             else
489             {
490                 compilerConfiguration.setSourceFiles( staleSources );
491             }
492         }
493         catch ( CompilerException e )
494         {
495             throw new MojoExecutionException( "Error while computing stale sources.", e );
496         }
497 
498         if ( staleSources.isEmpty() )
499         {
500             getLog().info( "Nothing to compile - all classes are up to date" );
501 
502             return;
503         }
504 
505         // ----------------------------------------------------------------------
506         // Dump configuration
507         // ----------------------------------------------------------------------
508 
509         if ( getLog().isDebugEnabled() )
510         {
511             getLog().debug( "Classpath:" );
512 
513             for ( Iterator it = getClasspathElements().iterator(); it.hasNext(); )
514             {
515                 String s = (String) it.next();
516 
517                 getLog().debug( " " + s );
518             }
519 
520             getLog().debug( "Source roots:" );
521 
522             for ( Iterator it = getCompileSourceRoots().iterator(); it.hasNext(); )
523             {
524                 String root = (String) it.next();
525 
526                 getLog().debug( " " + root );
527             }
528 
529             if ( fork )
530             {
531                 try
532                 {
533                     if ( compilerConfiguration.getExecutable() != null )
534                     {
535                         getLog().debug( "Excutable: " );
536                         getLog().debug( " " + compilerConfiguration.getExecutable() );
537                     }
538 
539                     String[] cl = compiler.createCommandLine( compilerConfiguration );
540                     if ( cl != null && cl.length > 0 )
541                     {
542                         StringBuffer sb = new StringBuffer();
543                         sb.append( cl[0] );
544                         for ( int i = 1; i < cl.length; i++ )
545                         {
546                             sb.append( " " );
547                             sb.append( cl[i] );
548                         }
549                         getLog().debug( "Command line options:" );
550                         getLog().debug( sb );
551                     }
552                 }
553                 catch ( CompilerException ce )
554                 {
555                     getLog().debug( ce );
556                 }
557             }
558         }
559 
560         // ----------------------------------------------------------------------
561         // Compile!
562         // ----------------------------------------------------------------------
563 
564         if ( StringUtils.isEmpty( compilerConfiguration.getSourceEncoding() ) )
565         {
566             getLog().warn(
567                            "File encoding has not been set, using platform encoding " + ReaderFactory.FILE_ENCODING
568                                + ", i.e. build is platform dependent!" );
569         }
570 
571         List messages;
572 
573         try
574         {
575             messages = compiler.compile( compilerConfiguration );
576         }
577         catch ( Exception e )
578         {
579             // TODO: don't catch Exception
580             throw new MojoExecutionException( "Fatal error compiling", e );
581         }
582 
583         boolean compilationError = false;
584 
585         for ( Iterator i = messages.iterator(); i.hasNext(); )
586         {
587             CompilerError message = (CompilerError) i.next();
588 
589             if ( message.isError() )
590             {
591                 compilationError = true;
592                 break;
593             }
594         }
595 
596         if ( compilationError && failOnError )
597         {
598             getLog().info( "-------------------------------------------------------------" );
599             getLog().error( "COMPILATION ERROR : " );
600             getLog().info( "-------------------------------------------------------------" );
601             if ( messages != null )
602             {
603                 for ( Iterator i = messages.iterator(); i.hasNext(); )
604                 {
605                     CompilerError message = (CompilerError) i.next();
606 
607                     getLog().error( message.toString() );
608 
609                 }
610                 getLog().info( messages.size() + ( ( messages.size() > 1 ) ? " errors " : "error" ) );
611                 getLog().info( "-------------------------------------------------------------" );
612             }
613             throw new CompilationFailureException( messages );
614         }
615         else
616         {
617             for ( Iterator i = messages.iterator(); i.hasNext(); )
618             {
619                 CompilerError message = (CompilerError) i.next();
620 
621                 getLog().warn( message.toString() );
622             }
623         }
624     }
625 
626     private String getMemoryValue( String setting )
627     {
628         String value = null;
629 
630         // Allow '128' or '128m'
631         if ( isDigits( setting ) )
632         {
633             value = setting + "m";
634         }
635         else
636         {
637             if ( ( isDigits( setting.substring( 0, setting.length() - 1 ) ) ) &&
638                 ( setting.toLowerCase().endsWith( "m" ) ) )
639             {
640                 value = setting;
641             }
642         }
643         return value;
644     }
645 
646     //TODO remove the part with ToolchainManager lookup once we depend on
647     //3.0.9 (have it as prerequisite). Define as regular component field then.
648     private Toolchain getToolchain()
649     {
650         Toolchain tc = null;
651         if ( toolchainManager != null )
652         {
653             tc = toolchainManager.getToolchainFromBuildContext( "jdk", session );
654         }
655         return tc;
656     }
657 
658     private boolean isDigits( String string )
659     {
660         for ( int i = 0; i < string.length(); i++ )
661         {
662             if ( !Character.isDigit( string.charAt( i ) ) )
663             {
664                 return false;
665             }
666         }
667         return true;
668     }
669 
670     private Set computeStaleSources( CompilerConfiguration compilerConfiguration, Compiler compiler,
671                                      SourceInclusionScanner scanner )
672         throws MojoExecutionException, CompilerException
673     {
674         CompilerOutputStyle outputStyle = compiler.getCompilerOutputStyle();
675 
676         SourceMapping mapping;
677 
678         File outputDirectory;
679 
680         if ( outputStyle == CompilerOutputStyle.ONE_OUTPUT_FILE_PER_INPUT_FILE )
681         {
682             mapping = new SuffixMapping( compiler.getInputFileEnding( compilerConfiguration ), compiler
683                 .getOutputFileEnding( compilerConfiguration ) );
684 
685             outputDirectory = getOutputDirectory();
686         }
687         else if ( outputStyle == CompilerOutputStyle.ONE_OUTPUT_FILE_FOR_ALL_INPUT_FILES )
688         {
689             mapping = new SingleTargetSourceMapping( compiler.getInputFileEnding( compilerConfiguration ), compiler
690                 .getOutputFile( compilerConfiguration ) );
691 
692             outputDirectory = buildDirectory;
693         }
694         else
695         {
696             throw new MojoExecutionException( "Unknown compiler output style: '" + outputStyle + "'." );
697         }
698 
699         scanner.addSourceMapping( mapping );
700 
701         Set staleSources = new HashSet();
702 
703         for ( Iterator it = getCompileSourceRoots().iterator(); it.hasNext(); )
704         {
705             String sourceRoot = (String) it.next();
706 
707             File rootFile = new File( sourceRoot );
708 
709             if ( !rootFile.isDirectory() )
710             {
711                 continue;
712             }
713 
714             try
715             {
716                 staleSources.addAll( scanner.getIncludedSources( rootFile, outputDirectory ) );
717             }
718             catch ( InclusionScanException e )
719             {
720                 throw new MojoExecutionException(
721                     "Error scanning source root: \'" + sourceRoot + "\' " + "for stale files to recompile.", e );
722             }
723         }
724 
725         return staleSources;
726     }
727 
728     /**
729      * @todo also in ant plugin. This should be resolved at some point so that it does not need to
730      * be calculated continuously - or should the plugins accept empty source roots as is?
731      */
732     private static List removeEmptyCompileSourceRoots( List compileSourceRootsList )
733     {
734         List newCompileSourceRootsList = new ArrayList();
735         if ( compileSourceRootsList != null )
736         {
737             // copy as I may be modifying it
738             for ( Iterator i = compileSourceRootsList.iterator(); i.hasNext(); )
739             {
740                 String srcDir = (String) i.next();
741                 if ( !newCompileSourceRootsList.contains( srcDir ) && new File( srcDir ).exists() )
742                 {
743                     newCompileSourceRootsList.add( srcDir );
744                 }
745             }
746         }
747         return newCompileSourceRootsList;
748     }
749 }