Update the API of ApkBuilder to make it clearer what is stable and what isn't.
This commit is contained in:
@@ -16,8 +16,9 @@
|
|||||||
|
|
||||||
package com.android.ant;
|
package com.android.ant;
|
||||||
|
|
||||||
import com.android.apkbuilder.ApkBuilder;
|
import com.android.apkbuilder.ApkBuilder.ApkCreationException;
|
||||||
import com.android.apkbuilder.ApkBuilder.ApkFile;
|
import com.android.apkbuilder.internal.ApkBuilderImpl;
|
||||||
|
import com.android.apkbuilder.internal.ApkBuilderImpl.ApkFile;
|
||||||
import com.android.sdklib.internal.project.ApkConfigurationHelper;
|
import com.android.sdklib.internal.project.ApkConfigurationHelper;
|
||||||
import com.android.sdklib.internal.project.ProjectProperties;
|
import com.android.sdklib.internal.project.ProjectProperties;
|
||||||
import com.android.sdklib.internal.project.ProjectProperties.PropertyType;
|
import com.android.sdklib.internal.project.ProjectProperties.PropertyType;
|
||||||
@@ -153,7 +154,7 @@ public class ApkBuilderTask extends Task {
|
|||||||
public void execute() throws BuildException {
|
public void execute() throws BuildException {
|
||||||
Project taskProject = getProject();
|
Project taskProject = getProject();
|
||||||
|
|
||||||
ApkBuilder apkBuilder = new ApkBuilder();
|
ApkBuilderImpl apkBuilder = new ApkBuilderImpl();
|
||||||
apkBuilder.setVerbose(mVerbose);
|
apkBuilder.setVerbose(mVerbose);
|
||||||
apkBuilder.setSignedPackage(mSigned);
|
apkBuilder.setSignedPackage(mSigned);
|
||||||
|
|
||||||
@@ -169,17 +170,17 @@ public class ApkBuilderTask extends Task {
|
|||||||
|
|
||||||
// now go through the list of file to directly add the to the list.
|
// now go through the list of file to directly add the to the list.
|
||||||
for (Value v : mFileList) {
|
for (Value v : mFileList) {
|
||||||
mArchiveFiles.add(ApkBuilder.getInputFile(v.mPath));
|
mArchiveFiles.add(ApkBuilderImpl.getInputFile(v.mPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
// now go through the list of file to directly add the to the list.
|
// now go through the list of file to directly add the to the list.
|
||||||
for (Value v : mSourceList) {
|
for (Value v : mSourceList) {
|
||||||
ApkBuilder.processSourceFolderForResource(v.mPath, mJavaResources);
|
ApkBuilderImpl.processSourceFolderForResource(v.mPath, mJavaResources);
|
||||||
}
|
}
|
||||||
|
|
||||||
// now go through the list of jar folders.
|
// now go through the list of jar folders.
|
||||||
for (Value v : mJarList) {
|
for (Value v : mJarList) {
|
||||||
ApkBuilder.processJarFolder(v.mPath, mResourcesJars);
|
ApkBuilderImpl.processJarFolder(v.mPath, mResourcesJars);
|
||||||
}
|
}
|
||||||
|
|
||||||
// now the native lib folder.
|
// now the native lib folder.
|
||||||
@@ -193,7 +194,7 @@ public class ApkBuilderTask extends Task {
|
|||||||
offset++;
|
offset++;
|
||||||
}
|
}
|
||||||
|
|
||||||
ApkBuilder.processNativeFolder(offset, f, mNativeLibraries);
|
ApkBuilderImpl.processNativeFolder(offset, f, mNativeLibraries);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -217,6 +218,8 @@ public class ApkBuilderTask extends Task {
|
|||||||
throw new BuildException(e);
|
throw new BuildException(e);
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
throw new BuildException(e);
|
throw new BuildException(e);
|
||||||
|
} catch (ApkCreationException e) {
|
||||||
|
throw new BuildException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,9 +231,10 @@ public class ApkBuilderTask extends Task {
|
|||||||
* @param resourceFilter the resource configuration filter to pass to aapt (if configName is
|
* @param resourceFilter the resource configuration filter to pass to aapt (if configName is
|
||||||
* non null)
|
* non null)
|
||||||
* @throws FileNotFoundException
|
* @throws FileNotFoundException
|
||||||
|
* @throws ApkCreationException
|
||||||
*/
|
*/
|
||||||
private void createApk(ApkBuilder apkBuilder, String configName, String resourceFilter)
|
private void createApk(ApkBuilderImpl apkBuilder, String configName, String resourceFilter)
|
||||||
throws FileNotFoundException {
|
throws FileNotFoundException, ApkCreationException {
|
||||||
// All the files to be included in the archive have already been prep'ed up, except
|
// All the files to be included in the archive have already been prep'ed up, except
|
||||||
// the resource package.
|
// the resource package.
|
||||||
// figure out its name.
|
// figure out its name.
|
||||||
|
|||||||
@@ -16,419 +16,69 @@
|
|||||||
|
|
||||||
package com.android.apkbuilder;
|
package com.android.apkbuilder;
|
||||||
|
|
||||||
import com.android.jarutils.DebugKeyProvider;
|
import com.android.apkbuilder.internal.ApkBuilderImpl;
|
||||||
import com.android.jarutils.JavaResourceFilter;
|
|
||||||
import com.android.jarutils.SignedJarBuilder;
|
|
||||||
import com.android.jarutils.DebugKeyProvider.KeytoolException;
|
|
||||||
import com.android.prefs.AndroidLocation.AndroidLocationException;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.FilenameFilter;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.security.PrivateKey;
|
|
||||||
import java.security.cert.X509Certificate;
|
|
||||||
import java.text.DateFormat;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Command line APK builder with signing support.
|
* Command line APK builder with signing support.
|
||||||
*/
|
*/
|
||||||
public final class ApkBuilder {
|
public final class ApkBuilder {
|
||||||
|
|
||||||
private final static Pattern PATTERN_JAR_EXT = Pattern.compile("^.+\\.jar$",
|
public final static class WrongOptionException extends Exception {
|
||||||
Pattern.CASE_INSENSITIVE);
|
private static final long serialVersionUID = 1L;
|
||||||
private final static Pattern PATTERN_NATIVELIB_EXT = Pattern.compile("^.+\\.so$",
|
|
||||||
Pattern.CASE_INSENSITIVE);
|
|
||||||
|
|
||||||
private final static String NATIVE_LIB_ROOT = "lib/";
|
public WrongOptionException(String message) {
|
||||||
|
super(message);
|
||||||
/**
|
|
||||||
* A File to be added to the APK archive.
|
|
||||||
* <p/>This includes the {@link File} representing the file and its path in the archive.
|
|
||||||
*/
|
|
||||||
public final static class ApkFile {
|
|
||||||
String archivePath;
|
|
||||||
File file;
|
|
||||||
|
|
||||||
ApkFile(File file, String path) {
|
|
||||||
this.file = file;
|
|
||||||
this.archivePath = path;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private JavaResourceFilter mResourceFilter = new JavaResourceFilter();
|
public final static class ApkCreationException extends Exception {
|
||||||
private boolean mVerbose = false;
|
private static final long serialVersionUID = 1L;
|
||||||
private boolean mSignedPackage = true;
|
|
||||||
/** the optional type of the debug keystore. If <code>null</code>, the default */
|
public ApkCreationException(String message) {
|
||||||
private String mStoreType = null;
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ApkCreationException(Throwable throwable) {
|
||||||
|
super(throwable);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param args
|
* Main method. This is meant to be called from the command line through an exec.
|
||||||
|
* <p/>WARNING: this will call {@link System#exit(int)} if anything goes wrong.
|
||||||
|
* @param args command line arguments.
|
||||||
*/
|
*/
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
new ApkBuilder().run(args);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setVerbose(boolean verbose) {
|
|
||||||
mVerbose = verbose;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSignedPackage(boolean signedPackage) {
|
|
||||||
mSignedPackage = signedPackage;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void run(String[] args) {
|
|
||||||
if (args.length < 1) {
|
|
||||||
printUsageAndQuit();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// read the first args that should be a file path
|
new ApkBuilderImpl().run(args);
|
||||||
File outFile = getOutFile(args[0]);
|
} catch (WrongOptionException e) {
|
||||||
|
printUsageAndQuit();
|
||||||
ArrayList<FileInputStream> zipArchives = new ArrayList<FileInputStream>();
|
|
||||||
ArrayList<File> archiveFiles = new ArrayList<File>();
|
|
||||||
ArrayList<ApkFile> javaResources = new ArrayList<ApkFile>();
|
|
||||||
ArrayList<FileInputStream> resourcesJars = new ArrayList<FileInputStream>();
|
|
||||||
ArrayList<ApkFile> nativeLibraries = new ArrayList<ApkFile>();
|
|
||||||
|
|
||||||
int index = 1;
|
|
||||||
do {
|
|
||||||
String argument = args[index++];
|
|
||||||
|
|
||||||
if ("-v".equals(argument)) {
|
|
||||||
mVerbose = true;
|
|
||||||
} else if ("-u".equals(argument)) {
|
|
||||||
mSignedPackage = false;
|
|
||||||
} else if ("-z".equals(argument)) {
|
|
||||||
// quick check on the next argument.
|
|
||||||
if (index == args.length) printUsageAndQuit();
|
|
||||||
|
|
||||||
try {
|
|
||||||
FileInputStream input = new FileInputStream(args[index++]);
|
|
||||||
zipArchives.add(input);
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
printAndExit(e.getMessage());
|
|
||||||
}
|
|
||||||
} else if ("-f". equals(argument)) {
|
|
||||||
// quick check on the next argument.
|
|
||||||
if (index == args.length) printUsageAndQuit();
|
|
||||||
|
|
||||||
archiveFiles.add(getInputFile(args[index++]));
|
|
||||||
} else if ("-rf". equals(argument)) {
|
|
||||||
// quick check on the next argument.
|
|
||||||
if (index == args.length) printUsageAndQuit();
|
|
||||||
|
|
||||||
processSourceFolderForResource(args[index++], javaResources);
|
|
||||||
} else if ("-rj". equals(argument)) {
|
|
||||||
// quick check on the next argument.
|
|
||||||
if (index == args.length) printUsageAndQuit();
|
|
||||||
|
|
||||||
processJarFolder(args[index++], resourcesJars);
|
|
||||||
} else if ("-nf".equals(argument)) {
|
|
||||||
// quick check on the next argument.
|
|
||||||
if (index == args.length) printUsageAndQuit();
|
|
||||||
|
|
||||||
String parameter = args[index++];
|
|
||||||
File f = new File(parameter);
|
|
||||||
|
|
||||||
// compute the offset to get the relative path
|
|
||||||
int offset = parameter.length();
|
|
||||||
if (parameter.endsWith(File.separator) == false) {
|
|
||||||
offset++;
|
|
||||||
}
|
|
||||||
|
|
||||||
processNativeFolder(offset, f, nativeLibraries);
|
|
||||||
} else if ("-storetype".equals(argument)) {
|
|
||||||
// quick check on the next argument.
|
|
||||||
if (index == args.length) printUsageAndQuit();
|
|
||||||
|
|
||||||
mStoreType = args[index++];
|
|
||||||
} else {
|
|
||||||
printAndExit("Unknown argument: " + argument);
|
|
||||||
}
|
|
||||||
} while (index < args.length);
|
|
||||||
|
|
||||||
createPackage(outFile, zipArchives, archiveFiles, javaResources, resourcesJars,
|
|
||||||
nativeLibraries);
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
printAndExit(e.getMessage());
|
|
||||||
} catch (FileNotFoundException e) {
|
} catch (FileNotFoundException e) {
|
||||||
printAndExit(e.getMessage());
|
printAndExit(e.getMessage());
|
||||||
}
|
} catch (ApkCreationException e) {
|
||||||
}
|
printAndExit(e.getMessage());
|
||||||
|
|
||||||
|
|
||||||
private File getOutFile(String filepath) {
|
|
||||||
File f = new File(filepath);
|
|
||||||
|
|
||||||
if (f.isDirectory()) {
|
|
||||||
printAndExit(filepath + " is a directory!");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (f.exists()) { // will be a file in this case.
|
|
||||||
if (f.canWrite() == false) {
|
|
||||||
printAndExit("Cannot write " + filepath);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
if (f.createNewFile() == false) {
|
|
||||||
printAndExit("Failed to create " + filepath);
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
printAndExit("Failed to create '" + filepath + "' : " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return f;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static File getInputFile(String filepath) throws IllegalArgumentException {
|
|
||||||
File f = new File(filepath);
|
|
||||||
|
|
||||||
if (f.isDirectory()) {
|
|
||||||
throw new IllegalArgumentException(filepath + " is a directory!");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (f.exists()) {
|
|
||||||
if (f.canRead() == false) {
|
|
||||||
throw new IllegalArgumentException("Cannot read " + filepath);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new IllegalArgumentException(filepath + " does not exists!");
|
|
||||||
}
|
|
||||||
|
|
||||||
return f;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Processes a source folder and adds its java resources to a given list of {@link ApkFile}.
|
|
||||||
* @param folderPath the path to the source folder.
|
|
||||||
* @param javaResources the list of {@link ApkFile} to fill.
|
|
||||||
*/
|
|
||||||
public static void processSourceFolderForResource(String folderPath,
|
|
||||||
ArrayList<ApkFile> javaResources) {
|
|
||||||
|
|
||||||
File folder = new File(folderPath);
|
|
||||||
|
|
||||||
if (folder.isDirectory()) {
|
|
||||||
// file is a directory, process its content.
|
|
||||||
File[] files = folder.listFiles();
|
|
||||||
for (File file : files) {
|
|
||||||
processFileForResource(file, null, javaResources);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// not a directory? output error and quit.
|
|
||||||
if (folder.exists()) {
|
|
||||||
throw new IllegalArgumentException(folderPath + " is not a folder!");
|
|
||||||
} else {
|
|
||||||
throw new IllegalArgumentException(folderPath + " does not exist!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void processJarFolder(String parameter, ArrayList<FileInputStream> resourcesJars)
|
|
||||||
throws FileNotFoundException {
|
|
||||||
File f = new File(parameter);
|
|
||||||
if (f.isDirectory()) {
|
|
||||||
String[] files = f.list(new FilenameFilter() {
|
|
||||||
public boolean accept(File dir, String name) {
|
|
||||||
return PATTERN_JAR_EXT.matcher(name).matches();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
for (String file : files) {
|
|
||||||
String path = f.getAbsolutePath() + File.separator + file;
|
|
||||||
FileInputStream input = new FileInputStream(path);
|
|
||||||
resourcesJars.add(input);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
FileInputStream input = new FileInputStream(parameter);
|
|
||||||
resourcesJars.add(input);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Processes a {@link File} that could be a {@link ApkFile}, or a folder containing
|
|
||||||
* java resources.
|
|
||||||
* @param file the {@link File} to process.
|
|
||||||
* @param path the relative path of this file to the source folder. Can be <code>null</code> to
|
|
||||||
* identify a root file.
|
|
||||||
* @param javaResources the list of {@link ApkFile} object to fill.
|
|
||||||
*/
|
|
||||||
private static void processFileForResource(File file, String path,
|
|
||||||
ArrayList<ApkFile> javaResources) {
|
|
||||||
if (file.isDirectory()) {
|
|
||||||
// a directory? we check it
|
|
||||||
if (JavaResourceFilter.checkFolderForPackaging(file.getName())) {
|
|
||||||
// if it's valid, we append its name to the current path.
|
|
||||||
if (path == null) {
|
|
||||||
path = file.getName();
|
|
||||||
} else {
|
|
||||||
path = path + "/" + file.getName();
|
|
||||||
}
|
|
||||||
|
|
||||||
// and process its content.
|
|
||||||
File[] files = file.listFiles();
|
|
||||||
for (File contentFile : files) {
|
|
||||||
processFileForResource(contentFile, path, javaResources);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// a file? we check it
|
|
||||||
if (JavaResourceFilter.checkFileForPackaging(file.getName())) {
|
|
||||||
// we append its name to the current path
|
|
||||||
if (path == null) {
|
|
||||||
path = file.getName();
|
|
||||||
} else {
|
|
||||||
path = path + "/" + file.getName();
|
|
||||||
}
|
|
||||||
|
|
||||||
// and add it to the list.
|
|
||||||
javaResources.add(new ApkFile(file, path));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process a {@link File} for native library inclusion.
|
* API entry point similar to the {@link #main(String[])} method.
|
||||||
* @param offset the length of the root folder (used to compute relative path)
|
* <p/>Unlike {@link #main(String[])}, this will not call {@link System#exit(int)} and instead
|
||||||
* @param f the {@link File} to process
|
* will throw exceptions.
|
||||||
* @param nativeLibraries the array to add native libraries.
|
* @param args command line arguments.
|
||||||
|
* @throws WrongOptionException if the command line arguments are incorrect.
|
||||||
|
* @throws FileNotFoundException if a required file was not found.
|
||||||
|
* @throws ApkCreationException if an error happened during the creation of the APK.
|
||||||
*/
|
*/
|
||||||
public static void processNativeFolder(int offset, File f, ArrayList<ApkFile> nativeLibraries) {
|
public static void createApk(String[] args) throws FileNotFoundException, WrongOptionException,
|
||||||
if (f.isDirectory()) {
|
ApkCreationException {
|
||||||
File[] children = f.listFiles();
|
new ApkBuilderImpl().run(args);
|
||||||
|
|
||||||
if (children != null) {
|
|
||||||
for (File child : children) {
|
|
||||||
processNativeFolder(offset, child, nativeLibraries);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (f.isFile()) {
|
|
||||||
if (PATTERN_NATIVELIB_EXT.matcher(f.getName()).matches()) {
|
|
||||||
String path = NATIVE_LIB_ROOT +
|
|
||||||
f.getAbsolutePath().substring(offset).replace('\\', '/');
|
|
||||||
|
|
||||||
nativeLibraries.add(new ApkFile(f, path));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private static void printUsageAndQuit() {
|
||||||
* Creates the application package
|
|
||||||
* @param outFile
|
|
||||||
* @param zipArchives
|
|
||||||
* @param resourcesJars
|
|
||||||
* @param files
|
|
||||||
* @param javaResources
|
|
||||||
* keystore type of the Java VM is used.
|
|
||||||
*/
|
|
||||||
public void createPackage(File outFile, ArrayList<FileInputStream> zipArchives,
|
|
||||||
ArrayList<File> files, ArrayList<ApkFile> javaResources,
|
|
||||||
ArrayList<FileInputStream> resourcesJars, ArrayList<ApkFile> nativeLibraries) {
|
|
||||||
|
|
||||||
// get the debug key
|
|
||||||
try {
|
|
||||||
SignedJarBuilder builder;
|
|
||||||
|
|
||||||
if (mSignedPackage) {
|
|
||||||
System.err.println(String.format("Using keystore: %s",
|
|
||||||
DebugKeyProvider.getDefaultKeyStoreOsPath()));
|
|
||||||
|
|
||||||
|
|
||||||
DebugKeyProvider keyProvider = new DebugKeyProvider(
|
|
||||||
null /* osKeyPath: use default */,
|
|
||||||
mStoreType, null /* IKeyGenOutput */);
|
|
||||||
PrivateKey key = keyProvider.getDebugKey();
|
|
||||||
X509Certificate certificate = (X509Certificate)keyProvider.getCertificate();
|
|
||||||
|
|
||||||
if (key == null) {
|
|
||||||
throw new IllegalArgumentException("Unable to get debug signature key");
|
|
||||||
}
|
|
||||||
|
|
||||||
// compare the certificate expiration date
|
|
||||||
if (certificate != null && certificate.getNotAfter().compareTo(new Date()) < 0) {
|
|
||||||
// TODO, regenerate a new one.
|
|
||||||
throw new IllegalArgumentException("Debug Certificate expired on " +
|
|
||||||
DateFormat.getInstance().format(certificate.getNotAfter()));
|
|
||||||
}
|
|
||||||
|
|
||||||
builder = new SignedJarBuilder(
|
|
||||||
new FileOutputStream(outFile.getAbsolutePath(), false /* append */), key,
|
|
||||||
certificate);
|
|
||||||
} else {
|
|
||||||
builder = new SignedJarBuilder(
|
|
||||||
new FileOutputStream(outFile.getAbsolutePath(), false /* append */),
|
|
||||||
null /* key */, null /* certificate */);
|
|
||||||
}
|
|
||||||
|
|
||||||
// add the archives
|
|
||||||
for (FileInputStream input : zipArchives) {
|
|
||||||
builder.writeZip(input, null /* filter */);
|
|
||||||
}
|
|
||||||
|
|
||||||
// add the single files
|
|
||||||
for (File input : files) {
|
|
||||||
// always put the file at the root of the archive in this case
|
|
||||||
builder.writeFile(input, input.getName());
|
|
||||||
if (mVerbose) {
|
|
||||||
System.err.println(String.format("%1$s => %2$s", input.getAbsolutePath(),
|
|
||||||
input.getName()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// add the java resource from the source folders.
|
|
||||||
for (ApkFile resource : javaResources) {
|
|
||||||
builder.writeFile(resource.file, resource.archivePath);
|
|
||||||
if (mVerbose) {
|
|
||||||
System.err.println(String.format("%1$s => %2$s",
|
|
||||||
resource.file.getAbsolutePath(), resource.archivePath));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// add the java resource from jar files.
|
|
||||||
for (FileInputStream input : resourcesJars) {
|
|
||||||
builder.writeZip(input, mResourceFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
// add the native files
|
|
||||||
for (ApkFile file : nativeLibraries) {
|
|
||||||
builder.writeFile(file.file, file.archivePath);
|
|
||||||
if (mVerbose) {
|
|
||||||
System.err.println(String.format("%1$s => %2$s", file.file.getAbsolutePath(),
|
|
||||||
file.archivePath));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// close and sign the application package.
|
|
||||||
builder.close();
|
|
||||||
} catch (KeytoolException e) {
|
|
||||||
if (e.getJavaHome() == null) {
|
|
||||||
throw new IllegalArgumentException(e.getMessage() +
|
|
||||||
"\nJAVA_HOME seems undefined, setting it will help locating keytool automatically\n" +
|
|
||||||
"You can also manually execute the following command\n:" +
|
|
||||||
e.getCommandLine());
|
|
||||||
} else {
|
|
||||||
throw new IllegalArgumentException(e.getMessage() +
|
|
||||||
"\nJAVA_HOME is set to: " + e.getJavaHome() +
|
|
||||||
"\nUpdate it if necessary, or manually execute the following command:\n" +
|
|
||||||
e.getCommandLine());
|
|
||||||
}
|
|
||||||
} catch (AndroidLocationException e) {
|
|
||||||
throw new IllegalArgumentException(e);
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new IllegalArgumentException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void printUsageAndQuit() {
|
|
||||||
// 80 cols marker: 01234567890123456789012345678901234567890123456789012345678901234567890123456789
|
// 80 cols marker: 01234567890123456789012345678901234567890123456789012345678901234567890123456789
|
||||||
System.err.println("A command line tool to package an Android application from various sources.");
|
System.err.println("A command line tool to package an Android application from various sources.");
|
||||||
System.err.println("Usage: apkbuilder <out archive> [-v][-u][-storetype STORE_TYPE] [-z inputzip]");
|
System.err.println("Usage: apkbuilder <out archive> [-v][-u][-storetype STORE_TYPE] [-z inputzip]");
|
||||||
@@ -459,7 +109,7 @@ public final class ApkBuilder {
|
|||||||
System.exit(1);
|
System.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void printAndExit(String... messages) {
|
private static void printAndExit(String... messages) {
|
||||||
for (String message : messages) {
|
for (String message : messages) {
|
||||||
System.err.println(message);
|
System.err.println(message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,437 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2008 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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.android.apkbuilder.internal;
|
||||||
|
|
||||||
|
import com.android.apkbuilder.ApkBuilder.WrongOptionException;
|
||||||
|
import com.android.apkbuilder.ApkBuilder.ApkCreationException;
|
||||||
|
import com.android.jarutils.DebugKeyProvider;
|
||||||
|
import com.android.jarutils.JavaResourceFilter;
|
||||||
|
import com.android.jarutils.SignedJarBuilder;
|
||||||
|
import com.android.jarutils.DebugKeyProvider.KeytoolException;
|
||||||
|
import com.android.prefs.AndroidLocation.AndroidLocationException;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.FilenameFilter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.text.DateFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command line APK builder with signing support.
|
||||||
|
*/
|
||||||
|
public final class ApkBuilderImpl {
|
||||||
|
|
||||||
|
private final static Pattern PATTERN_JAR_EXT = Pattern.compile("^.+\\.jar$",
|
||||||
|
Pattern.CASE_INSENSITIVE);
|
||||||
|
private final static Pattern PATTERN_NATIVELIB_EXT = Pattern.compile("^.+\\.so$",
|
||||||
|
Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
|
private final static String NATIVE_LIB_ROOT = "lib/";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A File to be added to the APK archive.
|
||||||
|
* <p/>This includes the {@link File} representing the file and its path in the archive.
|
||||||
|
*/
|
||||||
|
public final static class ApkFile {
|
||||||
|
String archivePath;
|
||||||
|
File file;
|
||||||
|
|
||||||
|
ApkFile(File file, String path) {
|
||||||
|
this.file = file;
|
||||||
|
this.archivePath = path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private JavaResourceFilter mResourceFilter = new JavaResourceFilter();
|
||||||
|
private boolean mVerbose = false;
|
||||||
|
private boolean mSignedPackage = true;
|
||||||
|
/** the optional type of the debug keystore. If <code>null</code>, the default */
|
||||||
|
private String mStoreType = null;
|
||||||
|
|
||||||
|
public void setVerbose(boolean verbose) {
|
||||||
|
mVerbose = verbose;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSignedPackage(boolean signedPackage) {
|
||||||
|
mSignedPackage = signedPackage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void run(String[] args) throws WrongOptionException, FileNotFoundException,
|
||||||
|
ApkCreationException {
|
||||||
|
if (args.length < 1) {
|
||||||
|
throw new WrongOptionException("No options specified");
|
||||||
|
}
|
||||||
|
|
||||||
|
// read the first args that should be a file path
|
||||||
|
File outFile = getOutFile(args[0]);
|
||||||
|
|
||||||
|
ArrayList<FileInputStream> zipArchives = new ArrayList<FileInputStream>();
|
||||||
|
ArrayList<File> archiveFiles = new ArrayList<File>();
|
||||||
|
ArrayList<ApkFile> javaResources = new ArrayList<ApkFile>();
|
||||||
|
ArrayList<FileInputStream> resourcesJars = new ArrayList<FileInputStream>();
|
||||||
|
ArrayList<ApkFile> nativeLibraries = new ArrayList<ApkFile>();
|
||||||
|
|
||||||
|
int index = 1;
|
||||||
|
do {
|
||||||
|
String argument = args[index++];
|
||||||
|
|
||||||
|
if ("-v".equals(argument)) {
|
||||||
|
mVerbose = true;
|
||||||
|
} else if ("-u".equals(argument)) {
|
||||||
|
mSignedPackage = false;
|
||||||
|
} else if ("-z".equals(argument)) {
|
||||||
|
// quick check on the next argument.
|
||||||
|
if (index == args.length) {
|
||||||
|
throw new WrongOptionException("Missing value for -z");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
FileInputStream input = new FileInputStream(args[index++]);
|
||||||
|
zipArchives.add(input);
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
throw new ApkCreationException("-z file is not found");
|
||||||
|
}
|
||||||
|
} else if ("-f". equals(argument)) {
|
||||||
|
// quick check on the next argument.
|
||||||
|
if (index == args.length) {
|
||||||
|
throw new WrongOptionException("Missing value for -f");
|
||||||
|
}
|
||||||
|
|
||||||
|
archiveFiles.add(getInputFile(args[index++]));
|
||||||
|
} else if ("-rf". equals(argument)) {
|
||||||
|
// quick check on the next argument.
|
||||||
|
if (index == args.length) {
|
||||||
|
throw new WrongOptionException("Missing value for -rf");
|
||||||
|
}
|
||||||
|
|
||||||
|
processSourceFolderForResource(args[index++], javaResources);
|
||||||
|
} else if ("-rj". equals(argument)) {
|
||||||
|
// quick check on the next argument.
|
||||||
|
if (index == args.length) {
|
||||||
|
throw new WrongOptionException("Missing value for -rj");
|
||||||
|
}
|
||||||
|
|
||||||
|
processJarFolder(args[index++], resourcesJars);
|
||||||
|
} else if ("-nf".equals(argument)) {
|
||||||
|
// quick check on the next argument.
|
||||||
|
if (index == args.length) {
|
||||||
|
throw new WrongOptionException("Missing value for -nf");
|
||||||
|
}
|
||||||
|
|
||||||
|
String parameter = args[index++];
|
||||||
|
File f = new File(parameter);
|
||||||
|
|
||||||
|
// compute the offset to get the relative path
|
||||||
|
int offset = parameter.length();
|
||||||
|
if (parameter.endsWith(File.separator) == false) {
|
||||||
|
offset++;
|
||||||
|
}
|
||||||
|
|
||||||
|
processNativeFolder(offset, f, nativeLibraries);
|
||||||
|
} else if ("-storetype".equals(argument)) {
|
||||||
|
// quick check on the next argument.
|
||||||
|
if (index == args.length) {
|
||||||
|
throw new WrongOptionException("Missing value for -storetype");
|
||||||
|
}
|
||||||
|
|
||||||
|
mStoreType = args[index++];
|
||||||
|
} else {
|
||||||
|
throw new WrongOptionException("Unknown argument: " + argument);
|
||||||
|
}
|
||||||
|
} while (index < args.length);
|
||||||
|
|
||||||
|
createPackage(outFile, zipArchives, archiveFiles, javaResources, resourcesJars,
|
||||||
|
nativeLibraries);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private File getOutFile(String filepath) throws ApkCreationException {
|
||||||
|
File f = new File(filepath);
|
||||||
|
|
||||||
|
if (f.isDirectory()) {
|
||||||
|
throw new ApkCreationException(filepath + " is a directory!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (f.exists()) { // will be a file in this case.
|
||||||
|
if (f.canWrite() == false) {
|
||||||
|
throw new ApkCreationException("Cannot write " + filepath);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
if (f.createNewFile() == false) {
|
||||||
|
throw new ApkCreationException("Failed to create " + filepath);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ApkCreationException(
|
||||||
|
"Failed to create '" + filepath + "' : " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static File getInputFile(String filepath) throws ApkCreationException {
|
||||||
|
File f = new File(filepath);
|
||||||
|
|
||||||
|
if (f.isDirectory()) {
|
||||||
|
throw new ApkCreationException(filepath + " is a directory!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (f.exists()) {
|
||||||
|
if (f.canRead() == false) {
|
||||||
|
throw new ApkCreationException("Cannot read " + filepath);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new ApkCreationException(filepath + " does not exists!");
|
||||||
|
}
|
||||||
|
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes a source folder and adds its java resources to a given list of {@link ApkFile}.
|
||||||
|
* @param folderPath the path to the source folder.
|
||||||
|
* @param javaResources the list of {@link ApkFile} to fill.
|
||||||
|
* @throws ApkCreationException
|
||||||
|
*/
|
||||||
|
public static void processSourceFolderForResource(String folderPath,
|
||||||
|
ArrayList<ApkFile> javaResources) throws ApkCreationException {
|
||||||
|
|
||||||
|
File folder = new File(folderPath);
|
||||||
|
|
||||||
|
if (folder.isDirectory()) {
|
||||||
|
// file is a directory, process its content.
|
||||||
|
File[] files = folder.listFiles();
|
||||||
|
for (File file : files) {
|
||||||
|
processFileForResource(file, null, javaResources);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// not a directory? output error and quit.
|
||||||
|
if (folder.exists()) {
|
||||||
|
throw new ApkCreationException(folderPath + " is not a folder!");
|
||||||
|
} else {
|
||||||
|
throw new ApkCreationException(folderPath + " does not exist!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void processJarFolder(String parameter, Collection<FileInputStream> resourcesJars)
|
||||||
|
throws FileNotFoundException {
|
||||||
|
File f = new File(parameter);
|
||||||
|
if (f.isDirectory()) {
|
||||||
|
String[] files = f.list(new FilenameFilter() {
|
||||||
|
public boolean accept(File dir, String name) {
|
||||||
|
return PATTERN_JAR_EXT.matcher(name).matches();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (String file : files) {
|
||||||
|
String path = f.getAbsolutePath() + File.separator + file;
|
||||||
|
FileInputStream input = new FileInputStream(path);
|
||||||
|
resourcesJars.add(input);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
FileInputStream input = new FileInputStream(parameter);
|
||||||
|
resourcesJars.add(input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes a {@link File} that could be a {@link ApkFile}, or a folder containing
|
||||||
|
* java resources.
|
||||||
|
* @param file the {@link File} to process.
|
||||||
|
* @param path the relative path of this file to the source folder. Can be <code>null</code> to
|
||||||
|
* identify a root file.
|
||||||
|
* @param javaResources the Collection of {@link ApkFile} object to fill.
|
||||||
|
*/
|
||||||
|
private static void processFileForResource(File file, String path,
|
||||||
|
Collection<ApkFile> javaResources) {
|
||||||
|
if (file.isDirectory()) {
|
||||||
|
// a directory? we check it
|
||||||
|
if (JavaResourceFilter.checkFolderForPackaging(file.getName())) {
|
||||||
|
// if it's valid, we append its name to the current path.
|
||||||
|
if (path == null) {
|
||||||
|
path = file.getName();
|
||||||
|
} else {
|
||||||
|
path = path + "/" + file.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
// and process its content.
|
||||||
|
File[] files = file.listFiles();
|
||||||
|
for (File contentFile : files) {
|
||||||
|
processFileForResource(contentFile, path, javaResources);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// a file? we check it
|
||||||
|
if (JavaResourceFilter.checkFileForPackaging(file.getName())) {
|
||||||
|
// we append its name to the current path
|
||||||
|
if (path == null) {
|
||||||
|
path = file.getName();
|
||||||
|
} else {
|
||||||
|
path = path + "/" + file.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
// and add it to the list.
|
||||||
|
javaResources.add(new ApkFile(file, path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process a {@link File} for native library inclusion.
|
||||||
|
* @param offset the length of the root folder (used to compute relative path)
|
||||||
|
* @param f the {@link File} to process
|
||||||
|
* @param nativeLibraries the collection to add native libraries to.
|
||||||
|
*/
|
||||||
|
public static void processNativeFolder(int offset, File f,
|
||||||
|
Collection<ApkFile> nativeLibraries) {
|
||||||
|
if (f.isDirectory()) {
|
||||||
|
File[] children = f.listFiles();
|
||||||
|
|
||||||
|
if (children != null) {
|
||||||
|
for (File child : children) {
|
||||||
|
processNativeFolder(offset, child, nativeLibraries);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (f.isFile()) {
|
||||||
|
if (PATTERN_NATIVELIB_EXT.matcher(f.getName()).matches()) {
|
||||||
|
String path = NATIVE_LIB_ROOT +
|
||||||
|
f.getAbsolutePath().substring(offset).replace('\\', '/');
|
||||||
|
|
||||||
|
nativeLibraries.add(new ApkFile(f, path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the application package
|
||||||
|
* @param outFile the package file to create
|
||||||
|
* @param zipArchives the list of zip archive
|
||||||
|
* @param files the list of files to include in the archive
|
||||||
|
* @param javaResources the list of java resources from the source folders.
|
||||||
|
* @param resourcesJars the list of jar files from which to take java resources
|
||||||
|
* @throws ApkCreationException
|
||||||
|
*/
|
||||||
|
public void createPackage(File outFile, Iterable<? extends FileInputStream> zipArchives,
|
||||||
|
Iterable<? extends File> files, Iterable<? extends ApkFile> javaResources,
|
||||||
|
Iterable<? extends FileInputStream> resourcesJars,
|
||||||
|
Iterable<? extends ApkFile> nativeLibraries) throws ApkCreationException {
|
||||||
|
|
||||||
|
// get the debug key
|
||||||
|
try {
|
||||||
|
SignedJarBuilder builder;
|
||||||
|
|
||||||
|
if (mSignedPackage) {
|
||||||
|
System.err.println(String.format("Using keystore: %s",
|
||||||
|
DebugKeyProvider.getDefaultKeyStoreOsPath()));
|
||||||
|
|
||||||
|
|
||||||
|
DebugKeyProvider keyProvider = new DebugKeyProvider(
|
||||||
|
null /* osKeyPath: use default */,
|
||||||
|
mStoreType, null /* IKeyGenOutput */);
|
||||||
|
PrivateKey key = keyProvider.getDebugKey();
|
||||||
|
X509Certificate certificate = (X509Certificate)keyProvider.getCertificate();
|
||||||
|
|
||||||
|
if (key == null) {
|
||||||
|
throw new ApkCreationException("Unable to get debug signature key");
|
||||||
|
}
|
||||||
|
|
||||||
|
// compare the certificate expiration date
|
||||||
|
if (certificate != null && certificate.getNotAfter().compareTo(new Date()) < 0) {
|
||||||
|
// TODO, regenerate a new one.
|
||||||
|
throw new ApkCreationException("Debug Certificate expired on " +
|
||||||
|
DateFormat.getInstance().format(certificate.getNotAfter()));
|
||||||
|
}
|
||||||
|
|
||||||
|
builder = new SignedJarBuilder(
|
||||||
|
new FileOutputStream(outFile.getAbsolutePath(), false /* append */), key,
|
||||||
|
certificate);
|
||||||
|
} else {
|
||||||
|
builder = new SignedJarBuilder(
|
||||||
|
new FileOutputStream(outFile.getAbsolutePath(), false /* append */),
|
||||||
|
null /* key */, null /* certificate */);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the archives
|
||||||
|
for (FileInputStream input : zipArchives) {
|
||||||
|
builder.writeZip(input, null /* filter */);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the single files
|
||||||
|
for (File input : files) {
|
||||||
|
// always put the file at the root of the archive in this case
|
||||||
|
builder.writeFile(input, input.getName());
|
||||||
|
if (mVerbose) {
|
||||||
|
System.err.println(String.format("%1$s => %2$s", input.getAbsolutePath(),
|
||||||
|
input.getName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the java resource from the source folders.
|
||||||
|
for (ApkFile resource : javaResources) {
|
||||||
|
builder.writeFile(resource.file, resource.archivePath);
|
||||||
|
if (mVerbose) {
|
||||||
|
System.err.println(String.format("%1$s => %2$s",
|
||||||
|
resource.file.getAbsolutePath(), resource.archivePath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the java resource from jar files.
|
||||||
|
for (FileInputStream input : resourcesJars) {
|
||||||
|
builder.writeZip(input, mResourceFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the native files
|
||||||
|
for (ApkFile file : nativeLibraries) {
|
||||||
|
builder.writeFile(file.file, file.archivePath);
|
||||||
|
if (mVerbose) {
|
||||||
|
System.err.println(String.format("%1$s => %2$s", file.file.getAbsolutePath(),
|
||||||
|
file.archivePath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// close and sign the application package.
|
||||||
|
builder.close();
|
||||||
|
} catch (KeytoolException e) {
|
||||||
|
if (e.getJavaHome() == null) {
|
||||||
|
throw new ApkCreationException(e.getMessage() +
|
||||||
|
"\nJAVA_HOME seems undefined, setting it will help locating keytool automatically\n" +
|
||||||
|
"You can also manually execute the following command\n:" +
|
||||||
|
e.getCommandLine());
|
||||||
|
} else {
|
||||||
|
throw new ApkCreationException(e.getMessage() +
|
||||||
|
"\nJAVA_HOME is set to: " + e.getJavaHome() +
|
||||||
|
"\nUpdate it if necessary, or manually execute the following command:\n" +
|
||||||
|
e.getCommandLine());
|
||||||
|
}
|
||||||
|
} catch (AndroidLocationException e) {
|
||||||
|
throw new ApkCreationException(e);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new ApkCreationException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user