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