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.phase09package;
18  
19  import static com.simpligility.maven.plugins.android.common.AndroidExtension.AAR;
20  import static com.simpligility.maven.plugins.android.common.AndroidExtension.APKLIB;
21  
22  import java.io.File;
23  import java.io.FileInputStream;
24  import java.io.FileOutputStream;
25  import java.io.FilenameFilter;
26  import java.io.IOException;
27  import java.util.ArrayList;
28  import java.util.List;
29  
30  import org.apache.commons.io.FileUtils;
31  import org.apache.commons.io.IOUtils;
32  import org.apache.commons.lang3.StringUtils;
33  import org.apache.commons.lang3.SystemUtils;
34  import org.apache.maven.artifact.Artifact;
35  import org.apache.maven.plugin.MojoExecutionException;
36  import org.apache.maven.plugin.MojoFailureException;
37  import org.apache.maven.plugins.annotations.LifecyclePhase;
38  import org.apache.maven.plugins.annotations.Mojo;
39  import org.apache.maven.plugins.annotations.Parameter;
40  import org.apache.maven.plugins.annotations.ResolutionScope;
41  import org.codehaus.plexus.archiver.ArchiverException;
42  import org.codehaus.plexus.archiver.jar.JarArchiver;
43  import org.codehaus.plexus.archiver.util.DefaultFileSet;
44  import org.codehaus.plexus.archiver.zip.ZipArchiver;
45  
46  import com.android.SdkConstants;
47  import com.simpligility.maven.plugins.android.AbstractAndroidMojo;
48  import com.simpligility.maven.plugins.android.CommandExecutor;
49  import com.simpligility.maven.plugins.android.ExecutionException;
50  import com.simpligility.maven.plugins.android.common.AaptCommandBuilder;
51  import com.simpligility.maven.plugins.android.common.AndroidExtension;
52  import com.simpligility.maven.plugins.android.common.NativeHelper;
53  import com.simpligility.maven.plugins.android.config.PullParameter;
54  
55  
56  /**
57   * Creates an Android Archive (aar) file.<br>
58   */
59  @Mojo(
60          name = "aar",
61          defaultPhase = LifecyclePhase.VERIFY,
62          requiresDependencyResolution = ResolutionScope.COMPILE
63  )
64  public class AarMojo extends AbstractAndroidMojo
65  {
66      /**
67       * The name of the top level folder in the AAR where native libraries are found.
68       * NOTE: This is inconsistent with APK where the folder is called "lib", and does not match APKLIB
69       * layout either, where the folder is called "libs".
70       */
71      public static final String NATIVE_LIBRARIES_FOLDER = "jni";
72  
73      /**
74       * <p>Classifier to add to the artifact generated. If given, the artifact will be an attachment instead.</p>
75       */
76      @Parameter
77      private String classifier;
78  
79      /**
80       * Specifies the application makefile to use for the build (if other than the default Application.mk).
81       */
82      @Parameter
83      @PullParameter
84      private String applicationMakefile;
85  
86      /**
87       * Defines the architecture for the NDK build
88       */
89      @Parameter( property = "android.ndk.build.architecture" )
90      @PullParameter
91      private String ndkArchitecture;
92  
93      /**
94       * Specifies the classifier with which the artifact should be stored in the repository
95       */
96      @Parameter( property = "android.ndk.build.native-classifier" )
97      @PullParameter
98      private String ndkClassifier;
99  
100     /**
101      * Specifies the files that should be included in the classes.jar within the aar
102      */
103     @Parameter
104     @PullParameter
105     private String[] classesJarIncludes = new String[]{"**/*"};
106 
107     /**
108      * Specifies the files that should be excluded from the classes.jar within the aar
109      */
110     @Parameter
111     @PullParameter
112     private String[] classesJarExcludes = new String[]{"**/R.class", "**/R$*.class"};
113 
114     /**
115      * Specifies the proguard rule files to be included in the final package. All specified files will be merged into
116      * one proguard.txt file.
117      */
118     @Parameter
119     private File[] consumerProguardFiles;
120 
121     @Parameter(
122             property = "android.proguard.obfuscatedJar",
123             defaultValue = "${project.build.directory}/${project.build.finalName}_obfuscated.jar"
124     )
125     private String obfuscatedJar;
126 
127     private List<String> sourceFolders = new ArrayList<String>();
128 
129     /**
130      * @throws MojoExecutionException
131      * @throws MojoFailureException
132      */
133     public void execute() throws MojoExecutionException, MojoFailureException
134     {
135         String out = targetDirectory.getPath();
136         for ( String src : project.getCompileSourceRoots() )
137         {
138             if ( !src.startsWith( out ) )
139             {
140                 sourceFolders.add( src );
141             }
142         }
143 
144         getLog().info( "Generating AAR file : " + project.getArtifactId() );
145         generateIntermediateApk();
146 
147         final File outputFile = createAarLibraryFile( createAarClassesJar() );
148 
149         if ( classifier == null )
150         {
151             // Set the generated file as the main artifact (because the pom states <packaging>aar</packaging>)
152             project.getArtifact().setFile( outputFile );
153         }
154         else
155         {
156             // If there is a classifier specified, attach the artifact using that
157             projectHelper.attachArtifact( project, AndroidExtension.AAR, classifier, outputFile );
158         }
159     }
160 
161     /**
162      * Creates an appropriate aar/classes.jar that does not include R
163      *
164      * @return File which is the AAR classes jar.
165      * @throws MojoExecutionException
166      */
167     protected File createAarClassesJar() throws MojoExecutionException
168     {
169         final File obfuscatedJarFile = new File( obfuscatedJar );
170         if ( obfuscatedJarFile.exists() )
171         {
172             attachJar( obfuscatedJarFile );
173             return obfuscatedJarFile;
174         }
175 
176         final File classesJar = new File( targetDirectory, finalName + ".aar.classes.jar" );
177         try
178         {
179             JarArchiver jarArchiver = new JarArchiver();
180             jarArchiver.setDestFile( classesJar );
181             jarArchiver.addDirectory( projectOutputDirectory,
182                     classesJarIncludes,
183                     classesJarExcludes );
184             jarArchiver.createArchive();
185             attachJar( classesJar );
186             return classesJar;
187         }
188         catch ( ArchiverException e )
189         {
190             throw new MojoExecutionException( "ArchiverException while creating ." + classesJar + " file.", e );
191         }
192         catch ( IOException e )
193         {
194             throw new MojoExecutionException( "IOException while creating ." + classesJar + " file.", e );
195         }
196 
197     }
198 
199     private void attachJar( File jarFile )
200     {
201         if ( attachJar )
202         {
203             projectHelper.attachArtifact( project, "jar", project.getArtifact().getClassifier(), jarFile );
204         }
205     }
206 
207     /**
208      * @return AAR file.
209      * @throws MojoExecutionException
210      */
211     protected File createAarLibraryFile( File classesJar ) throws MojoExecutionException
212     {
213         final File aarLibrary = new File( targetDirectory,
214                 finalName + "." + AAR );
215         FileUtils.deleteQuietly( aarLibrary );
216 
217         try
218         {
219             final ZipArchiver zipArchiver = new ZipArchiver();
220             zipArchiver.setDestFile( aarLibrary );
221 
222             zipArchiver.addFile( destinationManifestFile, "AndroidManifest.xml" );
223             addDirectory( zipArchiver, assetsDirectory, "assets", false );
224 
225             // res folder must be included in the archive even if empty or non-existent.
226             if ( !resourceDirectory.exists() )
227             {
228                 resourceDirectory.mkdir();
229             }
230             addDirectory( zipArchiver, resourceDirectory, "res", true );
231 
232             zipArchiver.addFile( classesJar, SdkConstants.FN_CLASSES_JAR );
233 
234             final File[] overlayDirectories = getResourceOverlayDirectories();
235             for ( final File resOverlayDir : overlayDirectories )
236             {
237                 if ( resOverlayDir != null && resOverlayDir.exists() )
238                 {
239                     addDirectory( zipArchiver, resOverlayDir, "res", false );
240                 }
241             }
242 
243             if ( consumerProguardFiles != null )
244             {
245                 final File mergedConsumerProguardFile = new File( targetDirectory, "consumer-proguard.txt" );
246                 if ( mergedConsumerProguardFile.exists() )
247                 {
248                     FileUtils.forceDelete( mergedConsumerProguardFile );
249                 }
250                 mergedConsumerProguardFile.createNewFile();
251                 StringBuilder mergedConsumerProguardFileBuilder = new StringBuilder();
252                 for ( File consumerProguardFile : consumerProguardFiles )
253                 {
254                     if ( consumerProguardFile.exists() )
255                     {
256                         getLog().info( "Adding consumer proguard file " + consumerProguardFile );
257                         FileInputStream consumerProguardFileInputStream = null;
258                         try
259                         {
260                             consumerProguardFileInputStream = new FileInputStream( consumerProguardFile );
261                             mergedConsumerProguardFileBuilder.append(
262                                 IOUtils.toString( consumerProguardFileInputStream ) );
263                             mergedConsumerProguardFileBuilder.append( SystemUtils.LINE_SEPARATOR );
264                         }
265                         catch ( IOException e )
266                         {
267                             throw new MojoExecutionException( "Error writing consumer proguard file ", e );
268                         }
269                         finally
270                         {
271                             IOUtils.closeQuietly( consumerProguardFileInputStream );
272                         }
273                     }
274                 }
275                 FileOutputStream mergedConsumerProguardFileOutputStream = null;
276                 try
277                 {
278                     mergedConsumerProguardFileOutputStream = new FileOutputStream( mergedConsumerProguardFile );
279                     IOUtils.write( mergedConsumerProguardFileBuilder, mergedConsumerProguardFileOutputStream );
280                 }
281                 catch ( IOException e )
282                 {
283                     throw new MojoExecutionException( "Error writing consumer proguard file ", e );
284                 }
285                 finally
286                 {
287                     IOUtils.closeQuietly( mergedConsumerProguardFileOutputStream );
288                 }
289 
290                 zipArchiver.addFile( mergedConsumerProguardFile, "proguard.txt" );
291             }
292 
293             addR( zipArchiver );
294 
295             // Lastly, add any native libraries
296             addNativeLibraries( zipArchiver );
297 
298             zipArchiver.createArchive();
299         }
300         catch ( ArchiverException e )
301         {
302             throw new MojoExecutionException( "ArchiverException while creating ." + AAR + " file.", e );
303         }
304         catch ( IOException e )
305         {
306             throw new MojoExecutionException( "IOException while creating ." + AAR + " file.", e );
307         }
308 
309         return aarLibrary;
310     }
311 
312     private void addR( ZipArchiver zipArchiver ) throws MojoExecutionException, IOException
313     {
314         final File rFile = new File( targetDirectory, "R.txt" );
315         if ( !rFile.exists() )
316         {
317             getLog().debug( "No resources - creating empty R.txt" );
318             if ( !rFile.createNewFile() )
319             {
320                 getLog().warn( "Unable to create R.txt in AAR" );
321             }
322         }
323         zipArchiver.addFile( rFile, "R.txt" );
324         getLog().debug( "Packaging R.txt in AAR" );
325     }
326 
327     private void addNativeLibraries( final ZipArchiver zipArchiver ) throws MojoExecutionException
328     {
329         try
330         {
331             if ( nativeLibrariesDirectory.exists() )
332             {
333                 getLog().info( nativeLibrariesDirectory + " exists, adding libraries." );
334                 addDirectory( zipArchiver, nativeLibrariesDirectory, NATIVE_LIBRARIES_FOLDER, false );
335             }
336             else
337             {
338                 getLog().info( nativeLibrariesDirectory
339                         + " does not exist, looking for libraries in target directory." );
340                 // Add native libraries built and attached in this build
341                 String[] ndkArchitectures = NativeHelper.getNdkArchitectures( ndkArchitecture,
342                                                                               applicationMakefile,
343                                                                               project.getBasedir() );
344                 for ( String architecture : ndkArchitectures )
345                 {
346                     final File ndkLibsDirectory = new File( ndkOutputDirectory, architecture );
347                     addSharedLibraries( zipArchiver, ndkLibsDirectory, architecture );
348 
349                     // Add native library dependencies
350                     // FIXME: Remove as causes duplicate libraries when building final APK if this set includes
351                     //        libraries from dependencies of the AAR
352                     //final File dependentLibs = new File( ndkOutputDirectory.getAbsolutePath(), ndkArchitecture );
353                     //addSharedLibraries( jarArchiver, dependentLibs, prefix );
354                 }
355             }
356         }
357         catch ( ArchiverException e )
358         {
359             throw new MojoExecutionException( "IOException while creating ." + AAR + " file.", e );
360         }
361         // TODO: Next is to check for any:
362         // TODO: - compiled in (as part of this build) libs
363         // TODO:    - That is of course easy if the artifact is indeed attached
364         // TODO:    - If not attached, it gets a little trickier  - check the target dir for any compiled .so files (generated by NDK mojo)
365         // TODO:        - But where is that directory configured?
366     }
367 
368     /**
369      * Makes sure the string ends with "/"
370      *
371      * @param prefix any string, or null.
372      * @return the prefix with a "/" at the end, never null.
373      */
374     protected String endWithSlash( String prefix )
375     {
376         prefix = StringUtils.defaultIfEmpty( prefix, "/" );
377         if ( ! prefix.endsWith( "/" ) )
378         {
379             prefix = prefix + "/";
380         }
381         return prefix;
382     }
383 
384     /**
385      * Adds a directory to a {@link JarArchiver} with a directory prefix.
386      *
387      * @param zipArchiver   ZipArchiver to use to archive the file.
388      * @param directory     The directory to add.
389      * @param prefix        An optional prefix for where in the Jar file the directory's contents should go.
390      * @param includeEmptyFolders   Whether to include an entry for empty folder in the archive.
391      */
392     protected void addDirectory( ZipArchiver zipArchiver, File directory, String prefix, boolean includeEmptyFolders )
393     {
394         if ( directory != null && directory.exists() )
395         {
396             final DefaultFileSet fileSet = new DefaultFileSet();
397             fileSet.setPrefix( endWithSlash( prefix ) );
398             fileSet.setDirectory( directory );
399             fileSet.setIncludingEmptyDirectories( includeEmptyFolders );
400             zipArchiver.addFileSet( fileSet );
401             getLog().debug( "Added files from " + directory );
402         }
403     }
404 
405     /**
406      * Adds all shared libraries (.so) to a {@link JarArchiver} under 'jni'.
407      *
408      * @param zipArchiver The jarArchiver to add files to
409      * @param directory   The directory to scan for .so files
410      * @param architecture      The prefix for where in the jar the .so files will go.
411      */
412     protected void addSharedLibraries( ZipArchiver zipArchiver, File directory, String architecture )
413     {
414         getLog().debug( "Searching for shared libraries in " + directory );
415         File[] libFiles = directory.listFiles( new FilenameFilter()
416         {
417             public boolean accept( final File dir, final String name )
418             {
419                 return name.startsWith( "lib" ) && name.endsWith( ".so" );
420             }
421         } );
422 
423         if ( libFiles != null )
424         {
425             for ( File libFile : libFiles )
426             {
427                 String dest = NATIVE_LIBRARIES_FOLDER + "/" + architecture + "/" + libFile.getName();
428                 getLog().debug( "Adding " + libFile + " as " + dest );
429                 zipArchiver.addFile( libFile, dest );
430             }
431         }
432     }
433 
434     /**
435      * Generates an intermediate apk file (actually .ap_) containing the resources and assets.
436      *
437      * @throws MojoExecutionException
438      */
439     private void generateIntermediateApk() throws MojoExecutionException
440     {
441         // Have to generate the AAR against the dependent resources or build will fail if any local resources
442         // directly reference any of the dependent resources. NB this does NOT include the dep resources in the AAR.
443         List<File> dependenciesResDirectories = new ArrayList<File>();
444         for ( Artifact libraryArtifact : getTransitiveDependencyArtifacts( APKLIB, AAR ) )
445         {
446             final File apkLibResDir = getUnpackedLibResourceFolder( libraryArtifact );
447             if ( apkLibResDir.exists() )
448             {
449                 dependenciesResDirectories.add( apkLibResDir );
450             }
451         }
452 
453         final CommandExecutor executor = CommandExecutor.Factory.createDefaultCommmandExecutor();
454         executor.setLogger( this.getLog() );
455 
456         File outputFile = new File( targetDirectory, finalName + ".ap_" );
457 
458         final AaptCommandBuilder commandBuilder = AaptCommandBuilder
459                 .packageResources( getLog() )
460                 .makePackageDirectories()
461                 .forceOverwriteExistingFiles()
462                 .setPathToAndroidManifest( destinationManifestFile )
463                 .addResourceDirectoriesIfExists( getResourceOverlayDirectories() )
464                 .addResourceDirectoryIfExists( resourceDirectory )
465                 .addResourceDirectoriesIfExists( dependenciesResDirectories )
466                 .autoAddOverlay()
467                 .addRawAssetsDirectoryIfExists( combinedAssets )
468                 .addExistingPackageToBaseIncludeSet( getAndroidSdk().getAndroidJar() )
469                 .setOutputApkFile( outputFile )
470                 .addConfigurations( configurations )
471                 .setResourceConstantsFolder( genDirectory )
472                 .makeResourcesNonConstant()
473                 .generateRTextFile( targetDirectory )
474                 .setVerbose( aaptVerbose );
475 
476         getLog().debug( getAndroidSdk().getAaptPath() + " " + commandBuilder.toString() );
477         getLog().info( "Generating aar" );
478         try
479         {
480             executor.setCaptureStdOut( true );
481             final List<String> commands = commandBuilder.build();
482             executor.executeCommand( getAndroidSdk().getAaptPath(), commands, project.getBasedir(), false );
483         }
484         catch ( ExecutionException e )
485         {
486             throw new MojoExecutionException( "", e );
487         }
488     }
489 }