View Javadoc

1   package com.jayway.maven.plugins.android.phase04processclasses;
2   
3   import com.jayway.maven.plugins.android.AbstractAndroidMojo;
4   import com.jayway.maven.plugins.android.CommandExecutor;
5   import com.jayway.maven.plugins.android.ExecutionException;
6   import com.jayway.maven.plugins.android.config.ConfigHandler;
7   import com.jayway.maven.plugins.android.config.ConfigPojo;
8   import com.jayway.maven.plugins.android.config.PullParameter;
9   import com.jayway.maven.plugins.android.configuration.Proguard;
10  import org.apache.commons.lang.StringUtils;
11  import org.apache.maven.RepositoryUtils;
12  import org.apache.maven.artifact.Artifact;
13  import org.apache.maven.plugin.MojoExecutionException;
14  import org.apache.maven.plugin.MojoFailureException;
15  import org.sonatype.aether.util.artifact.DefaultArtifact;
16  import org.sonatype.aether.util.artifact.JavaScopes;
17  
18  import java.io.File;
19  import java.util.ArrayList;
20  import java.util.Arrays;
21  import java.util.Collection;
22  import java.util.HashSet;
23  import java.util.Iterator;
24  import java.util.LinkedList;
25  import java.util.List;
26  
27  /**
28   * Processes both application and dependency classes using the ProGuard byte code obfuscator,
29   * minimzer, and optimizer. For more information, see https://proguard.sourceforge.net.
30   *
31   * @author Jonson
32   * @author Matthias Kaeppler
33   * @author Manfred Moser
34   * @author Michal Harakal
35   * @goal proguard
36   * @phase process-classes
37   * @requiresDependencyResolution compile
38   */
39  public class ProguardMojo extends AbstractAndroidMojo
40  {
41  
42      /**
43       * <p>
44       * ProGuard configuration. ProGuard is disabled by default. Set the skip parameter to false to activate proguard.
45       * A complete configuartion can include any of the following:
46       * </p>
47       * <p/>
48       * <pre>
49       * &lt;proguard&gt;
50       *    &lt;skip&gt;true|false&lt;/skip&gt;
51       *    &lt;config&gt;proguard.cfg&lt;/config&gt;
52       *    &lt;configs&gt;
53       *      &lt;config&gt;${env.ANDROID_HOME}/tools/proguard/proguard-android.txt&lt;/config&gt;
54       *    &lt;/configs&gt;
55       *    &lt;proguardJarPath&gt;someAbsolutePathToProguardJar&lt;/proguardJarPath&gt;
56       *    &lt;filterMavenDescriptor&gt;true|false&lt;/filterMavenDescriptor&gt;
57       *    &lt;filterManifest&gt;true|false&lt;/filterManifest&gt;
58       *    &lt;jvmArguments&gt;
59       *     &lt;jvmArgument&gt;-Xms256m&lt;/jvmArgument&gt;
60       *     &lt;jvmArgument&gt;-Xmx512m&lt;/jvmArgument&gt;
61       *   &lt;/jvmArguments&gt;
62       * &lt;/proguard&gt;
63       * </pre>
64       * <p>
65       * A good practice is to create a release profile in your POM, in which you enable ProGuard.
66       * ProGuard should be disabled for development builds, since it obfuscates class and field
67       * names, and it may interfere with test projects that rely on your application classes.
68       * All parameters can be overridden in profiles or the the proguard* properties. Default values apply and are
69       * documented with these properties.
70       * </p>
71       *
72       * @parameter
73       */
74      @ConfigPojo
75      protected Proguard proguard;
76  
77      /**
78       * Whether ProGuard is enabled or not. Defaults to true.
79       *
80       * @parameter expression="${android.proguard.skip}"
81       * @optional
82       */
83      private Boolean proguardSkip;
84  
85      @PullParameter( defaultValue = "true" )
86      private Boolean parsedSkip;
87  
88      /**
89       * Path to the ProGuard configuration file (relative to project root). Defaults to "proguard.cfg"
90       *
91       * @parameter expression="${android.proguard.config}"
92       * @optional
93       */
94      private String proguardConfig;
95  
96      @PullParameter( defaultValue = "proguard.cfg" )
97      private String parsedConfig;
98  
99      /**
100      * Additional ProGuard configuration files (relative to project root).
101      *
102      * @parameter expression="${android.proguard.configs}"
103      * @optional
104      */
105     private String[] proguardConfigs;
106 
107     @PullParameter( defaultValueGetterMethod = "getDefaultProguardConfigs" )
108     private String[] parsedConfigs;
109 
110     /**
111      * Path to the proguard jar and therefore version of proguard to be used. By default this will load the jar from
112      * the Android SDK install. Overriding it with an absolute path allows you to use a newer or custom proguard
113      * version..
114      * <p/>
115      * You can also reference an external Proguard version as a plugin dependency like this:
116      * <pre>
117      * &lt;plugin&gt;
118      *   &lt;groupId&gt;com.jayway.maven.plugins.android.generation2&lt;/groupId&gt;
119      *   &lt;artifactId&gt;android-maven-plugin&lt;/artifactId&gt;
120      *     &lt;dependencies&gt;
121      *       &lt;dependency&gt;
122      *         &lt;groupId&gt;net.sf.proguard&lt;/groupId&gt;
123      *         &lt;artifactId&gt;proguard-base&lt;/artifactId&gt;
124      *         &lt;version&gt;4.7&lt;/version&gt;
125      *       &lt;/dependency&gt;
126      *     &lt;/dependencies&gt;
127      * </pre>
128      * <p/>
129      * which will download and use Proguard 4.7 as deployed to the Central Repository.
130      *
131      * @parameter expression="${android.proguard.proguardJarPath}
132      * @optional
133      */
134     private String proguardProguardJarPath;
135 
136     @PullParameter( defaultValueGetterMethod = "getProguardJarPath" )
137     private String parsedProguardJarPath;
138     
139     /**
140      * Path relative to the project's build directory (target) where proguard puts folowing files:
141      * <p/>
142      * <ul>
143      *   <li>dump.txt</li>
144      *   <li>seeds.txt</li>
145      *   <li>usage.txt</li>
146      *   <li>mapping.txt</li>
147      * </ul>
148      * <p/>
149      * You can define the directory like this:
150      * <pre>
151      * &lt;proguard&gt;
152      *   &lt;skip&gt;false&lt;/skip&gt;
153      *   &lt;config&gt;proguard.cfg&lt;/config&gt;
154      *   &lt;outputDirectory&gt;my_proguard&lt;/outputDirectory&gt;
155      * &lt;/proguard&gt; 
156      * </pre>
157      * <p/>
158      * Output directory is defined relatively so it could be also outside of the target directory.
159      * <p/>
160      *
161      * @parameter expression="${android.proguard.outputDirectory}"  default-value="proguard"
162      * @optional
163      */
164     private String outputDirectory;
165 
166     @PullParameter( defaultValue = "proguard" )
167     private String parsedOutputDirectory;
168     
169 
170     /**
171      * Extra JVM Arguments. Using these you can e.g. increase memory for the jvm running the build.
172      * Defaults to "-Xmx512M".
173      *
174      * @parameter expression="${android.proguard.jvmArguments}"
175      * @optional
176      */
177     private String[] proguardJvmArguments;
178 
179     @PullParameter( defaultValueGetterMethod = "getDefaultJvmArguments" )
180     private String[] parsedJvmArguments;
181 
182     /**
183      * If set to true will add a filter to remove META-INF/maven/* files. Defaults to false.
184      *
185      * @parameter expression="${android.proguard.filterMavenDescriptor}"
186      * @optional
187      */
188     private Boolean proguardFilterMavenDescriptor;
189 
190     @PullParameter( defaultValue = "true" )
191     private Boolean parsedFilterMavenDescriptor;
192 
193     /**
194      * If set to true will add a filter to remove META-INF/MANIFEST.MF files.  Defaults to false.
195      *
196      * @parameter expression="${android.proguard.filterManifest}"
197      * @optional
198      */
199     private Boolean proguardFilterManifest;
200 
201     @PullParameter( defaultValue = "true" )
202     private Boolean parsedFilterManifest;
203     
204     /**
205      * If set to true JDK jars will be included as library jars and corresponding filters
206      * will be applied to android.jar.  Defaults to true.
207      * @parameter expression="${android.proguard.includeJdkLibs}"
208      */
209     private Boolean includeJdkLibs;
210     
211     @PullParameter( defaultValue = "true" )
212     private Boolean parsedIncludeJdkLibs;
213 
214     /**
215      * The plugin dependencies.
216      *
217      * @parameter expression="${plugin.artifacts}"
218      * @required
219      * @readonly
220      */
221     protected List<Artifact> pluginDependencies;
222 
223     public static final String PROGUARD_OBFUSCATED_JAR = "proguard-obfuscated.jar";
224 
225     private static final Collection<String> ANDROID_LIBRARY_EXCLUDED_FILTER = Arrays
226             .asList( "org/xml/**", "org/w3c/**", "java/**", "javax/**" );
227 
228     private static final Collection<String> MAVEN_DESCRIPTOR = Arrays.asList( "META-INF/maven/**" );
229     private static final Collection<String> META_INF_MANIFEST = Arrays.asList( "META-INF/MANIFEST.MF" );
230 
231     /**
232      * For Proguard is required only jar type dependencies, all other like .so or .apklib can be skipped.
233      */
234     private static final String USED_DEPENDENCY_TYPE = "jar";
235 
236     private Collection<String> globalInJarExcludes = new HashSet<String>();
237 
238     private List<Artifact> artifactBlacklist = new LinkedList<Artifact>();
239     private List<Artifact> artifactsToShift = new LinkedList<Artifact>();
240 
241     private List<ProGuardInput> inJars = new LinkedList<ProguardMojo.ProGuardInput>();
242     private List<ProGuardInput> libraryJars = new LinkedList<ProguardMojo.ProGuardInput>();
243 
244     private File javaHomeDir;
245     private File javaLibDir;
246     private File altJavaLibDir;
247 
248     private static class ProGuardInput
249     {
250 
251         private String path;
252         private Collection<String> excludedFilter;
253 
254         public ProGuardInput( String path, Collection<String> excludedFilter )
255         {
256             this.path = path;
257             this.excludedFilter = excludedFilter;
258         }
259 
260         public String toCommandLine()
261         {
262             if ( excludedFilter != null && ! excludedFilter.isEmpty() )
263             {
264                 StringBuilder sb = new StringBuilder( path );
265                 sb.append( '(' );
266                 for ( Iterator<String> it = excludedFilter.iterator(); it.hasNext(); )
267                 {
268                     sb.append( '!' ).append( it.next() );
269                     if ( it.hasNext() )
270                     {
271                         sb.append( ',' );
272                     }
273                 }
274                 sb.append( ')' );
275                 return sb.toString();
276             }
277             else
278             {
279                 return "\'" + path + "\'";
280             }
281         }
282     }
283 
284     @Override
285     public void execute() throws MojoExecutionException, MojoFailureException
286     {
287         ConfigHandler configHandler = new ConfigHandler( this );
288         configHandler.parseConfiguration();
289 
290         if ( ! parsedSkip )
291         {
292             executeProguard();
293         }
294     }
295 
296     private void executeProguard() throws MojoExecutionException
297     {
298 
299         final File proguardDir = new File( project.getBuild().getDirectory(), parsedOutputDirectory );
300           
301         if ( ! proguardDir.exists() && ! proguardDir.mkdir() )
302         {
303             throw new MojoExecutionException( "Cannot create proguard output directory" );
304         }
305         else
306         {
307             if ( proguardDir.exists() && ! proguardDir.isDirectory() )
308             {
309                 throw new MojoExecutionException( "Non-directory exists at " + proguardDir.getAbsolutePath() );
310             }
311         }
312 
313         CommandExecutor executor = CommandExecutor.Factory.createDefaultCommmandExecutor();
314         executor.setLogger( this.getLog() );
315         List<String> commands = new ArrayList<String>();
316 
317         collectJvmArguments( commands );
318 
319         commands.add( "-jar" );
320         commands.add( parsedProguardJarPath );
321 
322         commands.add( "@" + parsedConfig );
323 
324         for ( String config : parsedConfigs )
325         {
326             commands.add( "@" + config );
327         }
328 
329         if ( proguardFile != null )
330         {
331             commands.add( "@" + proguardFile.getAbsolutePath() );
332         }
333 
334         collectInputFiles( commands );
335 
336         commands.add( "-outjars" );
337         commands.add( "'" + project.getBuild().getDirectory() + File.separator + PROGUARD_OBFUSCATED_JAR + "'" );
338 
339         commands.add( "-dump" );
340         commands.add( "'" + proguardDir + File.separator + "dump.txt'" );
341         commands.add( "-printseeds" );
342         commands.add( "'" + proguardDir + File.separator + "seeds.txt'" );
343         commands.add( "-printusage" );
344         commands.add( "'" + proguardDir + File.separator + "usage.txt'" );
345         commands.add( "-printmapping" );
346         commands.add( "'" + proguardDir + File.separator + "mapping.txt'" );
347 
348         final String javaExecutable = getJavaExecutable().getAbsolutePath();
349         getLog().info( javaExecutable + " " + commands.toString() );
350         try
351         {
352             executor.executeCommand( javaExecutable, commands, project.getBasedir(), false );
353         }
354         catch ( ExecutionException e )
355         {
356             throw new MojoExecutionException( "", e );
357         }
358     }
359 
360     /**
361      * Convert the jvm arguments in parsedJvmArguments as populated by the config in format as needed by the java
362      * command. Also preserve backwards compatibility in terms of dashes required or not..
363      *
364      * @param commands
365      */
366     private void collectJvmArguments( List<String> commands )
367     {
368         if ( parsedJvmArguments != null )
369         {
370             for ( String jvmArgument : parsedJvmArguments )
371             {
372                 // preserve backward compatibility allowing argument with or without dash (e.g.
373                 // Xmx512m as well as -Xmx512m should work) (see
374                 // http://code.google.com/p/maven-android-plugin/issues/detail?id=153)
375                 if ( ! jvmArgument.startsWith( "-" ) )
376                 {
377                     jvmArgument = "-" + jvmArgument;
378                 }
379                 commands.add( jvmArgument );
380             }
381         }
382     }
383 
384     private void collectInputFiles( List<String> commands )
385     {
386         // commons-logging breaks everything horribly, so we skip it from the program
387         // dependencies and declare it to be a library dependency instead
388         skipArtifact( "commons-logging", "commons-logging", true );
389 
390         collectProgramInputFiles();
391         for ( ProGuardInput injar : inJars )
392         {
393             commands.add( "-injars" );
394             commands.add( injar.toCommandLine() );
395         }
396 
397         collectLibraryInputFiles();
398         for ( ProGuardInput libraryjar : libraryJars )
399         {
400             commands.add( "-libraryjars" );
401             commands.add( libraryjar.toCommandLine() );
402         }
403     }
404 
405     /**
406      * Figure out the full path to the current java executable.
407      *
408      * @return the full path to the current java executable.
409      */
410     private static File getJavaExecutable()
411     {
412         final String javaHome = System.getProperty( "java.home" );
413         final String slash = File.separator;
414         return new File( javaHome + slash + "bin" + slash + "java" );
415     }
416 
417     private void skipArtifact( String groupId, String artifactId, boolean shiftToLibraries )
418     {
419         artifactBlacklist.add( RepositoryUtils.toArtifact( new DefaultArtifact( groupId, artifactId, null, null ) ) );
420         if ( shiftToLibraries )
421         {
422             artifactsToShift
423                     .add( RepositoryUtils.toArtifact( new DefaultArtifact( groupId, artifactId, null, null ) ) );
424         }
425     }
426 
427     private boolean isBlacklistedArtifact( Artifact artifact )
428     {
429         for ( Artifact artifactToSkip : artifactBlacklist )
430         {
431             if ( artifactToSkip.getGroupId().equals( artifact.getGroupId() ) && artifactToSkip.getArtifactId()
432                     .equals( artifact.getArtifactId() ) )
433             {
434                 return true;
435             }
436         }
437         return false;
438     }
439 
440     private boolean isShiftedArtifact( Artifact artifact )
441     {
442         for ( Artifact artifactToShift : artifactsToShift )
443         {
444             if ( artifactToShift.getGroupId().equals( artifact.getGroupId() ) && artifactToShift.getArtifactId()
445                     .equals( artifact.getArtifactId() ) )
446             {
447                 return true;
448             }
449         }
450         return false;
451     }
452 
453     private void collectProgramInputFiles()
454     {
455         if ( parsedFilterManifest )
456         {
457             globalInJarExcludes.addAll( META_INF_MANIFEST );
458         }
459         if ( parsedFilterMavenDescriptor )
460         {
461             globalInJarExcludes.addAll( MAVEN_DESCRIPTOR );
462         }
463 
464         // we first add the application's own class files
465         addInJar( project.getBuild().getOutputDirectory() );
466 
467         // we then add all its dependencies (incl. transitive ones), unless they're blacklisted
468         for ( Artifact artifact : getAllRelevantDependencyArtifacts() )
469         {
470             if ( isBlacklistedArtifact( artifact ) || !USED_DEPENDENCY_TYPE.equals( artifact.getType() ) )
471             {
472                 continue;
473             }
474             addInJar( artifact.getFile().getAbsolutePath(), globalInJarExcludes );
475         }
476     }
477 
478     private void addInJar( String path, Collection<String> filterExpression )
479     {
480         inJars.add( new ProGuardInput( path, filterExpression ) );
481     }
482 
483     private void addInJar( String path )
484     {
485         addInJar( path, null );
486     }
487 
488     private void addLibraryJar( String path, Collection<String> filterExpression )
489     {
490         libraryJars.add( new ProGuardInput( path, filterExpression ) );
491     }
492 
493     private void addLibraryJar( String path )
494     {
495         addLibraryJar( path, null );
496     }
497 
498     private void collectLibraryInputFiles()
499     {
500         if ( parsedIncludeJdkLibs )
501         {
502             // we have to add the Java framework classes to the library JARs, since they are not
503             // distributed with the JAR on Central, and since we'll strip them out of the android.jar
504             // that is shipped with the SDK (since that is not a complete Java distribution)
505             File rtJar = getJVMLibrary( "rt.jar" );
506             if ( rtJar == null )
507             {
508                 rtJar = getJVMLibrary( "classes.jar" );
509             }
510             if ( rtJar != null )
511             {
512                 addLibraryJar( rtJar.getPath() );
513             }
514 
515             // we also need to add the JAR containing e.g. javax.servlet
516             File jsseJar = getJVMLibrary( "jsse.jar" );
517             if ( jsseJar != null )
518             {
519                 addLibraryJar( jsseJar.getPath() );
520             }
521 
522             // and the javax.crypto stuff
523             File jceJar = getJVMLibrary( "jce.jar" );
524             if ( jceJar != null )
525             {
526                 addLibraryJar( jceJar.getPath() );
527             }
528         }
529 
530         // we treat any dependencies with provided scope as library JARs
531         for ( Artifact artifact : project.getArtifacts() )
532         {
533             if ( artifact.getScope().equals( JavaScopes.PROVIDED ) )
534             {
535                 if ( artifact.getArtifactId().equals( "android" ) && parsedIncludeJdkLibs )
536                 {
537                     addLibraryJar( artifact.getFile().getAbsolutePath(), ANDROID_LIBRARY_EXCLUDED_FILTER );
538                 }
539                 else
540                 {
541                     addLibraryJar( artifact.getFile().getAbsolutePath() );
542                 }
543             }
544             else
545             {
546                 if ( isShiftedArtifact( artifact ) )
547                 {
548                     // this is a blacklisted artifact that should be processed as a library instead
549                     addLibraryJar( artifact.getFile().getAbsolutePath() );
550                 }
551             }
552         }
553     }
554 
555 
556     /**
557      * Get the path to the proguard jar.
558      *
559      * @return
560      * @throws MojoExecutionException
561      */
562     private String getProguardJarPath() throws MojoExecutionException
563     {
564         String proguardJarPath = getProguardJarPathFromDependencies();
565         if ( StringUtils.isEmpty( proguardJarPath ) )
566         {
567             File proguardJarPathFile = new File( getAndroidSdk().getToolsPath(), "proguard/lib/proguard.jar" );
568             return proguardJarPathFile.getAbsolutePath();
569         }
570         return proguardJarPath;
571     }
572 
573     private String getProguardJarPathFromDependencies() throws MojoExecutionException
574     {
575         Artifact proguardArtifact = null;
576         int proguardArtifactDistance = - 1;
577         for ( Artifact artifact : pluginDependencies )
578         {
579             getLog().debug( "pluginArtifact: " + artifact.getFile() );
580             if ( ( "proguard".equals( artifact.getArtifactId() ) ) || ( "proguard-base"
581                                                                                 .equals( artifact.getArtifactId() ) ) )
582             {
583                 int distance = artifact.getDependencyTrail().size();
584                 getLog().debug( "proguard DependencyTrail: " + distance );
585                 if ( proguardArtifactDistance == - 1 )
586                 {
587                     proguardArtifact = artifact;
588                     proguardArtifactDistance = distance;
589                 }
590                 else
591                 {
592                     if ( distance < proguardArtifactDistance )
593                     {
594                         proguardArtifact = artifact;
595                         proguardArtifactDistance = distance;
596                     }
597                 }
598             }
599         }
600         if ( proguardArtifact != null )
601         {
602             getLog().debug( "proguardArtifact: " + proguardArtifact.getFile() );
603             return proguardArtifact.getFile().getAbsoluteFile().toString();
604         }
605         else
606         {
607             return null;
608         }
609 
610     }
611 
612     /**
613      * Get the default JVM arguments for the proguard invocation.
614      *
615      * @return
616      * @see #parsedJvmArguments
617      */
618     private String[] getDefaultJvmArguments()
619     {
620         return new String[]{ "-Xmx512M" };
621     }
622 
623     /**
624      * Get the default ProGuard config files.
625      *
626      * @return
627      * @see #parsedConfigs
628      */
629     private String[] getDefaultProguardConfigs()
630     {
631         return new String[0];
632     }
633 
634     /**
635      * Finds a library file in either the primary or alternate lib directory.
636      * @param fileName The base name of the file.
637      * @return Either a canonical filename, or {@code null} if not found.
638      */
639     private File getJVMLibrary( String fileName )
640     {
641         File libFile = new File( getJavaLibDir(), fileName );
642         if ( !libFile.exists() )
643         {
644             libFile = new File( getAltJavaLibDir(), fileName );
645             if ( !libFile.exists() )
646             {
647                 libFile = null;
648             }
649         }
650         return libFile;
651     }
652 
653     /**
654      * Determines the java.home directory.
655      * @return The java.home directory, as a File.
656      */
657     private File getJavaHomeDir()
658     {
659         if ( javaHomeDir == null )
660         {
661             javaHomeDir = new File( System.getProperty( "java.home" ) );
662         }
663         return javaHomeDir;
664     }
665 
666     /**
667      * Determines the primary JVM library location.
668      * @return The primary library directory, as a File.
669      */
670     private File getJavaLibDir()
671     {
672         if ( javaLibDir == null )
673         {
674             javaLibDir = new File( getJavaHomeDir(), "lib" );
675         }
676         return javaLibDir;
677     }
678 
679     /**
680      * Determines the alternate JVM library location (applies with older
681      * MacOSX JVMs).
682      * @return The alternate JVM library location, as a File.
683      */
684     private File getAltJavaLibDir()
685     {
686         if ( altJavaLibDir == null )
687         {
688             altJavaLibDir = new File( getJavaHomeDir().getParent(), "Classes" );
689         }
690         return altJavaLibDir;
691     }
692 }