/* * Copyright (C) 2009 Jayway AB * Copyright (C) 2007-2008 JVending Masa * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.simpligility.maven.plugins.android.phase09package; import com.android.sdklib.build.ApkBuilder; import com.android.sdklib.build.ApkCreationException; import com.android.sdklib.build.DuplicateFileException; import com.android.sdklib.build.SealedApkException; import com.simpligility.maven.plugins.android.AbstractAndroidMojo; import com.google.common.io.Files; import com.simpligility.maven.plugins.android.AndroidNdk; import com.simpligility.maven.plugins.android.AndroidSigner; import com.simpligility.maven.plugins.android.IncludeExcludeSet; import com.simpligility.maven.plugins.android.CommandExecutor; import com.simpligility.maven.plugins.android.ExecutionException; import com.simpligility.maven.plugins.android.common.AaptCommandBuilder; import com.simpligility.maven.plugins.android.common.AndroidExtension; import com.simpligility.maven.plugins.android.common.NativeHelper; import com.simpligility.maven.plugins.android.config.ConfigHandler; import com.simpligility.maven.plugins.android.config.ConfigPojo; import com.simpligility.maven.plugins.android.config.PullParameter; import com.simpligility.maven.plugins.android.configuration.Apk; import com.simpligility.maven.plugins.android.configuration.MetaInf; import com.simpligility.maven.plugins.android.configuration.Sign; import org.apache.commons.io.FileUtils; import org.apache.commons.io.filefilter.DirectoryFileFilter; import org.apache.commons.io.filefilter.FileFileFilter; import org.apache.commons.io.filefilter.FileFilterUtils; import org.apache.commons.io.filefilter.IOFileFilter; import org.apache.maven.artifact.Artifact; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.annotations.ResolutionScope; import org.apache.maven.plugins.shade.resource.ResourceTransformer; import java.io.File; import java.io.FileFilter; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.jar.JarOutputStream; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipOutputStream; import static com.simpligility.maven.plugins.android.InclusionExclusionResolver.filterArtifacts; import static com.simpligility.maven.plugins.android.common.AndroidExtension.AAR; import static com.simpligility.maven.plugins.android.common.AndroidExtension.APK; import static com.simpligility.maven.plugins.android.common.AndroidExtension.APKLIB; /** * Creates the apk file. By default signs it with debug keystore.<br> * Change that by setting configuration parameter * <code><sign><debug>false</debug></sign></code>. * * @author hugo.josefson@jayway.com */ @Mojo( name = "apk", defaultPhase = LifecyclePhase.PACKAGE, requiresDependencyResolution = ResolutionScope.COMPILE ) public class ApkMojo extends AbstractAndroidMojo { /** * <p>How to sign the apk.</p> * <p>Looks like this:</p> * <pre> * <sign> * <debug>auto</debug> * </sign> * </pre> * <p>Valid values for <code><debug></code> are: * <ul> * <li><code>true</code> = sign with the debug keystore. * <li><code>false</code> = don't sign with the debug keystore. * <li><code>both</code> = create a signed as well as an unsigned apk. * <li><code>auto</code> (default) = sign with debug keystore, unless another keystore is defined. (Signing with * other keystores is not yet implemented. See * <a href="http://code.google.com/p/maven-android-plugin/issues/detail?id=2">Issue 2</a>.) * </ul></p> * <p>Can also be configured from command-line with parameter <code>-Dandroid.sign.debug</code>.</p> */ @Parameter private Sign sign; /** * <p>Parameter designed to pick up <code>-Dandroid.sign.debug</code> in case there is no pom with a * <code><sign></code> configuration tag.</p> * <p>Corresponds to {@link com.simpligility.maven.plugins.android.configuration.Sign#debug}.</p> */ @Parameter( property = "android.sign.debug", defaultValue = "auto", readonly = true ) private String signDebug; /** * <p>Rewrite the manifest so that all of its instrumentation components target the given package. * This value will be passed on to the aapt parameter --rename-instrumentation-target-package. * Look to aapt for more help on this. </p> * * TODO pass this into AaptExecutor */ @Parameter( property = "android.renameInstrumentationTargetPackage" ) private String renameInstrumentationTargetPackage; /** * <p>Allows to detect and extract the duplicate files from embedded jars. In that case, the plugin analyzes * the content of all embedded dependencies and checks they are no duplicates inside those dependencies. Indeed, * Android does not support duplicates, and all dependencies are inlined in the APK. If duplicates files are found, * the resource is kept in the first dependency and removes from others. */ @Parameter( property = "android.extractDuplicates", defaultValue = "false" ) private boolean extractDuplicates; /** * <p>Classifier to add to the artifact generated. If given, the artifact will be an attachment instead.</p> */ @Parameter private String classifier; /** * The apk file produced by the apk goal. Per default the file is placed into the build directory (target * normally) using the build final name and apk as extension. */ @Parameter( property = "android.outputApk", defaultValue = "${project.build.directory}/${project.build.finalName}.apk" ) private String outputApk; /** * <p>Additional source directories that contain resources to be packaged into the apk.</p> * <p>These are not source directories, that contain java classes to be compiled. * It corresponds to the -df option of the apkbuilder program. It allows you to specify directories, * that contain additional resources to be packaged into the apk. </p> * So an example inside the plugin configuration could be: * <pre> * <configuration> * ... * <sourceDirectories> * <sourceDirectory>${project.basedir}/additionals</sourceDirectory> * </sourceDirectories> * ... * </configuration> * </pre> */ @Parameter( property = "android.sourceDirectories" ) private File[] sourceDirectories; /** * Pattern for additional META-INF resources to be packaged into the apk. * <p> * The APK builder filters these resources and doesn't include them into the apk. * This leads to bad behaviour of dependent libraries relying on these resources, * for instance service discovery doesn't work.<br> * By specifying this pattern, the android plugin adds these resources to the final apk. * </p> * <p>The pattern is relative to META-INF, i.e. one must use * <pre> * <code> * <apkMetaIncludes> * <metaInclude>services/**</metaInclude> * </apkMetaIncludes> * </code> * </pre> * ... instead of * <pre> * <code> * <apkMetaIncludes> * <metaInclude>META-INF/services/**</metaInclude> * </apkMetaIncludes> * </code> * </pre> * <p> * See also <a href="http://code.google.com/p/maven-android-plugin/issues/detail?id=97">Issue 97</a> * </p> * * @deprecated in favour of apk.metaInf */ @PullParameter private String[] apkMetaIncludes; @PullParameter( defaultValueGetterMethod = "getDefaultMetaInf" ) private MetaInf apkMetaInf; @Parameter( alias = "metaInf" ) private MetaInf pluginMetaInf; /** * Defines whether or not the APK is being produced in debug mode or not. */ @Parameter( property = "android.apk.debug" ) @PullParameter( defaultValue = "false" ) private Boolean apkDebug; @Parameter( property = "android.nativeToolchain" ) @PullParameter( defaultValue = "arm-linux-androideabi-4.4.3" ) private String apkNativeToolchain; /** * Specifies the final name of the library output by the build (this allows */ @Parameter( property = "android.ndk.build.build.final-library.name" ) private String ndkFinalLibraryName; /** * Specify a list of patterns that are matched against the names of jar file * dependencies. Matching jar files will not have their resources added to the * resulting APK. * * The patterns are standard Java regexes. */ @Parameter private String[] excludeJarResources; private Pattern[] excludeJarResourcesPatterns; /** * Embedded configuration of this mojo. */ @Parameter @ConfigPojo( prefix = "apk" ) private Apk apk; /** * Skips transitive dependencies. May be useful if the target classes directory is populated with the * {@code maven-dependency-plugin} and already contains all dependency classes. */ @Parameter( property = "skipDependencies", defaultValue = "false" ) private boolean skipDependencies; /** * Allows to include or exclude artifacts by type. The {@code include} parameter has higher priority than the * {@code exclude} parameter. These two parameters can be overridden by the {@code artifactSet} parameter. Empty * strings are ignored. Example: * <pre> * <artifactTypeSet> * <includes> * <include>aar</include> * <includes> * <excludes> * <exclude>jar</exclude> * <excludes> * </artifactTypeSet> * </pre> */ @Parameter( property = "artifactTypeSet" ) private IncludeExcludeSet artifactTypeSet; /** * Allows to include or exclude artifacts by {@code groupId}, {@code artifactId}, and {@code versionId}. The * {@code include} parameter has higher priority than the {@code exclude} parameter. These two parameters can * override the {@code artifactTypeSet} and {@code skipDependencies} parameters. Artifact {@code groupId}, * {@code artifactId}, and {@code versionId} are specified by a string with the respective values separated using * a colon character {@code :}. {@code artifactId} and {@code versionId} can be optional covering an artifact * range. Empty strings are ignored. Example: * <pre> * <artifactTypeSet> * <includes> * <include>foo-group:foo-artifact:1.0-SNAPSHOT</include> * <include>bar-group:bar-artifact:1.0-SNAPSHOT</include> * <include>baz-group:*</include> * <includes> * <excludes> * <exclude>qux-group:qux-artifact:*</exclude> * <excludes> * </artifactTypeSet> * </pre> */ @Parameter( property = "artifactSet" ) private IncludeExcludeSet artifactSet; private static final Pattern PATTERN_JAR_EXT = Pattern.compile( "^.+\\.jar$", Pattern.CASE_INSENSITIVE ); private static final String DEX_SUFFIX = ".dex"; private static final String CLASSES = "classes"; /** * <p>Default hardware architecture for native library dependencies (with {@code <type>so</type>}) * without a classifier.</p> * <p>Valid values currently include {@code armeabi}, {@code armeabi-v7a}, {@code mips} and {@code x86}.</p> */ @Parameter( property = "android.nativeLibrariesDependenciesHardwareArchitectureDefault", defaultValue = "armeabi" ) private String nativeLibrariesDependenciesHardwareArchitectureDefault; @Parameter private ResourceTransformer[] transformers; /** * @throws MojoExecutionException * @throws MojoFailureException */ public void execute() throws MojoExecutionException, MojoFailureException { // Make an early exit if we're not supposed to generate the APK if ( ! generateApk ) { return; } ConfigHandler cfh = new ConfigHandler( this, this.session, this.execution ); cfh.parseConfiguration(); generateIntermediateApk(); // Compile resource exclusion patterns, if any if ( excludeJarResources != null && excludeJarResources.length > 0 ) { getLog().debug( "Compiling " + excludeJarResources.length + " patterns" ); excludeJarResourcesPatterns = new Pattern[excludeJarResources.length]; for ( int index = 0; index < excludeJarResources.length; ++index ) { excludeJarResourcesPatterns[index] = Pattern.compile( excludeJarResources[index] ); } } // Initialize apk build configuration File outputFile = new File( outputApk ); final boolean signWithDebugKeyStore = getAndroidSigner().isSignWithDebugKeyStore(); if ( getAndroidSigner().shouldCreateBothSignedAndUnsignedApk() ) { getLog().info( "Creating debug key signed apk file " + outputFile ); createApkFile( outputFile, true ); final File unsignedOutputFile = new File( targetDirectory, finalName + "-unsigned." + APK ); getLog().info( "Creating additional unsigned apk file " + unsignedOutputFile ); createApkFile( unsignedOutputFile, false ); projectHelper.attachArtifact( project, unsignedOutputFile, classifier == null ? "unsigned" : classifier + "_unsigned" ); } else { createApkFile( outputFile, signWithDebugKeyStore ); } if ( classifier == null ) { // Set the generated .apk file as the main artifact (because the pom states <packaging>apk</packaging>) project.getArtifact().setFile( outputFile ); } else { // If there is a classifier specified, attach the artifact using that projectHelper.attachArtifact( project, AndroidExtension.APK, classifier, outputFile ); } } void createApkFile( File outputFile, boolean signWithDebugKeyStore ) throws MojoExecutionException { //this needs to come from DexMojo File dexFile = new File( targetDirectory, "classes.dex" ); if ( !dexFile.exists() ) { dexFile = new File( targetDirectory, "classes.zip" ); } File zipArchive = new File( targetDirectory, finalName + ".ap_" ); ArrayList<File> sourceFolders = new ArrayList<File>(); if ( sourceDirectories != null ) { sourceFolders.addAll( Arrays.asList( sourceDirectories ) ); } ArrayList<File> jarFiles = new ArrayList<File>(); // Process the native libraries, looking both in the current build directory as well as // at the dependencies declared in the pom. Currently, all .so files are automatically included final Collection<File> nativeFolders = getNativeLibraryFolders(); getLog().info( "Adding native libraries : " + nativeFolders ); doAPKWithAPKBuilder( outputFile, dexFile, zipArchive, sourceFolders, jarFiles, nativeFolders, signWithDebugKeyStore ); if ( this.apkMetaInf != null ) { File outputJar = new File( outputApk.substring( 0, outputApk.length() - 3 ) + "jar" ); if ( outputJar.exists() ) { jarFiles.add( outputJar ); } else { getLog().warn( "Output jar doesn't exist:" + outputJar ); } try { addMetaInf( outputFile, jarFiles ); } catch ( IOException e ) { throw new MojoExecutionException( "Could not add META-INF resources.", e ); } } } private void addMetaInf( File outputFile, ArrayList<File> jarFiles ) throws IOException { File tmp = File.createTempFile( outputFile.getName(), ".add", outputFile.getParentFile() ); FileOutputStream fos = new FileOutputStream( tmp ); JarOutputStream zos = new JarOutputStream( fos ); Set<String> entries = new HashSet<String>(); updateWithMetaInf( zos, outputFile, entries, false ); for ( File f : jarFiles ) { updateWithMetaInf( zos, f, entries, true ); } if ( transformers != null ) { for ( ResourceTransformer transformer : transformers ) { if ( transformer.hasTransformedResource() ) { transformer.modifyOutputStream( zos ); } } } zos.close(); outputFile.delete(); if ( ! tmp.renameTo( outputFile ) ) { throw new IOException( String.format( "Cannot rename %s to %s", tmp, outputFile.getName() ) ); } } private void updateWithMetaInf( ZipOutputStream zos, File jarFile, Set<String> entries, boolean metaInfOnly ) throws IOException { ZipFile zin = new ZipFile( jarFile ); for ( Enumeration<? extends ZipEntry> en = zin.entries(); en.hasMoreElements(); ) { ZipEntry ze = en.nextElement(); if ( ze.isDirectory() ) { continue; } String zn = ze.getName(); if ( metaInfOnly ) { if ( ! zn.startsWith( "META-INF/" ) ) { continue; } if ( ! this.apkMetaInf.isIncluded( zn ) ) { continue; } } boolean resourceTransformed = false; if ( transformers != null ) { for ( ResourceTransformer transformer : transformers ) { if ( transformer.canTransformResource( zn ) ) { getLog().info( "Transforming " + zn + " using " + transformer.getClass().getName() ); InputStream is = zin.getInputStream( ze ); transformer.processResource( zn, is, null ); is.close(); resourceTransformed = true; break; } } } if ( !resourceTransformed ) { // Avoid duplicates that aren't accounted for by the resource transformers if ( metaInfOnly && this.extractDuplicates && ! entries.add( zn ) ) { continue; } InputStream is = zin.getInputStream( ze ); final ZipEntry ne; if ( ze.getMethod() == ZipEntry.STORED ) { ne = new ZipEntry( ze ); } else { ne = new ZipEntry( zn ); } zos.putNextEntry( ne ); copyStreamWithoutClosing( is, zos ); is.close(); zos.closeEntry(); } } zin.close(); } private Map<String, List<File>> jars = new HashMap<String, List<File>>(); private void computeDuplicateFiles( File jar ) throws IOException { ZipFile file = new ZipFile( jar ); Enumeration<? extends ZipEntry> list = file.entries(); while ( list.hasMoreElements() ) { ZipEntry ze = list.nextElement(); if ( ! ( ze.getName().contains( "META-INF/" ) || ze.isDirectory() ) ) { // Exclude META-INF and Directories List<File> l = jars.get( ze.getName() ); if ( l == null ) { l = new ArrayList<File>(); jars.put( ze.getName(), l ); } l.add( jar ); } } } private void computeDuplicateFilesInSource( File folder ) { String rPath = folder.getAbsolutePath(); for ( File file : Files.fileTreeTraverser().breadthFirstTraversal( folder ).toList() ) { String lPath = file.getAbsolutePath(); if ( lPath.equals( rPath ) ) { continue; //skip the root } lPath = lPath.substring( rPath.length() + 1 ); //strip root folder to make relative path if ( jars.get( lPath ) == null ) { jars.put( lPath, new ArrayList<File>() ); } jars.get( lPath ).add( folder ); } } private void extractDuplicateFiles( List<File> jarFiles, Collection<File> sourceFolders ) throws IOException { getLog().debug( "Extracting duplicates" ); List<String> duplicates = new ArrayList<String>(); List<File> jarToModify = new ArrayList<File>(); for ( String s : jars.keySet() ) { List<File> l = jars.get( s ); if ( l.size() > 1 ) { getLog().warn( "Duplicate file " + s + " : " + l ); duplicates.add( s ); for ( int i = 0; i < l.size(); i++ ) { if ( ! jarToModify.contains( l.get( i ) ) ) { jarToModify.add( l.get( i ) ); } } } } // Rebuild jars. Remove duplicates from ALL jars, then add them back into a duplicate-resources.jar File tmp = new File( targetDirectory.getAbsolutePath(), "unpacked-embedded-jars" ); tmp.mkdirs(); File duplicatesJar = new File( tmp, "duplicate-resources.jar" ); Set<String> duplicatesAdded = new HashSet<String>(); duplicatesJar.createNewFile(); final FileOutputStream fos = new FileOutputStream( duplicatesJar ); final JarOutputStream zos = new JarOutputStream( fos ); for ( File file : jarToModify ) { final int index = jarFiles.indexOf( file ); if ( index != -1 ) { final File newJar = removeDuplicatesFromJar( file, duplicates, duplicatesAdded, zos, index ); getLog().debug( "Removed duplicates from " + newJar ); if ( newJar != null ) { jarFiles.set( index, newJar ); } } else { removeDuplicatesFromFolder( file, file, duplicates, duplicatesAdded, zos ); getLog().debug( "Removed duplicates from " + file ); } } //add transformed resources to duplicate-resources.jar if ( transformers != null ) { for ( ResourceTransformer transformer : transformers ) { if ( transformer.hasTransformedResource() ) { transformer.modifyOutputStream( zos ); } } } zos.close(); fos.close(); if ( !jarToModify.isEmpty() && duplicatesJar.length() > 0 ) { jarFiles.add( duplicatesJar ); } } /** * Creates the APK file using the internal APKBuilder. * * @param outputFile the output file * @param dexFile the dex file * @param zipArchive the classes folder * @param sourceFolders the resources * @param jarFiles the embedded java files * @param nativeFolders the native folders * @param signWithDebugKeyStore enables the signature of the APK using the debug key * @throws MojoExecutionException if the APK cannot be created. */ private void doAPKWithAPKBuilder( File outputFile, File dexFile, File zipArchive, Collection<File> sourceFolders, List<File> jarFiles, Collection<File> nativeFolders, boolean signWithDebugKeyStore ) throws MojoExecutionException { getLog().debug( "Building APK with internal APKBuilder" ); //A when jack is running the classes directory will not get filled (usually) // so let's skip it if it wasn't created by something else if ( projectOutputDirectory.exists() || !getJack().isEnabled() ) { sourceFolders.add( projectOutputDirectory ); } for ( Artifact artifact : filterArtifacts( getRelevantCompileArtifacts(), skipDependencies, artifactTypeSet.getIncludes(), artifactTypeSet.getExcludes(), artifactSet.getIncludes(), artifactSet.getExcludes() ) ) { getLog().debug( "Found artifact for APK :" + artifact ); if ( extractDuplicates ) { try { computeDuplicateFiles( artifact.getFile() ); } catch ( Exception e ) { getLog().warn( "Cannot compute duplicates files from " + artifact.getFile().getAbsolutePath(), e ); } } jarFiles.add( artifact.getFile() ); } for ( File src : sourceFolders ) { computeDuplicateFilesInSource( src ); } // Check duplicates. if ( extractDuplicates ) { try { extractDuplicateFiles( jarFiles, sourceFolders ); } catch ( IOException e ) { getLog().error( "Could not extract duplicates to duplicate-resources.jar", e ); } } try { final String debugKeyStore = signWithDebugKeyStore ? ApkBuilder.getDebugKeystore() : null; final ApkBuilder apkBuilder = new ApkBuilder( outputFile, zipArchive, dexFile, debugKeyStore, null ); if ( apkDebug ) { apkBuilder.setDebugMode( true ); } for ( File sourceFolder : sourceFolders ) { getLog().debug( "Adding source folder : " + sourceFolder ); apkBuilder.addSourceFolder( sourceFolder ); } for ( File jarFile : jarFiles ) { boolean excluded = false; if ( excludeJarResourcesPatterns != null ) { final String name = jarFile.getName(); getLog().debug( "Checking " + name + " against patterns" ); for ( Pattern pattern : excludeJarResourcesPatterns ) { final Matcher matcher = pattern.matcher( name ); if ( matcher.matches() ) { getLog().debug( "Jar " + name + " excluded by pattern " + pattern ); excluded = true; break; } else { getLog().debug( "Jar " + name + " not excluded by pattern " + pattern ); } } } if ( excluded ) { continue; } if ( jarFile.isDirectory() ) { getLog().debug( "Adding resources from jar folder : " + jarFile ); final String[] filenames = jarFile.list( new FilenameFilter() { public boolean accept( File dir, String name ) { return PATTERN_JAR_EXT.matcher( name ).matches(); } } ); for ( String filename : filenames ) { final File innerJar = new File( jarFile, filename ); getLog().debug( "Adding resources from innerJar : " + innerJar ); apkBuilder.addResourcesFromJar( innerJar ); } } else { getLog().debug( "Adding resources from : " + jarFile ); apkBuilder.addResourcesFromJar( jarFile ); } } addSecondaryDexes( dexFile, apkBuilder ); for ( File nativeFolder : nativeFolders ) { getLog().debug( "Adding native library : " + nativeFolder ); apkBuilder.addNativeLibraries( nativeFolder ); } apkBuilder.sealApk(); } catch ( ApkCreationException e ) { throw new MojoExecutionException( e.getMessage() ); } catch ( DuplicateFileException e ) { final String msg = String.format( "Duplicated file: %s, found in archive %s and %s", e.getArchivePath(), e.getFile1(), e.getFile2() ); throw new MojoExecutionException( msg, e ); } catch ( SealedApkException e ) { throw new MojoExecutionException( e.getMessage() ); } } private void addSecondaryDexes( File dexFile, ApkBuilder apkBuilder ) throws ApkCreationException, SealedApkException, DuplicateFileException { int dexNumber = 2; String dexFileName = getNextDexFileName( dexNumber ); File secondDexFile = createNextDexFile( dexFile, dexFileName ); while ( secondDexFile.exists() ) { apkBuilder.addFile( secondDexFile, dexFileName ); dexNumber++; dexFileName = getNextDexFileName( dexNumber ); secondDexFile = createNextDexFile( dexFile, dexFileName ); } } private File createNextDexFile( File dexFile, String dexFileName ) { return new File( dexFile.getParentFile(), dexFileName ); } private String getNextDexFileName( int dexNumber ) { return CLASSES + dexNumber + DEX_SUFFIX; } private File removeDuplicatesFromJar( File in, List<String> duplicates, Set<String> duplicatesAdded, ZipOutputStream duplicateZos, int num ) { String target = targetDirectory.getAbsolutePath(); File tmp = new File( target, "unpacked-embedded-jars" ); tmp.mkdirs(); String jarName = String.format( "%s-%d.%s", Files.getNameWithoutExtension( in.getName() ), num, Files.getFileExtension( in.getName() ) ); File out = new File( tmp, jarName ); if ( out.exists() ) { return out; } else { try { out.createNewFile(); } catch ( IOException e ) { e.printStackTrace(); } } // Create a new Jar file final FileOutputStream fos; final ZipOutputStream jos; try { fos = new FileOutputStream( out ); jos = new ZipOutputStream( fos ); } catch ( FileNotFoundException e1 ) { getLog().error( "Cannot remove duplicates : the output file " + out.getAbsolutePath() + " does not found" ); return null; } final ZipFile inZip; try { inZip = new ZipFile( in ); Enumeration<? extends ZipEntry> entries = inZip.entries(); while ( entries.hasMoreElements() ) { ZipEntry entry = entries.nextElement(); // If the entry is not a duplicate, copy. if ( ! duplicates.contains( entry.getName() ) ) { // copy the entry header to jos jos.putNextEntry( entry ); InputStream currIn = inZip.getInputStream( entry ); copyStreamWithoutClosing( currIn, jos ); currIn.close(); jos.closeEntry(); } //if it is duplicate, check the resource transformers else { boolean resourceTransformed = false; if ( transformers != null ) { for ( ResourceTransformer transformer : transformers ) { if ( transformer.canTransformResource( entry.getName() ) ) { getLog().info( "Transforming " + entry.getName() + " using " + transformer.getClass().getName() ); InputStream currIn = inZip.getInputStream( entry ); transformer.processResource( entry.getName(), currIn, null ); currIn.close(); resourceTransformed = true; break; } } } //if not handled by transformer, add (once) to duplicates jar if ( !resourceTransformed ) { if ( !duplicatesAdded.contains( entry.getName() ) ) { duplicatesAdded.add( entry.getName() ); duplicateZos.putNextEntry( entry ); InputStream currIn = inZip.getInputStream( entry ); copyStreamWithoutClosing( currIn, duplicateZos ); currIn.close(); duplicateZos.closeEntry(); } } } } } catch ( IOException e ) { getLog().error( "Cannot removing duplicates : " + e.getMessage() ); return null; } try { inZip.close(); jos.close(); fos.close(); } catch ( IOException e ) { // ignore it. } getLog().info( in.getName() + " rewritten without duplicates : " + out.getAbsolutePath() ); return out; } private void removeDuplicatesFromFolder( File root, File in, List<String> duplicates, Set<String> duplicatesAdded, ZipOutputStream duplicateZos ) { String rPath = root.getAbsolutePath(); try { for ( File f : in.listFiles() ) { if ( f.isDirectory() ) { removeDuplicatesFromFolder( root, f, duplicates, duplicatesAdded, duplicateZos ); } else { String lName = f.getAbsolutePath(); lName = lName.substring( rPath.length() + 1 ); //make relative path if ( duplicates.contains( lName ) ) { boolean resourceTransformed = false; if ( transformers != null ) { for ( ResourceTransformer transformer : transformers ) { if ( transformer.canTransformResource( lName ) ) { getLog().info( "Transforming " + lName + " using " + transformer.getClass().getName() ); InputStream currIn = new FileInputStream( f ); transformer.processResource( lName, currIn, null ); currIn.close(); resourceTransformed = true; break; } } } //if not handled by transformer, add (once) to duplicates jar if ( !resourceTransformed ) { if ( !duplicatesAdded.contains( lName ) ) { duplicatesAdded.add( lName ); ZipEntry entry = new ZipEntry( lName ); duplicateZos.putNextEntry( entry ); InputStream currIn = new FileInputStream( f ); copyStreamWithoutClosing( currIn, duplicateZos ); currIn.close(); duplicateZos.closeEntry(); } } f.delete(); } } } } catch ( IOException e ) { getLog().error( "Cannot removing duplicates : " + e.getMessage() ); } } /** * Copies an input stream into an output stream but does not close the streams. * * @param in the input stream * @param out the output stream * @throws IOException if the stream cannot be copied */ private static void copyStreamWithoutClosing( InputStream in, OutputStream out ) throws IOException { final int bufferSize = 4096; byte[] b = new byte[ bufferSize ]; int n; while ( ( n = in.read( b ) ) != - 1 ) { out.write( b, 0, n ); } } private Collection<File> getNativeLibraryFolders() throws MojoExecutionException { final List<File> natives = new ArrayList<File>(); if ( nativeLibrariesDirectory.exists() ) { // If we have prebuilt native libs then copy them over to the native output folder. // NB they will be copied over the top of any native libs generated as part of the NdkBuildMojo copyLocalNativeLibraries( nativeLibrariesDirectory, ndkOutputDirectory ); } final Set<Artifact> artifacts = getNativeLibraryArtifacts(); for ( Artifact resolvedArtifact : artifacts ) { if ( APKLIB.equals( resolvedArtifact.getType() ) || AAR.equals( resolvedArtifact.getType() ) ) { // If the artifact is an AAR or APKLIB then add their native libs folder to the result. final File folder = getUnpackedLibNativesFolder( resolvedArtifact ); getLog().debug( "Adding native library folder " + folder ); natives.add( folder ); } // Copy the native lib dependencies into the native lib output folder for ( String ndkArchitecture : AndroidNdk.NDK_ARCHITECTURES ) { if ( NativeHelper.artifactHasHardwareArchitecture( resolvedArtifact, ndkArchitecture, nativeLibrariesDependenciesHardwareArchitectureDefault ) ) { // If the artifact is a native lib then copy it into the native libs output folder. copyNativeLibraryArtifact( resolvedArtifact, ndkOutputDirectory, ndkArchitecture ); } } } if ( apkDebug ) { // Copy the gdbserver binary into the native libs output folder (for each architecture). for ( String ndkArchitecture : AndroidNdk.NDK_ARCHITECTURES ) { copyGdbServer( ndkOutputDirectory, ndkArchitecture ); } } if ( ndkOutputDirectory.exists() ) { // If we have any native libs in the native output folder then add the output folder to the result. getLog().debug( "Adding built native library folder " + ndkOutputDirectory ); natives.add( ndkOutputDirectory ); } return natives; } /** * @return Any native dependencies or attached artifacts. This may include artifacts from the ndk-build MOJO. * @throws MojoExecutionException */ private Set<Artifact> getNativeLibraryArtifacts() throws MojoExecutionException { return getNativeHelper().getNativeDependenciesArtifacts( this, getUnpackedLibsDirectory(), true ); } private void copyNativeLibraryArtifact( Artifact artifact, File destinationDirectory, String ndkArchitecture ) throws MojoExecutionException { final File artifactFile = getArtifactResolverHelper().resolveArtifactToFile( artifact ); try { final String artifactId = artifact.getArtifactId(); String filename = artifactId.startsWith( "lib" ) ? artifactId + ".so" : "lib" + artifactId + ".so"; if ( ndkFinalLibraryName != null && artifact.getFile().getName().startsWith( "lib" + ndkFinalLibraryName ) ) { // The artifact looks like one we built with the NDK in this module // preserve the name from the NDK build filename = artifact.getFile().getName(); } final File folder = new File( destinationDirectory, ndkArchitecture ); final File file = new File( folder, filename ); getLog().debug( "Copying native dependency " + artifactId + " (" + artifact.getGroupId() + ") to " + file ); FileUtils.copyFile( artifactFile, file ); } catch ( IOException e ) { throw new MojoExecutionException( "Could not copy native dependency.", e ); } } /** * Copy the Ndk GdbServer into the architecture output folder if the folder exists but the GdbServer doesn't. */ private void copyGdbServer( File destinationDirectory, String architecture ) throws MojoExecutionException { try { final File destDir = new File( destinationDirectory, architecture ); if ( destDir.exists() ) { // Copy the gdbserver binary to libs/<architecture>/ final File gdbServerFile = getAndroidNdk().getGdbServer( architecture ); final File destFile = new File( destDir, "gdbserver" ); if ( ! destFile.exists() ) { getLog().debug( "Copying gdbServer to " + destFile ); FileUtils.copyFile( gdbServerFile, destFile ); } else { getLog().info( "Note: gdbserver binary already exists at destination, will not copy over" ); } } } catch ( Exception e ) { getLog().error( "Error while copying gdbserver: " + e.getMessage(), e ); throw new MojoExecutionException( "Error while copying gdbserver: " + e.getMessage(), e ); } } private void copyLocalNativeLibraries( final File localNativeLibrariesDirectory, final File destinationDirectory ) throws MojoExecutionException { getLog().debug( "Copying existing native libraries from " + localNativeLibrariesDirectory ); try { IOFileFilter libSuffixFilter = FileFilterUtils.suffixFileFilter( ".so" ); IOFileFilter gdbserverNameFilter = FileFilterUtils.nameFileFilter( "gdbserver" ); IOFileFilter orFilter = FileFilterUtils.or( libSuffixFilter, gdbserverNameFilter ); IOFileFilter libFiles = FileFilterUtils.and( FileFileFilter.FILE, orFilter ); FileFilter filter = FileFilterUtils.or( DirectoryFileFilter.DIRECTORY, libFiles ); org.apache.commons.io.FileUtils .copyDirectory( localNativeLibrariesDirectory, destinationDirectory, filter ); } catch ( IOException e ) { getLog().error( "Could not copy native libraries: " + e.getMessage(), e ); throw new MojoExecutionException( "Could not copy native dependency.", e ); } } /** * Generates an intermediate apk file (actually .ap_) containing the resources and assets. * * @throws MojoExecutionException */ private void generateIntermediateApk() throws MojoExecutionException { CommandExecutor executor = CommandExecutor.Factory.createDefaultCommmandExecutor(); executor.setLogger( this.getLog() ); File[] overlayDirectories = getResourceOverlayDirectories(); File androidJar = getAndroidSdk().getAndroidJar(); File outputFile = new File( targetDirectory, finalName + ".ap_" ); List<File> dependencyArtifactResDirectoryList = new ArrayList<File>(); for ( Artifact libraryArtifact : getTransitiveDependencyArtifacts( APKLIB, AAR ) ) { final File libraryResDir = getUnpackedLibResourceFolder( libraryArtifact ); if ( libraryResDir.exists() ) { dependencyArtifactResDirectoryList.add( libraryResDir ); } } AaptCommandBuilder commandBuilder = AaptCommandBuilder .packageResources( getLog() ) .forceOverwriteExistingFiles() .setPathToAndroidManifest( destinationManifestFile ) .addResourceDirectoriesIfExists( overlayDirectories ) .addResourceDirectoryIfExists( resourceDirectory ) .addResourceDirectoriesIfExists( dependencyArtifactResDirectoryList ) .autoAddOverlay() // NB aapt only accepts a single assets parameter - combinedAssets is a merge of all assets .addRawAssetsDirectoryIfExists( combinedAssets ) .renameManifestPackage( renameManifestPackage ) .renameInstrumentationTargetPackage( renameInstrumentationTargetPackage ) .addExistingPackageToBaseIncludeSet( androidJar ) .setOutputApkFile( outputFile ) .addConfigurations( configurations ) .setVerbose( aaptVerbose ) .setDebugMode( !release ) .addExtraArguments( aaptExtraArgs ); getLog().debug( getAndroidSdk().getAaptPath() + " " + commandBuilder.toString() ); try { executor.setCaptureStdOut( true ); List<String> commands = commandBuilder.build(); executor.executeCommand( getAndroidSdk().getAaptPath(), commands, project.getBasedir(), false ); } catch ( ExecutionException e ) { throw new MojoExecutionException( "", e ); } } protected AndroidSigner getAndroidSigner() { if ( sign == null ) { return new AndroidSigner( signDebug ); } else { return new AndroidSigner( sign.getDebug() ); } } /** * Used to populated the {@link #apkMetaInf} attribute via reflection. */ private MetaInf getDefaultMetaInf() { // check for deprecated first if ( apkMetaIncludes != null && apkMetaIncludes.length > 0 ) { return new MetaInf().include( apkMetaIncludes ); } return this.pluginMetaInf; } }