View Javadoc
1   /*
2    * Copyright (C) 2009 Jayway AB
3    * Copyright (C) 2007-2008 JVending Masa
4    *
5    * Licensed under the Apache License, Version 2.0 (the "License");
6    * you may not use this file except in compliance with the License.
7    * You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package com.simpligility.maven.plugins.android.phase08preparepackage;
18  
19  import static com.simpligility.maven.plugins.android.InclusionExclusionResolver.filterArtifacts;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.util.ArrayList;
24  import java.util.HashSet;
25  import java.util.List;
26  import java.util.Set;
27  
28  import org.apache.commons.io.FileUtils;
29  import org.apache.commons.lang3.StringUtils;
30  import org.apache.maven.artifact.Artifact;
31  import org.apache.maven.model.Resource;
32  import org.apache.maven.plugin.MojoExecutionException;
33  import org.apache.maven.plugin.MojoFailureException;
34  import org.apache.maven.plugins.annotations.LifecyclePhase;
35  import org.apache.maven.plugins.annotations.Mojo;
36  import org.apache.maven.plugins.annotations.Parameter;
37  import org.apache.maven.plugins.annotations.ResolutionScope;
38  import org.codehaus.plexus.archiver.ArchiverException;
39  import org.codehaus.plexus.archiver.jar.JarArchiver;
40  import org.codehaus.plexus.archiver.util.DefaultFileSet;
41  
42  import com.simpligility.maven.plugins.android.AbstractAndroidMojo;
43  import com.simpligility.maven.plugins.android.CommandExecutor;
44  import com.simpligility.maven.plugins.android.ExecutionException;
45  import com.simpligility.maven.plugins.android.IncludeExcludeSet;
46  import com.simpligility.maven.plugins.android.configuration.D8;
47  
48  /**
49   * Converts compiled Java classes (including those containing Java 8 syntax) to the Android dex format.
50   * It is a replacement for the {@link DexMojo}.
51   *
52   * You should only run one or the other.
53   * By default D8 will run and Dex will not. But this is determined by the
54   *
55   * @author william.ferguson@xandar.com.au
56   */
57  @Mojo(
58      name = "d8",
59      defaultPhase = LifecyclePhase.PREPARE_PACKAGE,
60      requiresDependencyResolution = ResolutionScope.COMPILE
61  )
62  public class D8Mojo extends AbstractAndroidMojo
63  {
64      private static final String JAR = "jar";
65  
66      /**
67       * Configuration for the D8 command execution. It can be configured in the plugin configuration like so
68       *
69       * <pre>
70       * &lt;dexCompiler&gt;d8&lt;/dexCompiler&gt;
71       * &lt;d8&gt;
72       *   &lt;jvmArguments&gt;
73       *     &lt;jvmArgument&gt;-Xms256m&lt;/jvmArgument&gt;
74       *     &lt;jvmArgument&gt;-Xmx512m&lt;/jvmArgument&gt;
75       *   &lt;/jvmArguments&gt;
76       *   &lt;intermediate&gt;true|false&lt;/intermediate&gt;
77       *   &lt;mainDexList&gt;path to class list file&lt;/mainDexList&gt;
78       *   &lt;release&gt;path to class list file&lt;/release&gt;
79       *   &lt;minApi&gt;minimum API level compatibility&lt;/minApi&gt;
80       *   &lt;arguments&gt;
81       *     &lt;argument&gt;--opt1&lt;/argument&gt;
82       *     &lt;argument&gt;value1A&lt;/argument&gt;
83       *     &lt;argument&gt;--opt2&lt;/argument&gt;
84       *   &lt;/arguments&gt;
85       * &lt;/d8&gt;
86       * </pre>
87       *
88       * or via properties d8* or command line parameters android.d8.*
89       */
90  
91      /**
92       * The dex compiler to use. Allowed values are 'dex' (default) and 'd8'.
93       */
94      @Parameter( property = "android.dex.compiler", defaultValue = "dex" )
95      private String dexCompiler;
96  
97      @Parameter
98      private D8 d8;
99  
100     /**
101      * Extra JVM Arguments. Using these you can e.g. increase memory for the jvm running the build.
102      */
103     @Parameter( property = "android.d8.jvmArguments", defaultValue = "-Xmx1024M" )
104     private String[] d8JvmArguments;
105 
106     /**
107      * Decides whether to pass the --intermediate flag to d8.
108      */
109     @Parameter( property = "android.d8.intermediate", defaultValue = "false" )
110     private boolean d8Intermediate;
111 
112     /**
113      * Full path to class list to multi dex
114      */
115     @Parameter( property = "android.d8.mainDexList" )
116     private String d8MainDexList;
117 
118     /**
119      * Whether to pass the --release flag to d8.
120      */
121     @Parameter( property = "android.d8.release", defaultValue = "false" )
122     private boolean d8Release;
123 
124     /**
125      * The minApi (if any) to pass to d8.
126      */
127     @Parameter( property = "android.d8.minApi" )
128     private Integer d8MinApi;
129 
130     /**
131      * Additional command line parameters passed to d8.
132      */
133     @Parameter( property = "android.d8.arguments" )
134     private String[] d8Arguments;
135 
136     /**
137      * The name of the obfuscated JAR.
138      */
139     @Parameter( property = "android.proguard.obfuscatedJar" )
140     private File obfuscatedJar;
141 
142     /**
143      * Skips transitive dependencies. May be useful if the target classes directory is populated with the
144      * {@code maven-dependency-plugin} and already contains all dependency classes.
145      */
146     @Parameter( property = "skipDependencies", defaultValue = "false" )
147     private boolean skipDependencies;
148 
149     /**
150      * Allows to include or exclude artifacts by type. The {@code include} parameter has higher priority than the
151      * {@code exclude} parameter. These two parameters can be overridden by the {@code artifactSet} parameter. Empty
152      * strings are ignored. Example:
153      * <pre>
154      *     &lt;artifactTypeSet&gt;
155      *         &lt;includes&gt;
156      *             &lt;include&gt;aar&lt;/include&gt;
157      *         &lt;includes&gt;
158      *         &lt;excludes&gt;
159      *             &lt;exclude&gt;jar&lt;/exclude&gt;
160      *         &lt;excludes&gt;
161      *     &lt;/artifactTypeSet&gt;
162      * </pre>
163      */
164     @Parameter( property = "artifactTypeSet" )
165     private IncludeExcludeSet artifactTypeSet;
166 
167     /**
168      * Allows to include or exclude artifacts by {@code groupId}, {@code artifactId}, and {@code versionId}. The
169      * {@code include} parameter has higher priority than the {@code exclude} parameter. These two parameters can
170      * override the {@code artifactTypeSet} and {@code skipDependencies} parameters. Artifact {@code groupId},
171      * {@code artifactId}, and {@code versionId} are specified by a string with the respective values separated using
172      * a colon character {@code :}. {@code artifactId} and {@code versionId} can be optional covering an artifact
173      * range. Empty strings are ignored. Example:
174      * <pre>
175      *     &lt;artifactTypeSet&gt;
176      *         &lt;includes&gt;
177      *             &lt;include&gt;foo-group:foo-artifact:1.0-SNAPSHOT&lt;/include&gt;
178      *             &lt;include&gt;bar-group:bar-artifact:1.0-SNAPSHOT&lt;/include&gt;
179      *             &lt;include&gt;baz-group:*&lt;/include&gt;
180      *         &lt;includes&gt;
181      *         &lt;excludes&gt;
182      *             &lt;exclude&gt;qux-group:qux-artifact:*&lt;/exclude&gt;
183      *         &lt;excludes&gt;
184      *     &lt;/artifactTypeSet&gt;
185      * </pre>
186      */
187     @Parameter( property = "artifactSet" )
188     private IncludeExcludeSet artifactSet;
189 
190     private String[] parsedJvmArguments;
191     private boolean parsedIntermediate;
192     private String parsedMainDexList;
193     private String[] parsedArguments;
194     private DexCompiler parsedDexCompiler;
195     private boolean parsedRelease;
196     private Integer parsedMinApi;
197 
198     /**
199      * @throws MojoExecutionException
200      * @throws MojoFailureException
201      */
202     @Override
203     public void execute() throws MojoExecutionException, MojoFailureException
204     {
205         parseConfiguration();
206 
207         getLog().debug( "DexCompiler set to " + parsedDexCompiler );
208         if ( parsedDexCompiler != DexCompiler.D8 )
209         {
210             getLog().info( "Not executing D8Mojo because DEX compiler is set to " + parsedDexCompiler );
211             return;
212         }
213 
214         CommandExecutor executor = CommandExecutor.Factory.createDefaultCommmandExecutor();
215         executor.setLogger( getLog() );
216 
217         if ( generateApk )
218         {
219             runD8( executor );
220         }
221 
222         if ( attachJar )
223         {
224             File jarFile = new File( targetDirectory + File.separator
225                 + finalName + ".jar" );
226             projectHelper.attachArtifact( project, "jar", project.getArtifact().getClassifier(), jarFile );
227         }
228 
229         if ( attachSources )
230         {
231             // Also attach an .apksources, containing sources from this project.
232             final File apksources = createApkSourcesFile();
233             projectHelper.attachArtifact( project, "apksources", apksources );
234         }
235     }
236 
237     private List<File> getDependencies()
238     {
239         final List<File> libraries = new ArrayList<>();
240         for ( Artifact artifact : filterArtifacts( getTransitiveDependencyArtifacts(), skipDependencies,
241                 artifactTypeSet.getIncludes(), artifactTypeSet.getExcludes(), artifactSet.getIncludes(),
242                 artifactSet.getExcludes() ) )
243         {
244             if ( "jar".equals( artifact.getType() ) )
245             {
246                 libraries.add( artifact.getFile() );
247             }
248         }
249 
250         return libraries;
251     }
252 
253     /**
254      * @return Set of input files for dex. This is a combination of directories and jar files.
255      */
256     private Set< File > getD8InputFiles()
257     {
258         final Set< File > inputs = new HashSet< File >();
259 
260         if ( obfuscatedJar != null && obfuscatedJar.exists() )
261         {
262             // proguard has been run, use this jar
263             getLog().debug( "Adding dex input (obfuscatedJar) : " + obfuscatedJar );
264             inputs.add( obfuscatedJar );
265         }
266         else
267         {
268             getLog().debug( "Using non-obfuscated input" );
269             final File classesJar = new File( targetDirectory, finalName + ".jar" );
270             inputs.add( classesJar );
271             getLog().debug( "Adding dex input from : " + classesJar );
272 
273             for ( Artifact artifact : filterArtifacts( getTransitiveDependencyArtifacts(), skipDependencies,
274                     artifactTypeSet.getIncludes(), artifactTypeSet.getExcludes(), artifactSet.getIncludes(),
275                     artifactSet.getExcludes() ) )
276             {
277                 if ( artifact.getType().equals( JAR ) )
278                 {
279                     getLog().debug( "Adding dex input : " + artifact.getFile() );
280                     inputs.add( artifact.getFile().getAbsoluteFile() );
281                 }
282             }
283         }
284 
285         return inputs;
286     }
287 
288     private void parseConfiguration()
289     {
290         // config in pom found
291         if ( d8 != null )
292         {
293             // the if statements make sure that properties/command line
294             // parameter overrides configuration
295             // and that the dafaults apply in all cases;
296             if ( d8.getJvmArguments() == null )
297             {
298                 parsedJvmArguments = d8JvmArguments;
299             }
300             else
301             {
302                 parsedJvmArguments = d8.getJvmArguments();
303             }
304             if ( d8.isIntermediate() == null )
305             {
306                 parsedIntermediate = d8Intermediate;
307             }
308             else
309             {
310                 parsedIntermediate = d8.isIntermediate();
311             }
312             if ( d8.getMainDexList() == null )
313             {
314                 parsedMainDexList = d8MainDexList;
315             }
316             else
317             {
318                 parsedMainDexList = d8.getMainDexList();
319             }
320             if ( d8.getArguments() == null )
321             {
322                 parsedArguments = d8Arguments;
323             }
324             else
325             {
326                 parsedArguments = d8.getArguments();
327             }
328             parsedDexCompiler = DexCompiler.valueOfIgnoreCase( dexCompiler );
329             if ( d8.isRelease() == null )
330             {
331                 parsedRelease = release;
332             }
333             else
334             {
335                 parsedRelease = d8.isRelease();
336             }
337             if ( d8.getMinApi() == null )
338             {
339                 parsedMinApi = d8MinApi;
340             }
341             else
342             {
343                 parsedMinApi = d8.getMinApi();
344             }
345         }
346         else
347         {
348             parsedJvmArguments = d8JvmArguments;
349             parsedIntermediate = d8Intermediate;
350             parsedMainDexList = d8MainDexList;
351             parsedArguments = d8Arguments;
352             parsedDexCompiler = DexCompiler.valueOfIgnoreCase( dexCompiler );
353             parsedRelease = d8Release;
354             parsedMinApi = d8MinApi;
355         }
356     }
357 
358     private List< String > dexDefaultCommands() throws MojoExecutionException
359     {
360         List< String > commands = jarDefaultCommands();
361         commands.add( getAndroidSdk().getD8JarPath() );
362         return commands;
363     }
364 
365     private List<String> jarDefaultCommands()
366     {
367         List< String > commands = javaDefaultCommands();
368         commands.add( "-jar" );
369         return commands;
370     }
371 
372     private List<String> javaDefaultCommands()
373     {
374         List< String > commands = new ArrayList< String > ();
375         if ( parsedJvmArguments != null )
376         {
377             for ( String jvmArgument : parsedJvmArguments )
378             {
379                 // preserve backward compatibility allowing argument with or
380                 // without dash (e.g. Xmx512m as well as
381                 // -Xmx512m should work) (see
382                 // http://code.google.com/p/maven-android-plugin/issues/detail?id=153)
383                 if ( !jvmArgument.startsWith( "-" ) )
384                 {
385                     jvmArgument = "-" + jvmArgument;
386                 }
387                 getLog().debug( "Adding jvm argument " + jvmArgument );
388                 commands.add( jvmArgument );
389             }
390         }
391         return commands;
392     }
393 
394     private void runD8( CommandExecutor executor )
395         throws MojoExecutionException
396     {
397         final List< String > commands = dexDefaultCommands();
398         final Set< File > inputFiles = getD8InputFiles();
399         if ( parsedIntermediate )
400         {
401             commands.add( "--intermediate" );
402         }
403         if ( parsedMainDexList != null )
404         {
405             commands.add( "--main-dex-list" + parsedMainDexList );
406         }
407         if ( parsedArguments != null )
408         {
409             for ( String argument : parsedArguments )
410             {
411                 commands.add( argument );
412             }
413         }
414 
415         if ( parsedRelease )
416         {
417             commands.add( "--release" );
418         }
419 
420         if ( parsedMinApi != null )
421         {
422             commands.add( "--min-api" );
423             commands.add( parsedMinApi.toString() );
424         }
425 
426         commands.add( "--output" );
427         commands.add( targetDirectory.getAbsolutePath() );
428 
429         final File androidJar = getAndroidSdk().getAndroidJar();
430         commands.add( "--lib" );
431         commands.add( androidJar.getAbsolutePath() );
432 
433         // Add project classpath
434         final List<File> dependencies = getDependencies();
435         for ( final File file : dependencies )
436         {
437             commands.add( "--classpath" );
438             commands.add( file.getAbsolutePath() );
439         }
440 
441         for ( File inputFile : inputFiles )
442         {
443             commands.add( inputFile.getAbsolutePath() );
444         }
445 
446         getLog().info( "Convert classes to Dex : " + targetDirectory );
447         executeJava( commands, executor );
448     }
449 
450     private String executeJava( final List<String> commands, CommandExecutor executor ) throws MojoExecutionException
451     {
452         final String javaExecutable = getJavaExecutable().getAbsolutePath();
453         getLog().debug( javaExecutable + " " + commands.toString() );
454         try
455         {
456             executor.setCaptureStdOut( true );
457             executor.executeCommand( javaExecutable, commands, project.getBasedir(), false );
458             return executor.getStandardOut();
459         }
460         catch ( ExecutionException e )
461         {
462             throw new MojoExecutionException( "", e );
463         }
464     }
465 
466     /**
467      * Figure out the full path to the current java executable.
468      *
469      * @return the full path to the current java executable.
470      */
471     private static File getJavaExecutable()
472     {
473         final String javaHome = System.getProperty( "java.home" );
474         final String slash = File.separator;
475         return new File( javaHome + slash + "bin" + slash + "java" );
476     }
477 
478     /**
479      * @return
480      * @throws MojoExecutionException
481      */
482     protected File createApkSourcesFile() throws MojoExecutionException
483     {
484         final File apksources = new File( targetDirectory, finalName
485             + ".apksources" );
486         FileUtils.deleteQuietly( apksources );
487 
488         try
489         {
490             JarArchiver jarArchiver = new JarArchiver();
491             jarArchiver.setDestFile( apksources );
492 
493             addDirectory( jarArchiver, assetsDirectory, "assets" );
494             addDirectory( jarArchiver, resourceDirectory, "res" );
495             addDirectory( jarArchiver, sourceDirectory, "src/main/java" );
496             addJavaResources( jarArchiver, resources );
497 
498             jarArchiver.createArchive();
499         }
500         catch ( ArchiverException e )
501         {
502             throw new MojoExecutionException( "ArchiverException while creating .apksource file.", e );
503         }
504         catch ( IOException e )
505         {
506             throw new MojoExecutionException( "IOException while creating .apksource file.", e );
507         }
508 
509         return apksources;
510     }
511 
512     /**
513      * Makes sure the string ends with "/"
514      *
515      * @param prefix
516      *            any string, or null.
517      * @return the prefix with a "/" at the end, never null.
518      */
519     protected String endWithSlash( String prefix )
520     {
521         prefix = StringUtils.defaultIfEmpty( prefix, "/" );
522         if ( !prefix.endsWith( "/" ) )
523         {
524             prefix = prefix + "/";
525         }
526         return prefix;
527     }
528 
529     /**
530      * Adds a directory to a {@link JarArchiver} with a directory prefix.
531      *
532      * @param jarArchiver
533      * @param directory
534      *            The directory to add.
535      * @param prefix
536      *            An optional prefix for where in the Jar file the directory's contents should go.
537      */
538     protected void addDirectory( JarArchiver jarArchiver, File directory, String prefix )
539     {
540         if ( directory != null && directory.exists() )
541         {
542             final DefaultFileSet fileSet = new DefaultFileSet();
543             fileSet.setPrefix( endWithSlash( prefix ) );
544             fileSet.setDirectory( directory );
545             jarArchiver.addFileSet( fileSet );
546         }
547     }
548 
549     /**
550      * @param jarArchiver
551      * @param javaResources
552      */
553     protected void addJavaResources( JarArchiver jarArchiver, List< Resource > javaResources )
554     {
555         for ( Resource javaResource : javaResources )
556         {
557             addJavaResource( jarArchiver, javaResource );
558         }
559     }
560 
561     /**
562      * Adds a Java Resources directory (typically "src/main/resources") to a {@link JarArchiver}.
563      *
564      * @param jarArchiver
565      * @param javaResource
566      *            The Java resource to add.
567      */
568     protected void addJavaResource( JarArchiver jarArchiver, Resource javaResource )
569     {
570         if ( javaResource != null )
571         {
572             final File javaResourceDirectory = new File( javaResource.getDirectory() );
573             if ( javaResourceDirectory.exists() )
574             {
575                 final DefaultFileSet javaResourceFileSet = new DefaultFileSet();
576                 javaResourceFileSet.setDirectory( javaResourceDirectory );
577                 javaResourceFileSet.setPrefix( endWithSlash( "src/main/resources" ) );
578                 jarArchiver.addFileSet( javaResourceFileSet );
579             }
580         }
581     }
582 }