diff --git a/tools/anttasks/src/com/android/ant/ApkBuilderTask.java b/tools/anttasks/src/com/android/ant/ApkBuilderTask.java index 3758b82f9..af87c78b4 100644 --- a/tools/anttasks/src/com/android/ant/ApkBuilderTask.java +++ b/tools/anttasks/src/com/android/ant/ApkBuilderTask.java @@ -16,8 +16,9 @@ package com.android.ant; -import com.android.apkbuilder.ApkBuilder; -import com.android.apkbuilder.ApkBuilder.ApkFile; +import com.android.apkbuilder.ApkBuilder.ApkCreationException; +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.ProjectProperties; import com.android.sdklib.internal.project.ProjectProperties.PropertyType; @@ -37,7 +38,7 @@ import java.util.Set; import java.util.Map.Entry; public class ApkBuilderTask extends Task { - + /** * Class to represent nested elements. Since they all have only one attribute ('path'), the * same class can be used for all the nested elements (zip, file, sourcefolder, jarfolder, @@ -45,7 +46,7 @@ public class ApkBuilderTask extends Task { */ public final static class Value extends ProjectComponent { String mPath; - + /** * Sets the value of the "path" attribute. * @param path the value. @@ -59,7 +60,7 @@ public class ApkBuilderTask extends Task { private String mBaseName; private boolean mVerbose = false; private boolean mSigned = true; - + private final ArrayList mZipList = new ArrayList(); private final ArrayList mFileList = new ArrayList(); private final ArrayList mSourceList = new ArrayList(); @@ -79,7 +80,7 @@ public class ApkBuilderTask extends Task { public void setOutfolder(Path outFolder) { mOutFolder = outFolder.toString(); } - + /** * Sets the value of the "basename" attribute. * @param baseName the value. @@ -87,7 +88,7 @@ public class ApkBuilderTask extends Task { public void setBasename(String baseName) { mBaseName = baseName; } - + /** * Sets the value of the "verbose" attribute. * @param verbose the value. @@ -95,7 +96,7 @@ public class ApkBuilderTask extends Task { public void setVerbose(boolean verbose) { mVerbose = verbose; } - + /** * Sets the value of the "signed" attribute. * @param signed the value. @@ -103,7 +104,7 @@ public class ApkBuilderTask extends Task { public void setSigned(boolean signed) { mSigned = signed; } - + /** * Returns an object representing a nested zip element. */ @@ -112,7 +113,7 @@ public class ApkBuilderTask extends Task { mZipList.add(zip); return zip; } - + /** * Returns an object representing a nested file element. */ @@ -139,7 +140,7 @@ public class ApkBuilderTask extends Task { mJarList.add(file); return file; } - + /** * Returns an object representing a nested nativefolder element. */ @@ -148,64 +149,64 @@ public class ApkBuilderTask extends Task { mNativeList.add(file); return file; } - + @Override public void execute() throws BuildException { Project taskProject = getProject(); - - ApkBuilder apkBuilder = new ApkBuilder(); + + ApkBuilderImpl apkBuilder = new ApkBuilderImpl(); apkBuilder.setVerbose(mVerbose); apkBuilder.setSignedPackage(mSigned); - + try { // setup the list of everything that needs to go in the archive. - + // go through the list of zip files to add. This will not include // the resource package, which is handled separaly for each apk to create. for (Value v : mZipList) { FileInputStream input = new FileInputStream(v.mPath); mZipArchives.add(input); } - + // now go through the list of file to directly add the to the list. 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. for (Value v : mSourceList) { - ApkBuilder.processSourceFolderForResource(v.mPath, mJavaResources); + ApkBuilderImpl.processSourceFolderForResource(v.mPath, mJavaResources); } - + // now go through the list of jar folders. for (Value v : mJarList) { - ApkBuilder.processJarFolder(v.mPath, mResourcesJars); + ApkBuilderImpl.processJarFolder(v.mPath, mResourcesJars); } - + // now the native lib folder. for (Value v : mNativeList) { String parameter = v.mPath; File f = new File(parameter); - + // compute the offset to get the relative path int offset = parameter.length(); if (parameter.endsWith(File.separator) == false) { offset++; } - ApkBuilder.processNativeFolder(offset, f, mNativeLibraries); + ApkBuilderImpl.processNativeFolder(offset, f, mNativeLibraries); } - + // first do a full resource package createApk(apkBuilder, null /*configName*/, null /*resourceFilter*/); - + // now see if we need to create file with filtered resources. // Get the project base directory. File baseDir = taskProject.getBaseDir(); ProjectProperties properties = ProjectProperties.load(baseDir.getAbsolutePath(), PropertyType.DEFAULT); - + Map apkConfigs = ApkConfigurationHelper.getConfigs(properties); if (apkConfigs.size() > 0) { Set> entrySet = apkConfigs.entrySet(); @@ -217,20 +218,23 @@ public class ApkBuilderTask extends Task { throw new BuildException(e); } catch (IllegalArgumentException e) { throw new BuildException(e); + } catch (ApkCreationException e) { + throw new BuildException(e); } } - + /** * Creates an application package. - * @param apkBuilder + * @param apkBuilder * @param configName the name of the filter config. Can be null in which case a full resource * package will be generated. * @param resourceFilter the resource configuration filter to pass to aapt (if configName is * non null) - * @throws FileNotFoundException + * @throws FileNotFoundException + * @throws ApkCreationException */ - private void createApk(ApkBuilder apkBuilder, String configName, String resourceFilter) - throws FileNotFoundException { + private void createApk(ApkBuilderImpl apkBuilder, String configName, String resourceFilter) + throws FileNotFoundException, ApkCreationException { // All the files to be included in the archive have already been prep'ed up, except // the resource package. // figure out its name. @@ -240,20 +244,20 @@ public class ApkBuilderTask extends Task { } else { filename = mBaseName + ".ap_"; } - + // now we add it to the list of zip archive (it's just a zip file). - + // it's used as a zip archive input FileInputStream resoucePackageZipFile = new FileInputStream(new File(mOutFolder, filename)); mZipArchives.add(resoucePackageZipFile); - + // prepare the filename to generate. Same thing as the resource file. if (configName != null && resourceFilter != null) { filename = mBaseName + "-" + configName; } else { filename = mBaseName; } - + if (mSigned) { filename = filename + "-debug.apk"; } else { @@ -279,13 +283,13 @@ public class ApkBuilderTask extends Task { filename, resourceFilter)); } } - + File f = new File(mOutFolder, filename); - + // and generate the apk apkBuilder.createPackage(f.getAbsoluteFile(), mZipArchives, mArchiveFiles, mJavaResources, mResourcesJars, mNativeLibraries); - + // we are done. We need to remove the resource package from the list of zip archives // in case we have another apk to generate. mZipArchives.remove(resoucePackageZipFile); diff --git a/tools/apkbuilder/src/com/android/apkbuilder/ApkBuilder.java b/tools/apkbuilder/src/com/android/apkbuilder/ApkBuilder.java index 40abff1ad..db9f35591 100644 --- a/tools/apkbuilder/src/com/android/apkbuilder/ApkBuilder.java +++ b/tools/apkbuilder/src/com/android/apkbuilder/ApkBuilder.java @@ -16,419 +16,69 @@ package com.android.apkbuilder; -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 com.android.apkbuilder.internal.ApkBuilderImpl; -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.Date; -import java.util.regex.Pattern; + /** * Command line APK builder with signing support. */ public final class ApkBuilder { - - 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. - *

This includes the {@link File} representing the file and its path in the archive. - */ - public final static class ApkFile { - String archivePath; - File file; + public final static class WrongOptionException extends Exception { + private static final long serialVersionUID = 1L; - ApkFile(File file, String path) { - this.file = file; - this.archivePath = path; + public WrongOptionException(String message) { + super(message); } } - private JavaResourceFilter mResourceFilter = new JavaResourceFilter(); - private boolean mVerbose = false; - private boolean mSignedPackage = true; - /** the optional type of the debug keystore. If null, the default */ - private String mStoreType = null; + public final static class ApkCreationException extends Exception { + private static final long serialVersionUID = 1L; + + public ApkCreationException(String message) { + 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. + *

WARNING: this will call {@link System#exit(int)} if anything goes wrong. + * @param args command line arguments. */ 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 { - // read the first args that should be a file path - File outFile = getOutFile(args[0]); - - ArrayList zipArchives = new ArrayList(); - ArrayList archiveFiles = new ArrayList(); - ArrayList javaResources = new ArrayList(); - ArrayList resourcesJars = new ArrayList(); - ArrayList nativeLibraries = new ArrayList(); - - 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()); + new ApkBuilderImpl().run(args); + } catch (WrongOptionException e) { + printUsageAndQuit(); } catch (FileNotFoundException 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 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 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 null to - * identify a root file. - * @param javaResources the list of {@link ApkFile} object to fill. - */ - private static void processFileForResource(File file, String path, - ArrayList 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 array to add native libraries. - */ - public static void processNativeFolder(int offset, File f, ArrayList 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)); - } + } catch (ApkCreationException e) { + printAndExit(e.getMessage()); } } /** - * Creates the application package - * @param outFile - * @param zipArchives - * @param resourcesJars - * @param files - * @param javaResources - * keystore type of the Java VM is used. + * API entry point similar to the {@link #main(String[])} method. + *

Unlike {@link #main(String[])}, this will not call {@link System#exit(int)} and instead + * will throw exceptions. + * @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 void createPackage(File outFile, ArrayList zipArchives, - ArrayList files, ArrayList javaResources, - ArrayList resourcesJars, ArrayList 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); - } + public static void createApk(String[] args) throws FileNotFoundException, WrongOptionException, + ApkCreationException { + new ApkBuilderImpl().run(args); } - private void printUsageAndQuit() { + private static void printUsageAndQuit() { // 80 cols marker: 01234567890123456789012345678901234567890123456789012345678901234567890123456789 System.err.println("A command line tool to package an Android application from various sources."); System.err.println("Usage: apkbuilder [-v][-u][-storetype STORE_TYPE] [-z inputzip]"); @@ -455,11 +105,11 @@ public final class ApkBuilder { System.err.println(""); System.err.println(" -nf Followed by the root folder containing native libraries to"); System.err.println(" include in the application package."); - + System.exit(1); } - - private void printAndExit(String... messages) { + + private static void printAndExit(String... messages) { for (String message : messages) { System.err.println(message); } diff --git a/tools/apkbuilder/src/com/android/apkbuilder/internal/ApkBuilderImpl.java b/tools/apkbuilder/src/com/android/apkbuilder/internal/ApkBuilderImpl.java new file mode 100644 index 000000000..780af756c --- /dev/null +++ b/tools/apkbuilder/src/com/android/apkbuilder/internal/ApkBuilderImpl.java @@ -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. + *

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 null, 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 zipArchives = new ArrayList(); + ArrayList archiveFiles = new ArrayList(); + ArrayList javaResources = new ArrayList(); + ArrayList resourcesJars = new ArrayList(); + ArrayList nativeLibraries = new ArrayList(); + + 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 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 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 null to + * identify a root file. + * @param javaResources the Collection of {@link ApkFile} object to fill. + */ + private static void processFileForResource(File file, String path, + Collection 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 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 zipArchives, + Iterable files, Iterable javaResources, + Iterable resourcesJars, + Iterable 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); + } + } +}