View Javadoc

1   /*
2    * Copyright 2013 Grzegorz Slowikowski (gslowikowski at gmail dot com)
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *   http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing,
11   * software distributed under the License is distributed on an
12   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
13   * KIND, either express or implied.  See the License for the
14   * specific language governing permissions and limitations
15   * under the License.
16   */
17  
18  package com.google.code.sbt;
19  
20  import java.io.IOException;
21  import java.io.File;
22  import java.util.ArrayList;
23  import java.util.Arrays;
24  import java.util.Collection;
25  import java.util.HashSet;
26  import java.util.Iterator;
27  import java.util.List;
28  import java.util.Map;
29  import java.util.Set;
30  
31  import org.apache.maven.artifact.Artifact;
32  import org.apache.maven.artifact.factory.ArtifactFactory;
33  import org.apache.maven.artifact.repository.ArtifactRepository;
34  import org.apache.maven.artifact.resolver.ArtifactNotFoundException;
35  import org.apache.maven.artifact.resolver.ArtifactResolutionException;
36  import org.apache.maven.artifact.resolver.ArtifactResolver;
37  import org.apache.maven.artifact.resolver.filter.AndArtifactFilter;
38  import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
39  import org.apache.maven.artifact.resolver.filter.ScopeArtifactFilter;
40  import org.apache.maven.plugin.AbstractMojo;
41  import org.apache.maven.plugin.MojoExecutionException;
42  import org.apache.maven.plugin.MojoFailureException;
43  import org.apache.maven.plugins.annotations.Component;
44  import org.apache.maven.plugins.annotations.Parameter;
45  import org.apache.maven.project.MavenProject;
46  import org.apache.maven.project.MavenProjectBuilder;
47  import org.apache.maven.project.ProjectBuildingException;
48  import org.apache.maven.project.artifact.InvalidDependencyVersionException;
49  
50  import org.codehaus.plexus.util.DirectoryScanner;
51  
52  import scala.Option;
53  
54  import com.typesafe.zinc.Compiler;
55  import com.typesafe.zinc.IncOptions;
56  import com.typesafe.zinc.Inputs;
57  import com.typesafe.zinc.Setup;
58  
59  /**
60   * Abstract base class for SBT compilation mojos.
61   * 
62   * @author <a href="mailto:gslowikowski@gmail.com">Grzegorz Slowikowski</a>
63   */
64  public abstract class AbstractSBTCompileMojo
65      extends AbstractMojo
66  {
67      /**
68       * Scala artifacts "groupId".
69       */
70      public static final String SCALA_GROUPID = "org.scala-lang";
71  
72      /**
73       * Scala library "artifactId".
74       */
75      public static final String SCALA_LIBRARY_ARTIFACTID = "scala-library";
76  
77      /**
78       * Scala compiler "artifactId".
79       */
80      public static final String SCALA_COMPILER_ARTIFACTID = "scala-compiler";
81  
82      /**
83       * SBT artifacts "groupId".
84       */
85      public static final String SBT_GROUP_ID = "com.typesafe.sbt";
86  
87      /**
88       * SBT incremental compile "artifactId".
89       */
90      public static final String COMPILER_INTEGRATION_ARTIFACT_ID = "incremental-compiler";
91  
92      /**
93       * SBT compile interface "artifactId".
94       */
95      public static final String COMPILER_INTERFACE_ARTIFACT_ID = "compiler-interface";
96  
97      /**
98       * SBT compile interface sources "classifier".
99       */
100     public static final String COMPILER_INTERFACE_CLASSIFIER = "sources";
101 
102     /**
103      * SBT interface "artifactId".
104      */
105     public static final String XSBTI_ARTIFACT_ID = "sbt-interface";
106 
107     /**
108      * SBT compilation order.
109      */
110     private static final String COMPILE_ORDER = "mixed";
111 
112     /**
113      * Run compilation in forked JVM.
114      */
115     private static final boolean FORK_JAVA = false;
116 
117     /**
118      * SBT version
119      * 
120      * @since 1.0.0
121      */
122     @Parameter( property = "sbt.version", defaultValue = "0.13.0" )
123     private String sbtVersion;
124 
125     /**
126      * The -encoding argument for Scala and Java compilers.
127      * 
128      * @since 1.0.0
129      */
130     @Parameter( property = "project.build.sourceEncoding" )
131     protected String sourceEncoding;
132 
133     /**
134      * Additional parameters for Java compiler.
135      * 
136      * @since 1.0.0
137      */
138     @Parameter( property = "sbt.javacOptions", defaultValue = "-g" )
139     protected String javacOptions;
140 
141     /**
142      * Additional parameters for Scala compiler.
143      * 
144      * @since 1.0.0
145      */
146     @Parameter( property = "sbt.scalacOptions", defaultValue = "-deprecation -unchecked" )
147     protected String scalacOptions;
148 
149     /**
150      * <i>Maven Internal</i>: Project to interact with.
151      */
152     @Component
153     protected MavenProject project;
154 
155     /**
156      * Maven project builder used to resolve artifacts.
157      */
158     @Component
159     protected MavenProjectBuilder mavenProjectBuilder;
160 
161     /**
162      * All projects in the reactor.
163      */
164     @Parameter( defaultValue = "${reactorProjects}", required = true, readonly = true )
165     protected List<MavenProject> reactorProjects;
166 
167     /**
168      * Artifact factory used to look up artifacts in the remote repository.
169      */
170     @Component
171     protected ArtifactFactory factory;
172 
173     /**
174      * Artifact resolver used to resolve artifacts.
175      */
176     @Component
177     protected ArtifactResolver resolver;
178 
179     /**
180      * Location of the local repository.
181      */
182     @Parameter( property = "localRepository", readonly = true, required = true )
183     protected ArtifactRepository localRepo;
184 
185     /**
186      * List of Remote Repositories used by the resolver
187      */
188     @Parameter( property = "project.remoteArtifactRepositories", readonly = true, required = true )
189     protected List<?> remoteRepos;
190 
191     /**
192      * Perform compilation.
193      * 
194      * @throws MojoExecutionException if an unexpected problem occurs.
195      * Throwing this exception causes a "BUILD ERROR" message to be displayed.
196      * @throws MojoFailureException if an expected problem (such as a compilation failure) occurs.
197      * Throwing this exception causes a "BUILD FAILURE" message to be displayed.
198      */
199     public void execute()
200         throws MojoExecutionException, MojoFailureException
201     {
202         if ( "pom".equals( project.getPackaging() ) )
203         {
204             return;
205         }
206 
207         try
208         {
209             long ts = System.currentTimeMillis();
210             internalExecute();
211             long te = System.currentTimeMillis();
212             getLog().debug( String.format( "Mojo execution time: %d ms", te - ts ) );
213         }
214         catch ( IOException e )
215         {
216             throw new MojoExecutionException( "Scala compilation failed", e );
217         }
218     }
219 
220     /**
221      * Actual compilation code, to be overridden.
222      * 
223      * @throws MojoExecutionException if an unexpected problem occurs.
224      * @throws MojoFailureException if an expected problem (such as a compilation failure) occurs.
225      * @throws IOException if an IO exception occurs.
226      */
227     protected void internalExecute()
228         throws MojoExecutionException, MojoFailureException, IOException
229     {
230         List<String> compileSourceRoots = getCompileSourceRoots();
231 
232         if ( compileSourceRoots.isEmpty() )// ?
233         {
234             getLog().info( "No sources to compile" );
235 
236             return;
237         }
238 
239         List<File> sourceRootDirs = new ArrayList<File>( compileSourceRoots.size() );
240         for ( String compileSourceRoot : compileSourceRoots )
241         {
242             sourceRootDirs.add( new File( compileSourceRoot ) );
243         }
244 
245         List<File> sourceFiles = getSourceFiles( sourceRootDirs );
246         if ( sourceFiles.isEmpty() )
247         {
248             getLog().info( "No sources to compile" );
249 
250             return;
251         }
252 
253         try
254         {
255             Artifact scalaLibraryArtifact =
256                 getDependencyArtifact( project.getArtifacts(), SCALA_GROUPID, SCALA_LIBRARY_ARTIFACTID, "jar" );
257             if ( scalaLibraryArtifact == null )
258             {
259                 throw new MojoExecutionException( String.format( "Required %s:%s:jar dependency not found",
260                                                                  SCALA_GROUPID, SCALA_LIBRARY_ARTIFACTID ) );
261             }
262 
263             String scalaVersion = scalaLibraryArtifact.getVersion();
264             Artifact scalaCompilerArtifact =
265                 getResolvedArtifact( SCALA_GROUPID, SCALA_COMPILER_ARTIFACTID, scalaVersion );
266             if ( scalaCompilerArtifact == null )
267             {
268                 throw new MojoExecutionException(
269                                                   String.format( "Required %s:%s:%s:jar dependency not found",
270                                                                  SCALA_GROUPID, SCALA_COMPILER_ARTIFACTID,
271                                                                  scalaVersion ) );
272             }
273 
274             List<File> scalaExtraJars = getCompilerDependencies( scalaCompilerArtifact );
275             scalaExtraJars.remove( scalaLibraryArtifact.getFile() );
276 
277             Artifact xsbtiArtifact = getResolvedArtifact( SBT_GROUP_ID, XSBTI_ARTIFACT_ID, sbtVersion );
278             if ( xsbtiArtifact == null )
279             {
280                 throw new MojoExecutionException( String.format( "Required %s:%s:%s:jar dependency not found",
281                                                                  SBT_GROUP_ID, XSBTI_ARTIFACT_ID, sbtVersion ) );
282             }
283 
284             Artifact compilerInterfaceSrc =
285                 getResolvedArtifact( SBT_GROUP_ID, COMPILER_INTERFACE_ARTIFACT_ID, sbtVersion,
286                                      COMPILER_INTERFACE_CLASSIFIER );
287             if ( compilerInterfaceSrc == null )
288             {
289                 throw new MojoExecutionException( String.format( "Required %s:%s:%s:%s:jar dependency not found",
290                                                                  SBT_GROUP_ID, COMPILER_INTERFACE_ARTIFACT_ID,
291                                                                  sbtVersion, COMPILER_INTERFACE_CLASSIFIER ) );
292             }
293 
294             List<String> classpathElements = getClasspathElements();
295             classpathElements.remove( getOutputDirectory().getAbsolutePath() );
296             List<File> classpathFiles = new ArrayList<File>( classpathElements.size() );
297             for ( String path : classpathElements )
298             {
299                 classpathFiles.add( new File( path ) );
300             }
301 
302             SBTLogger sbtLogger = new SBTLogger( getLog() );
303             Setup setup =
304                 Setup.create( scalaCompilerArtifact.getFile(), scalaLibraryArtifact.getFile(), scalaExtraJars,
305                               xsbtiArtifact.getFile(), compilerInterfaceSrc.getFile(), null, FORK_JAVA );
306             if ( getLog().isDebugEnabled() )
307             {
308                 Setup.debug( setup, sbtLogger );
309             }
310             Compiler compiler = Compiler.create( setup, sbtLogger );
311 
312             Inputs inputs =
313                 Inputs.create( classpathFiles, sourceFiles, getOutputDirectory(), getScalacOptions(),
314                                getJavacOptions(), getAnalysisCacheFile(), getAnalysisCacheMap(), COMPILE_ORDER,
315                                getIncOptions(), getLog().isDebugEnabled() /* mirrorAnalysisCache */ );
316             if ( getLog().isDebugEnabled() )
317             {
318                 Inputs.debug( inputs, sbtLogger );
319             }
320 
321             compiler.compile( inputs, sbtLogger );
322         }
323         catch ( xsbti.CompileFailed e )
324         {
325             throw new MojoFailureException( "Scala compilation failed", e );
326         }
327         catch ( ArtifactNotFoundException e )
328         {
329             throw new MojoFailureException( "Scala compilation failed", e );
330         }
331         catch ( ArtifactResolutionException e )
332         {
333             throw new MojoFailureException( "Scala compilation failed", e );
334         }
335         catch ( InvalidDependencyVersionException e )
336         {
337             throw new MojoFailureException( "Scala compilation failed", e );
338         }
339         catch ( ProjectBuildingException e )
340         {
341             throw new MojoFailureException( "Scala compilation failed", e );
342         }
343     }
344 
345     /**
346      * Returns compilation classpath elements.
347      * 
348      * @return classpath elements
349      */
350     protected abstract List<String> getClasspathElements();
351 
352     /**
353      * Returns compilation source roots.
354      * 
355      * @return source roots
356      */
357     protected abstract List<String> getCompileSourceRoots();
358 
359     /**
360      * Returns output directory.
361      * 
362      * @return output directory
363      */
364     protected abstract File getOutputDirectory();
365 
366     /**
367      * Returns incremental compilation analysis cache file.
368      * 
369      * @return analysis cache file
370      */
371     protected abstract File getAnalysisCacheFile();
372 
373     /**
374      * Returns incremental compilation analyses map for reactor projects.
375      * 
376      * @return analysis cache map
377      */
378     protected abstract Map<File, File> getAnalysisCacheMap();
379 
380     private Artifact getDependencyArtifact( Collection<?> classPathArtifacts, String groupId, String artifactId,
381                                               String type )
382     {
383         Artifact result = null;
384         for ( Iterator<?> iter = classPathArtifacts.iterator(); iter.hasNext(); )
385         {
386             Artifact artifact = (Artifact) iter.next();
387             if ( groupId.equals( artifact.getGroupId() ) && artifactId.equals( artifact.getArtifactId() )
388                 && type.equals( artifact.getType() ) )
389             {
390                 result = artifact;
391                 break;
392             }
393         }
394         return result;
395     }
396 
397     private List<File> getSourceFiles( List<File> sourceRootDirs )
398     {
399         List<File> sourceFiles = new ArrayList<File>();
400 
401         DirectoryScanner scanner = new DirectoryScanner();
402         scanner.setIncludes( new String[] { "**/*.java", "**/*.scala" } );
403         scanner.addDefaultExcludes();
404 
405         for ( File dir : sourceRootDirs )
406         {
407             if ( dir.isDirectory() )
408             {
409                 scanner.setBasedir( dir );
410                 scanner.scan();
411                 String[] includedFileNames = scanner.getIncludedFiles();
412                 for ( String includedFileName : includedFileNames )
413                 {
414                     File tmpAbsFile = new File( dir, includedFileName ).getAbsoluteFile(); // ?
415                     sourceFiles.add( tmpAbsFile );
416                 }
417             }
418         }
419         // scalac is sensible to scala file order, file system can't guarantee file order => unreproductible build error
420         // across platform
421         // to guarantee reproductible command line we order file by path (os dependend).
422         // Collections.sort( sourceFiles );
423         return sourceFiles;
424     }
425 
426     private List<String> getScalacOptions()
427     {
428         List<String> result = new ArrayList<String>( Arrays.asList( scalacOptions.split( " " ) ) );
429         if ( !result.contains( "-encoding" ) && sourceEncoding != null && sourceEncoding.length() > 0 )
430         {
431             result.add( "-encoding" );
432             result.add( sourceEncoding );
433         }
434         return result;
435     }
436 
437     private List<String> getJavacOptions()
438     {
439         List<String> result = new ArrayList<String>( Arrays.asList( javacOptions.split( " " ) ) );
440         if ( !result.contains( "-encoding" ) && sourceEncoding != null && sourceEncoding.length() > 0 )
441         {
442             result.add( "-encoding" );
443             result.add( sourceEncoding );
444         }
445         return result;
446     }
447 
448     private File defaultAnalysisDirectory( MavenProject p )
449     {
450         return new File( p.getBuild().getDirectory(), "analysis" );
451     }
452 
453     /**
454      * Returns incremental main compilation analysis cache file location for a project.
455      * 
456      * @param p Maven project
457      * @return analysis cache file location
458      */
459     protected File defaultAnalysisCacheFile( MavenProject p )
460     {
461         return new File( defaultAnalysisDirectory( p ), "compile" );
462     }
463 
464     /**
465      * Returns incremental test compilation analysis cache file location for a project.
466      * 
467      * @param p Maven project
468      * @return analysis cache file location
469      */
470     protected File defaultTestAnalysisCacheFile( MavenProject p )
471     {
472         return new File( defaultAnalysisDirectory( p ), "test-compile" );
473     }
474 
475     private IncOptions getIncOptions()
476     {
477         // comment from SBT (sbt.inc.IncOptions.scala):
478         // After which step include whole transitive closure of invalidated source files.
479         //
480         // comment from Zinc (com.typesafe.zinc.Settings.scala):
481         // Steps before transitive closure
482         int transitiveStep = 3;
483 
484         // comment from SBT (sbt.inc.IncOptions.scala):
485         // What's the fraction of invalidated source files when we switch to recompiling
486         // all files and giving up incremental compilation altogether. That's useful in
487         // cases when probability that we end up recompiling most of source files but
488         // in multiple steps is high. Multi-step incremental recompilation is slower
489         // than recompiling everything in one step.
490         //
491         // comment from Zinc (com.typesafe.zinc.Settings.scala):
492         // Limit before recompiling all sources
493         double recompileAllFraction = 0.5d;
494 
495         // comment from SBT (sbt.inc.IncOptions.scala):
496         // Print very detailed information about relations, such as dependencies between source files.
497         //
498         // comment from Zinc (com.typesafe.zinc.Settings.scala):
499         // Enable debug logging of analysis relations
500         boolean relationsDebug = false;
501 
502         // comment from SBT (sbt.inc.IncOptions.scala):
503         // Enable tools for debugging API changes. At the moment this option is unused but in the
504         // future it will enable for example:
505         //   - disabling API hashing and API minimization (potentially very memory consuming)
506         //   - diffing textual API representation which helps understanding what kind of changes
507         //     to APIs are visible to the incremental compiler
508         //
509         // comment from Zinc (com.typesafe.zinc.Settings.scala):
510         // Enable analysis API debugging
511         boolean apiDebug = false;
512 
513         // comment from SBT (sbt.inc.IncOptions.scala):
514         // Controls context size (in lines) displayed when diffs are produced for textual API
515         // representation.
516         //
517         // This option is used only when `apiDebug == true`.
518         //
519         // comment from Zinc (com.typesafe.zinc.Settings.scala):
520         // Diff context size (in lines) for API debug
521         int apiDiffContextSize = 5;
522 
523         // comment from SBT (sbt.inc.IncOptions.scala):
524         // The directory where we dump textual representation of APIs. This method might be called
525         // only if apiDebug returns true. This is unused option at the moment as the needed functionality
526         // is not implemented yet.
527         //
528         // comment from Zinc (com.typesafe.zinc.Settings.scala):
529         // Destination for analysis API dump
530         Option<File> apiDumpDirectory = Option.empty();
531 
532         // comment from Zinc (com.typesafe.zinc.Settings.scala):
533         // Restore previous class files on failure
534         boolean transactional = false;
535 
536         // comment from Zinc (com.typesafe.zinc.Settings.scala):
537         // Backup location (if transactional)
538         Option<File> backup = Option.empty();
539 
540         return new IncOptions( transitiveStep, recompileAllFraction, relationsDebug, apiDebug, apiDiffContextSize, apiDumpDirectory, transactional, backup );
541     }
542 
543     // Private utility methods
544 
545     private Artifact getResolvedArtifact( String groupId, String artifactId, String version )
546         throws ArtifactNotFoundException, ArtifactResolutionException
547     {
548         Artifact artifact = factory.createArtifact( groupId, artifactId, version, Artifact.SCOPE_RUNTIME, "jar" );
549         resolver.resolve( artifact, remoteRepos, localRepo );
550         return artifact;
551     }
552 
553     private Artifact getResolvedArtifact( String groupId, String artifactId, String version, String classifier )
554         throws ArtifactNotFoundException, ArtifactResolutionException
555     {
556         Artifact artifact = factory.createArtifactWithClassifier( groupId, artifactId, version, "jar", classifier );
557         resolver.resolve( artifact, remoteRepos, localRepo );
558         return artifact;
559     }
560 
561     private List<File> getCompilerDependencies( Artifact scalaCompilerArtifact )
562         throws ArtifactNotFoundException, ArtifactResolutionException, InvalidDependencyVersionException,
563         ProjectBuildingException
564     {
565         List<File> d = new ArrayList<File>();
566         for ( Artifact artifact : getAllDependencies( scalaCompilerArtifact ) )
567         {
568             d.add( artifact.getFile() );
569         }
570         return d;
571     }
572 
573     private Set<Artifact> getAllDependencies( Artifact artifact )
574         throws ArtifactNotFoundException, ArtifactResolutionException, InvalidDependencyVersionException,
575         ProjectBuildingException
576     {
577         Set<Artifact> result = new HashSet<Artifact>();
578         MavenProject p = mavenProjectBuilder.buildFromRepository( artifact, remoteRepos, localRepo );
579         Set<Artifact> d = resolveDependencyArtifacts( p );
580         result.addAll( d );
581         for ( Artifact dependency : d )
582         {
583             Set<Artifact> transitive = getAllDependencies( dependency );
584             result.addAll( transitive );
585         }
586         return result;
587     }
588 
589     /**
590      * This method resolves the dependency artifacts from the project.
591      * 
592      * @param theProject The POM.
593      * @return resolved set of dependency artifacts.
594      * @throws ArtifactResolutionException
595      * @throws ArtifactNotFoundException
596      * @throws InvalidDependencyVersionException
597      */
598     private Set<Artifact> resolveDependencyArtifacts( MavenProject theProject )
599         throws ArtifactNotFoundException, ArtifactResolutionException, InvalidDependencyVersionException
600     {
601         AndArtifactFilter filter = new AndArtifactFilter();
602         filter.add( new ScopeArtifactFilter( Artifact.SCOPE_TEST ) );
603         filter.add( new ArtifactFilter()
604         {
605             public boolean include( Artifact artifact )
606             {
607                 return !artifact.isOptional();
608             }
609         } );
610         // TODO follow the dependenciesManagement and override rules
611         Set<Artifact> artifacts = theProject.createArtifacts( factory, Artifact.SCOPE_RUNTIME, filter );
612         for ( Artifact artifact : artifacts )
613         {
614             resolver.resolve( artifact, remoteRepos, localRepo );
615         }
616         return artifacts;
617     }
618 
619 }