com.jayway.maven.plugins.android.phase09package.ApkMojo Maven / Gradle / Ivy
/*
* 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.jayway.maven.plugins.android.phase09package;
import java.io.File;
import java.io.FileFilter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import org.apache.commons.lang.StringUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.dependency.utils.resolvers.DefaultArtifactsResolver;
import org.codehaus.plexus.util.AbstractScanner;
import com.jayway.maven.plugins.android.AbstractAndroidMojo;
import com.jayway.maven.plugins.android.AndroidSigner;
import com.jayway.maven.plugins.android.CommandExecutor;
import com.jayway.maven.plugins.android.ExecutionException;
import com.jayway.maven.plugins.android.Sign;
/**
* Creates the apk file. By default signs it with debug keystore.
* Change that by setting configuration parameter <sign><debug>false</debug></sign>
.
*
* @author [email protected]
* @goal apk
* @phase package
* @requiresDependencyResolution compile
*/
public class ApkMojo extends AbstractAndroidMojo {
/**
* How to sign the apk.
* Looks like this:
*
* <sign>
* <debug>auto</debug>
* </sign>
*
* Valid values for <debug>
are:
*
* true
= sign with the debug keystore.
* false
= don't sign with the debug keystore.
* both
= create a signed as well as an unsigned apk.
* auto
(default) = sign with debug keystore, unless another keystore is defined. (Signing with
* other keystores is not yet implemented. See
* Issue 2.)
*
* Can also be configured from command-line with parameter -Dandroid.sign.debug
.
*
* @parameter
*/
private Sign sign;
/**
* Parameter designed to pick up -Dandroid.sign.debug
in case there is no pom with a
* <sign>
configuration tag.
* Corresponds to {@link Sign#debug}.
*
* @parameter expression="${android.sign.debug}" default-value="auto"
* @readonly
*/
private String signDebug;
/**
* Root folder containing native libraries to include in the application package.
*
* @parameter expression="${android.nativeLibrariesDirectory}" default-value="${project.basedir}/libs"
*/
private File nativeLibrariesDirectory;
/**
* Temporary folder for collecting native libraries.
*
* @parameter default-value="${project.build.directory}/libs"
* @readonly
*/
private File nativeLibrariesOutputDirectory;
/**
* Default hardware architecture for native library dependencies (with {@code <type>so</type>}).
* This value is used for dependencies without classifier, if {@code nativeLibrariesDependenciesHardwareArchitectureOverride} is not set.
* Valid values currently include {@code armeabi} and {@code armeabi-v7a}.
*
* @parameter expression="${android.nativeLibrariesDependenciesHardwareArchitectureDefault}" default-value="armeabi"
*/
private String nativeLibrariesDependenciesHardwareArchitectureDefault;
/**
* Override hardware architecture for native library dependencies (with {@code <type>so</type>}).
* This overrides any classifier on native library dependencies, and any {@code nativeLibrariesDependenciesHardwareArchitectureDefault}.
* Valid values currently include {@code armeabi} and {@code armeabi-v7a}.
*
* @parameter expression="${android.nativeLibrariesDependenciesHardwareArchitectureOverride}"
*/
private String nativeLibrariesDependenciesHardwareArchitectureOverride;
private static final Pattern PATTERN_JAR_EXT = Pattern.compile("^.+\\.jar$", 2);
public void execute() throws MojoExecutionException, MojoFailureException {
// Make an early exit if we're not supposed to generate the APK
if (!generateApk) {
return;
}
generateIntermediateAp_();
// Initialize apk build configuration
File outputFile = new File(project.getBuild().getDirectory(), project.getBuild().getFinalName() +
ANDROID_PACKAGE_EXTENSTION);
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(project.getBuild().getDirectory(), project.getBuild()
.getFinalName() + "-unsigned" + ANDROID_PACKAGE_EXTENSTION);
getLog().info("Creating additional unsigned apk file " + unsignedOutputFile);
createApkFile(unsignedOutputFile, false);
projectHelper.attachArtifact(project, unsignedOutputFile, "unsigned");
} else {
createApkFile(outputFile, signWithDebugKeyStore);
}
// Set the generated .apk file as the main artifact (because the pom states apk )
project.getArtifact().setFile(outputFile);
}
void createApkFile(File outputFile, boolean signWithDebugKeyStore) throws MojoExecutionException {
File dexFile = new File(project.getBuild().getDirectory(), "classes.dex");
File zipArchive = new File(project.getBuild().getDirectory(), project.getBuild().getFinalName() + ".ap_");
ArrayList sourceFolders = new ArrayList();
ArrayList jarFiles = new ArrayList();
ArrayList nativeFolders = new ArrayList();
boolean useInternalAPKBuilder = true;
try {
initializeAPKBuilder();
// Ok...
// So we can try to use the internal ApkBuilder
} catch (MojoExecutionException e) {
// Not supported platform try to old way.
useInternalAPKBuilder = false;
}
if (useInternalAPKBuilder) {
doAPKWithAPKBuilder(outputFile, dexFile, zipArchive, sourceFolders, jarFiles,
nativeFolders, false, signWithDebugKeyStore, false);
} else {
doAPKWithCommand(outputFile, dexFile, zipArchive, sourceFolders, jarFiles,
nativeFolders, signWithDebugKeyStore);
}
}
/**
* 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 verbose enables the verbose mode
* @param signWithDebugKeyStore enables the signature of the APK using the debug key
* @param debug enables the debug mode
* @throws MojoExecutionException if the APK cannot be created.
*/
private void doAPKWithAPKBuilder(File outputFile, File dexFile, File zipArchive,
ArrayList sourceFolders, ArrayList jarFiles,
ArrayList nativeFolders, boolean verbose, boolean signWithDebugKeyStore,
boolean debug) throws MojoExecutionException {
sourceFolders.add(new File(project.getBuild().getDirectory(), "classes"));
// 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
processNativeLibraries(nativeFolders);
for (Artifact artifact : getRelevantCompileArtifacts()) {
jarFiles.add(artifact.getFile());
}
ApkBuilder builder = new ApkBuilder(outputFile, zipArchive, dexFile, signWithDebugKeyStore, (verbose) ? System.out : null);
if (debug) {
builder.setDebugMode(debug);
}
for (File sourceFolder : sourceFolders) {
builder.addSourceFolder(sourceFolder);
}
for (File jarFile : jarFiles) {
if (jarFile.isDirectory()) {
String[] filenames = jarFile.list(new FilenameFilter() {
public boolean accept(File dir, String name) {
return PATTERN_JAR_EXT.matcher(name).matches();
}
});
for (String filename : filenames) {
builder.addResourcesFromJar(new File(jarFile, filename));
}
} else {
builder.addResourcesFromJar(jarFile);
}
}
for (File nativeFolder : nativeFolders) {
builder.addNativeLibraries(nativeFolder, null);
}
builder.sealApk();
}
/**
* Creates the APK file using the command line.
* @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 doAPKWithCommand(File outputFile, File dexFile, File zipArchive,
ArrayList sourceFolders, ArrayList jarFiles,
ArrayList nativeFolders, boolean signWithDebugKeyStore) throws MojoExecutionException {
CommandExecutor executor = CommandExecutor.Factory.createDefaultCommmandExecutor();
executor.setLogger(this.getLog());
List commands = new ArrayList();
commands.add(outputFile.getAbsolutePath());
if (! signWithDebugKeyStore) {
commands.add("-u");
}
commands.add("-z");
commands.add(new File(project.getBuild().getDirectory(), project.getBuild().getFinalName() + ".ap_").getAbsolutePath());
commands.add("-f");
commands.add(new File(project.getBuild().getDirectory(), "classes.dex").getAbsolutePath());
commands.add("-rf");
commands.add(new File(project.getBuild().getDirectory(), "classes").getAbsolutePath());
if (nativeFolders != null && ! nativeFolders.isEmpty()) {
for (File lib : nativeFolders) {
commands.add("-nf");
commands.add(lib.getAbsolutePath());
}
}
for (Artifact artifact : getRelevantCompileArtifacts()) {
commands.add("-rj");
commands.add(artifact.getFile().getAbsolutePath());
}
getLog().info(getAndroidSdk().getPathForTool("apkbuilder") + " " + commands.toString());
try {
executor.executeCommand(getAndroidSdk().getPathForTool("apkbuilder"), commands, project.getBasedir(), false);
} catch (ExecutionException e) {
throw new MojoExecutionException("", e);
}
}
private void initializeAPKBuilder() throws MojoExecutionException {
File file = getAndroidSdk().getSDKLibJar();
ApkBuilder.initialize(getLog(), file);
}
private void processNativeLibraries(final List natives) throws MojoExecutionException
{
final Set artifacts = getNativeDependenciesArtifacts();
final boolean hasValidNativeLibrariesDirectory = nativeLibrariesDirectory != null && nativeLibrariesDirectory.exists();
if (artifacts.isEmpty() && hasValidNativeLibrariesDirectory)
{
getLog().debug("No native library dependencies detected, will point directly to " + nativeLibrariesDirectory);
// Point directly to the directory in this case - no need to copy files around
natives.add(nativeLibrariesDirectory);
}
else if (!artifacts.isEmpty() || hasValidNativeLibrariesDirectory)
{
// In this case, we may have both .so files in it's normal location
// as well as .so dependencies
// Create the ${project.build.outputDirectory}/libs
File destinationDirectory = new File(nativeLibrariesOutputDirectory.getAbsolutePath());
destinationDirectory.mkdirs();
// Point directly to the directory
natives.add(destinationDirectory);
// If we have a valid native libs, copy those files - these already come in the structure required
if (hasValidNativeLibrariesDirectory)
{
getLog().debug("Copying existing native libraries from " + nativeLibrariesDirectory);
try
{
org.apache.commons.io.FileUtils.copyDirectory(nativeLibrariesDirectory,destinationDirectory, new FileFilter()
{
public boolean accept(final File pathname)
{
return pathname.getName().endsWith(".so");
}
});
}
catch (IOException e)
{
throw new MojoExecutionException(e.getMessage(), e);
}
}
if (!artifacts.isEmpty())
{
final DefaultArtifactsResolver artifactsResolver = new DefaultArtifactsResolver(this.artifactResolver, this.localRepository, this.remoteRepositories, true);
@SuppressWarnings("unchecked")
final Set resolvedArtifacts = artifactsResolver.resolve(artifacts, getLog());
for (Artifact resolvedArtifact : resolvedArtifacts)
{
final File artifactFile = resolvedArtifact.getFile();
try
{
final String artifactId = resolvedArtifact.getArtifactId();
final String filename = artifactId.startsWith("lib") ? artifactId + ".so" : "lib" + artifactId + ".so";
final File finalDestinationDirectory = getFinalDestinationDirectoryFor(resolvedArtifact, destinationDirectory);
final File file = new File(finalDestinationDirectory, filename);
getLog().debug("Copying native dependency " + artifactId + " (" + resolvedArtifact.getGroupId() + ") to " + file );
org.apache.commons.io.FileUtils.copyFile(artifactFile, file);
}
catch (Exception e)
{
throw new MojoExecutionException("Could not copy native dependency.", e);
}
}
}
}
}
private File getFinalDestinationDirectoryFor(Artifact resolvedArtifact, File destinationDirectory) {
final String hardwareArchitecture = getHardwareArchitectureFor(resolvedArtifact);
File finalDestinationDirectory = new File(destinationDirectory, hardwareArchitecture + "/");
finalDestinationDirectory.mkdirs();
return finalDestinationDirectory;
}
private String getHardwareArchitectureFor(Artifact resolvedArtifact) {
if (StringUtils.isNotBlank(nativeLibrariesDependenciesHardwareArchitectureOverride)){
return nativeLibrariesDependenciesHardwareArchitectureOverride;
}
final String classifier = resolvedArtifact.getClassifier();
if (StringUtils.isNotBlank(classifier)) {
return classifier;
}
return nativeLibrariesDependenciesHardwareArchitectureDefault;
}
private Set getNativeDependenciesArtifacts()
{
Set filteredArtifacts = new HashSet();
@SuppressWarnings("unchecked")
final Set allArtifacts = project.getDependencyArtifacts();
for (Artifact artifact : allArtifacts)
{
if ("so".equals(artifact.getType()) && (Artifact.SCOPE_COMPILE.equals( artifact.getScope() ) || Artifact.SCOPE_RUNTIME.equals( artifact.getScope() )))
{
filteredArtifacts.add(artifact);
}
}
return filteredArtifacts;
}
/**
* Generates an intermediate apk file (actually .ap_) containing the resources and assets.
*
* @throws MojoExecutionException
*/
private void generateIntermediateAp_() throws MojoExecutionException {
CommandExecutor executor = CommandExecutor.Factory.createDefaultCommmandExecutor();
executor.setLogger(this.getLog());
File[] overlayDirectories;
if (resourceOverlayDirectories == null || resourceOverlayDirectories.length == 0) {
overlayDirectories = new File[]{resourceOverlayDirectory};
} else {
overlayDirectories = resourceOverlayDirectories;
}
if (!combinedRes.exists()) {
if (!combinedRes.mkdirs()) {
throw new MojoExecutionException("Could not create directory for combined resources at " + combinedRes.getAbsolutePath());
}
}
if (extractedDependenciesRes.exists()) {
try {
getLog().info("Copying dependency resource files to combined resource directory.");
org.apache.commons.io.FileUtils.copyDirectory(extractedDependenciesRes, combinedRes);
} catch (IOException e) {
throw new MojoExecutionException("", e);
}
}
if (resourceDirectory.exists()) {
try {
getLog().info("Copying local resource files to combined resource directory.");
org.apache.commons.io.FileUtils.copyDirectory(resourceDirectory, combinedRes, new FileFilter() {
/**
* Excludes files matching one of the common file to exclude.
* The default excludes pattern are the ones from
* {org.codehaus.plexus.util.AbstractScanner#DEFAULTEXCLUDES}
* @see java.io.FileFilter#accept(java.io.File)
*/
public boolean accept(File file) {
for (String pattern : AbstractScanner.DEFAULTEXCLUDES) {
if (AbstractScanner.match(pattern, file.getAbsolutePath())) {
getLog().debug("Excluding " + file.getName() + " from resource copy : matching " + pattern);
return false;
}
}
return true;
}
});
} catch (IOException e) {
throw new MojoExecutionException("", e);
}
}
// Must combine assets.
// The aapt tools does not support several -A arguments.
// We copy the assets from extracted dependencies first, and then the local assets.
// This allows redefining the assets in the current project
if (extractedDependenciesAssets.exists()) {
try {
getLog().info("Copying dependency assets files to combined assets directory.");
org.apache.commons.io.FileUtils.copyDirectory(extractedDependenciesAssets, combinedAssets, new FileFilter() {
/**
* Excludes files matching one of the common file to exclude.
* The default excludes pattern are the ones from
* {org.codehaus.plexus.util.AbstractScanner#DEFAULTEXCLUDES}
* @see java.io.FileFilter#accept(java.io.File)
*/
public boolean accept(File file) {
for (String pattern : AbstractScanner.DEFAULTEXCLUDES) {
if (AbstractScanner.match(pattern, file.getAbsolutePath())) {
getLog().debug("Excluding " + file.getName() + " from asset copy : matching " + pattern);
return false;
}
}
return true;
}
});
} catch (IOException e) {
throw new MojoExecutionException("", e);
}
}
if (assetsDirectory.exists()) {
try {
getLog().info("Copying local assets files to combined assets directory.");
org.apache.commons.io.FileUtils.copyDirectory(assetsDirectory, combinedAssets, new FileFilter() {
/**
* Excludes files matching one of the common file to exclude.
* The default excludes pattern are the ones from
* {org.codehaus.plexus.util.AbstractScanner#DEFAULTEXCLUDES}
* @see java.io.FileFilter#accept(java.io.File)
*/
public boolean accept(File file) {
for (String pattern : AbstractScanner.DEFAULTEXCLUDES) {
if (AbstractScanner.match(pattern, file.getAbsolutePath())) {
getLog().debug("Excluding " + file.getName() + " from asset copy : matching " + pattern);
return false;
}
}
return true;
}
});
} catch (IOException e) {
throw new MojoExecutionException("", e);
}
}
File androidJar = getAndroidSdk().getAndroidJar();
File outputFile = new File(project.getBuild().getDirectory(), project.getBuild().getFinalName() + ".ap_");
List commands = new ArrayList();
commands.add("package");
commands.add("-f");
commands.add("-M");
commands.add(androidManifestFile.getAbsolutePath());
for (File resOverlayDir : overlayDirectories) {
if (resOverlayDir != null && resOverlayDir.exists()) {
commands.add("-S");
commands.add(resOverlayDir.getAbsolutePath());
}
}
if (combinedRes.exists()) {
commands.add("-S");
commands.add(combinedRes.getAbsolutePath());
}
// Use the combined assets.
// Indeed, aapt does not support several -A arguments.
if (combinedAssets.exists()) {
commands.add("-A");
commands.add(combinedAssets.getAbsolutePath());
}
commands.add("-I");
commands.add(androidJar.getAbsolutePath());
commands.add("-F");
commands.add(outputFile.getAbsolutePath());
if (StringUtils.isNotBlank(configurations)) {
commands.add("-c");
commands.add(configurations);
}
getLog().info(getAndroidSdk().getPathForTool("aapt") + " " + commands.toString());
try {
executor.executeCommand(getAndroidSdk().getPathForTool("aapt"), 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());
}
}
}