diff options
author | Xavier Ducrohet <xav@android.com> | 2010-06-17 11:04:26 -0700 |
---|---|---|
committer | Xavier Ducrohet <xav@android.com> | 2010-06-22 14:05:29 -0700 |
commit | 7c19dcf5aead68f4f6675db0bac6b495396e2967 (patch) | |
tree | 20dad20d7cd4f61c8e39cfc66d7dd311d6e70dc8 | |
parent | 13b97d8b6a4137d9be28e60196eb6d44dee742fa (diff) | |
download | sdk-7c19dcf5aead68f4f6675db0bac6b495396e2967.zip sdk-7c19dcf5aead68f4f6675db0bac6b495396e2967.tar.gz sdk-7c19dcf5aead68f4f6675db0bac6b495396e2967.tar.bz2 |
New ApkBuilder class.
this is meant to replace the one previously in apkbuilder.jar and the
one in ADT, while being part of the public sdklib API so that other
tools can use it if needed (to deprecate the command line version)
Another changelist will rename the ApkBuilder classes inside
ADT to make things less confusing.
Change-Id: I13f2a09d8d507a85be33af3fe659d175819cb641
12 files changed, 1230 insertions, 1074 deletions
diff --git a/anttasks/src/com/android/ant/ApkBuilderTask.java b/anttasks/src/com/android/ant/ApkBuilderTask.java index bec0417..bc5f53c 100644 --- a/anttasks/src/com/android/ant/ApkBuilderTask.java +++ b/anttasks/src/com/android/ant/ApkBuilderTask.java @@ -16,9 +16,10 @@ package com.android.ant; -import com.android.sdklib.internal.build.ApkBuilderHelper; -import com.android.sdklib.internal.build.ApkBuilderHelper.ApkCreationException; -import com.android.sdklib.internal.build.ApkBuilderHelper.ApkFile; +import com.android.sdklib.build.ApkBuilder; +import com.android.sdklib.build.ApkBuilder.ApkCreationException; +import com.android.sdklib.build.ApkBuilder.DuplicateFileException; +import com.android.sdklib.build.ApkBuilder.SealedApkException; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.Project; @@ -26,12 +27,15 @@ import org.apache.tools.ant.Task; import org.apache.tools.ant.types.Path; import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; +import java.io.FilenameFilter; import java.util.ArrayList; +import java.util.regex.Pattern; public class ApkBuilderTask extends Task { + private final static Pattern PATTERN_JAR_EXT = Pattern.compile("^.+\\.jar$", + Pattern.CASE_INSENSITIVE); + private String mOutFolder; @Deprecated private String mBaseName; private String mApkFilepath; @@ -42,20 +46,15 @@ public class ApkBuilderTask extends Task { private boolean mHasCode = true; private String mAbiFilter = null; + private Path mDexPath; + private final ArrayList<Path> mZipList = new ArrayList<Path>(); - private final ArrayList<Path> mDexList = new ArrayList<Path>(); private final ArrayList<Path> mFileList = new ArrayList<Path>(); private final ArrayList<Path> mSourceList = new ArrayList<Path>(); private final ArrayList<Path> mJarfolderList = new ArrayList<Path>(); private final ArrayList<Path> mJarfileList = new ArrayList<Path>(); private final ArrayList<Path> mNativeList = new ArrayList<Path>(); - private final ArrayList<FileInputStream> mZipArchives = new ArrayList<FileInputStream>(); - private final ArrayList<File> mArchiveFiles = new ArrayList<File>(); - private final ArrayList<ApkFile> mJavaResources = new ArrayList<ApkFile>(); - private final ArrayList<FileInputStream> mResourcesJars = new ArrayList<FileInputStream>(); - private final ArrayList<ApkFile> mNativeLibraries = new ArrayList<ApkFile>(); - /** * Sets the value of the "outfolder" attribute. * @param outFolder the value. @@ -70,7 +69,7 @@ public class ApkBuilderTask extends Task { * @deprecated */ public void setBasename(String baseName) { - System.out.println("WARNNG: Using deprecated 'basename' attribute in ApkBuilderTask." + + System.out.println("WARNING: Using deprecated 'basename' attribute in ApkBuilderTask." + "Use 'apkfilepath' (path) instead."); mBaseName = baseName; } @@ -154,15 +153,19 @@ public class ApkBuilderTask extends Task { * is <code>false</code> in which case it's ignored. */ public Object createDex() { - Path path = new Path(getProject()); - mDexList.add(path); - return path; + if (mDexPath == null) { + return mDexPath = new Path(getProject()); + } else { + throw new BuildException("Only one <dex> inner element can be provided"); + } } /** * Returns an object representing a nested <var>file</var> element. */ public Object createFile() { + System.out.println("WARNING: Using deprecated <file> inner element in ApkBuilderTask." + + "Use <dex path=...> instead."); Path path = new Path(getProject()); mFileList.add(path); return path; @@ -208,36 +211,70 @@ public class ApkBuilderTask extends Task { public void execute() throws BuildException { Project antProject = getProject(); - ApkBuilderHelper apkBuilder = new ApkBuilderHelper(); - apkBuilder.setVerbose(mVerbose); - apkBuilder.setSignedPackage(mSigned); - apkBuilder.setDebugMode(mDebug); + // get the rules revision to figure out how to build the output file. + String rulesRevStr = antProject.getProperty(TaskHelper.PROP_RULES_REV); + int rulesRev = 1; + try { + rulesRev = Integer.parseInt(rulesRevStr); + } catch (NumberFormatException e) { + // this shouldn't happen since setup task is the one setting up every time. + } + + File outputFile; + if (mApkFilepath != null) { + outputFile = new File(mApkFilepath); + } else if (rulesRev == 2) { + if (mSigned) { + outputFile = new File(mOutFolder, mBaseName + "-debug-unaligned.apk"); + } else { + outputFile = new File(mOutFolder, mBaseName + "-unsigned.apk"); + } + } else { + throw new BuildException("missing attribute 'apkFilepath'"); + } + + // check dexPath is only one file. + File dexFile = null; + if (mHasCode) { + String[] dexFiles = mDexPath.list(); + if (dexFiles.length != 1) { + throw new BuildException(String.format( + "Expected one dex file but path value resolve to %d files.", + dexFiles.length)); + } + dexFile = new File(dexFiles[0]); + } try { - // setup the list of everything that needs to go in the archive. + if (mSigned) { + System.out.println(String.format( + "Creating %s and signing it with a debug key...", outputFile.getName())); + } else { + System.out.println(String.format( + "Creating %s for release...", outputFile.getName())); + } - // 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. + ApkBuilder apkBuilder = new ApkBuilder( + outputFile, + new File(mOutFolder, mResourceFile), + dexFile, + mSigned ? ApkBuilder.getDebugKeystore() : null, + mVerbose ? System.out : null); + apkBuilder.setDebugMode(mDebug); + + + // add the content of the zip files. for (Path pathList : mZipList) { for (String path : pathList.list()) { - FileInputStream input = new FileInputStream(path); - mZipArchives.add(input); + apkBuilder.addZipFile(new File(path)); } } - // now go through the list of file to directly add the to the list. + // add the files that go to the root of the archive (this is deprecated) for (Path pathList : mFileList) { for (String path : pathList.list()) { - mArchiveFiles.add(ApkBuilderHelper.getInputFile(path)); - } - } - - // only attempt to add Dex files if hasCode is true. - if (mHasCode) { - for (Path pathList : mDexList) { - for (String path : pathList.list()) { - mArchiveFiles.add(ApkBuilderHelper.getInputFile(path)); - } + File f = new File(path); + apkBuilder.addFile(f, f.getName()); } } @@ -245,8 +282,7 @@ public class ApkBuilderTask extends Task { if (mHasCode) { for (Path pathList : mSourceList) { for (String path : pathList.list()) { - ApkBuilderHelper.processSourceFolderForResource(new File(path), - mJavaResources); + apkBuilder.addSourceFolder(new File(path)); } } } @@ -257,7 +293,15 @@ public class ApkBuilderTask extends Task { // it's ok if top level folders are missing File folder = new File(path); if (folder.isDirectory()) { - ApkBuilderHelper.processJar(folder, mResourcesJars); + String[] filenames = folder.list(new FilenameFilter() { + public boolean accept(File dir, String name) { + return PATTERN_JAR_EXT.matcher(name).matches(); + } + }); + + for (String filename : filenames) { + apkBuilder.addResourcesFromJar(new File(folder, filename)); + } } } } @@ -265,7 +309,7 @@ public class ApkBuilderTask extends Task { // now go through the list of jar files. for (Path pathList : mJarfileList) { for (String path : pathList.list()) { - ApkBuilderHelper.processJar(new File(path), mResourcesJars); + apkBuilder.addResourcesFromJar(new File(path)); } } @@ -275,77 +319,26 @@ public class ApkBuilderTask extends Task { // it's ok if top level folders are missing File folder = new File(path); if (folder.isDirectory()) { - ApkBuilderHelper.processNativeFolder(folder, mDebug, - mNativeLibraries, mVerbose, mAbiFilter); + apkBuilder.addNativeLibraries(folder, mAbiFilter); } } } - // get the rules revision - String rulesRevStr = antProject.getProperty(TaskHelper.PROP_RULES_REV); - int rulesRev = 1; - try { - rulesRev = Integer.parseInt(rulesRevStr); - } catch (NumberFormatException e) { - // this shouldn't happen since setup task is the one setting up every time. - } - - - File file; - if (mApkFilepath != null) { - file = new File(mApkFilepath); - } else if (rulesRev == 2) { - if (mSigned) { - file = new File(mOutFolder, mBaseName + "-debug-unaligned.apk"); - } else { - file = new File(mOutFolder, mBaseName + "-unsigned.apk"); - } - } else { - throw new BuildException("missing attribute 'apkFilepath'"); - } - // create the package. - createApk(apkBuilder, file); + // close the archive + apkBuilder.sealApk(); - } catch (FileNotFoundException e) { - throw new BuildException(e); - } catch (IllegalArgumentException e) { + } catch (DuplicateFileException e) { + System.err.println(String.format( + "Found duplicate file for APK: %1$s\nOrigin 1: %2$s\nOrigin 2: %3$s", + e.getArchivePath(), e.getFile1(), e.getFile2())); throw new BuildException(e); } catch (ApkCreationException e) { - e.printStackTrace(); + throw new BuildException(e); + } catch (SealedApkException e) { + throw new BuildException(e); + } catch (IllegalArgumentException e) { throw new BuildException(e); } } - - /** - * Creates an application package. - * @param apkBuilder - * @param outputfile the file to generate - * @throws FileNotFoundException - * @throws ApkCreationException - */ - private void createApk(ApkBuilderHelper apkBuilder, File outputfile) - throws FileNotFoundException, ApkCreationException { - - // add the resource pack file as a zip archive input. - FileInputStream resoucePackageZipFile = new FileInputStream( - new File(mOutFolder, mResourceFile)); - mZipArchives.add(resoucePackageZipFile); - - if (mSigned) { - System.out.println(String.format( - "Creating %s and signing it with a debug key...", outputfile.getName())); - } else { - System.out.println(String.format( - "Creating %s for release...", outputfile.getName())); - } - - // and generate the apk - apkBuilder.createPackage(outputfile.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/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AndroidConstants.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AndroidConstants.java index d5d2185..73f6fcb 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AndroidConstants.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AndroidConstants.java @@ -101,8 +101,6 @@ public class AndroidConstants { public final static String FN_COMPILED_RESOURCE_CLASS = FN_RESOURCE_BASE + DOT_CLASS; /** Manifest java class filename, i.e. "Manifest.java" */ public final static String FN_MANIFEST_CLASS = "Manifest.java"; //$NON-NLS-1$ - /** Dex conversion output filname, i.e. "classes.dex" */ - public final static String FN_CLASSES_DEX = "classes.dex"; //$NON-NLS-1$ /** Temporary packaged resources file name, i.e. "resources.ap_" */ public final static String FN_RESOURCES_AP_ = "resources.ap_"; //$NON-NLS-1$ /** Temporary packaged resources file name for a specific set of configuration */ diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/actions/MultiApkExportAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/actions/MultiApkExportAction.java index fe05b1d..aa4b700 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/actions/MultiApkExportAction.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/actions/MultiApkExportAction.java @@ -17,7 +17,6 @@ package com.android.ide.eclipse.adt.internal.actions; import com.android.ide.eclipse.adt.AdtPlugin; -import com.android.ide.eclipse.adt.AndroidConstants; import com.android.ide.eclipse.adt.internal.build.ApkBuilderHelper; import com.android.ide.eclipse.adt.internal.project.ProjectHelper; import com.android.ide.eclipse.adt.internal.project.ProjectState; @@ -282,17 +281,23 @@ public class MultiApkExportAction implements IObjectActionDelegate { apk.setOutputName(softVariant != null ? softVariant.getKey() : null, outputName); // do the final export. - IFile dexFile = projectBinFolder.getFile(AndroidConstants.FN_CLASSES_DEX); + IFile dexFile = projectBinFolder.getFile(SdkConstants.FN_APK_CLASSES_DEX); String outputFile = binFolder.getFile(outputName).getLocation().toOSString(); // get the list of referenced projects. IProject[] javaRefs = ProjectHelper.getReferencedProjects(project); IJavaProject[] referencedJavaProjects = ApkBuilderHelper.getJavaProjects(javaRefs); - helper.finalPackage(new File(projectBinFolderPath, pkgName).getAbsolutePath(), + helper.finalPackage( + new File(projectBinFolderPath, pkgName).getAbsolutePath(), dexFile.getLocation().toOSString(), - outputFile, javaProject, libProjects, referencedJavaProjects, - apk.getAbi(), false /*debuggable*/); + outputFile, + false /*debugSign */, + javaProject, + libProjects, + referencedJavaProjects, + apk.getAbi(), + false /*debuggable*/); } } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ApkBuilder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ApkBuilder.java index 9cd3036..8360feb 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ApkBuilder.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ApkBuilder.java @@ -350,7 +350,7 @@ public class ApkBuilder extends BaseBuilder { // check classes.dex is present. If not we force to recreate it. if (mConvertToDex == false) { - tmp = outputFolder.findMember(AndroidConstants.FN_CLASSES_DEX); + tmp = outputFolder.findMember(SdkConstants.FN_APK_CLASSES_DEX); if (tmp == null || tmp.exists() == false) { mConvertToDex = true; mBuildFinalPackage = true; @@ -445,7 +445,7 @@ public class ApkBuilder extends BaseBuilder { // then we check if we need to package the .class into classes.dex if (mConvertToDex) { if (helper.executeDx(javaProject, osBinPath, osBinPath + File.separator + - AndroidConstants.FN_CLASSES_DEX, referencedJavaProjects) == false) { + SdkConstants.FN_APK_CLASSES_DEX, referencedJavaProjects) == false) { // dx failed, we return return allRefProjects; } @@ -477,10 +477,11 @@ public class ApkBuilder extends BaseBuilder { // This is the default package with all the resources. String classesDexPath = osBinPath + File.separator + - AndroidConstants.FN_CLASSES_DEX; + SdkConstants.FN_APK_CLASSES_DEX; if (helper.finalPackage( osBinPath + File.separator + AndroidConstants.FN_RESOURCES_AP_, - classesDexPath, osFinalPackagePath, javaProject, libProjects, + classesDexPath, osFinalPackagePath, true /*debugSign*/, + javaProject, libProjects, referencedJavaProjects, null /*abiFilter*/, debuggable) == false) { return allRefProjects; } diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ApkBuilderHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ApkBuilderHelper.java index 04acf44..3c2d86a 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ApkBuilderHelper.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ApkBuilderHelper.java @@ -27,12 +27,14 @@ import com.android.ide.eclipse.adt.internal.sdk.Sdk; import com.android.prefs.AndroidLocation.AndroidLocationException; import com.android.sdklib.IAndroidTarget; import com.android.sdklib.SdkConstants; +import com.android.sdklib.build.ApkBuilder; +import com.android.sdklib.build.ApkBuilder.ApkCreationException; +import com.android.sdklib.build.ApkBuilder.DuplicateFileException; +import com.android.sdklib.build.ApkBuilder.JarStatus; +import com.android.sdklib.build.ApkBuilder.SealedApkException; import com.android.sdklib.internal.build.DebugKeyProvider; -import com.android.sdklib.internal.build.JavaResourceFilter; import com.android.sdklib.internal.build.SignedJarBuilder; -import com.android.sdklib.internal.build.DebugKeyProvider.IKeyGenOutput; import com.android.sdklib.internal.build.DebugKeyProvider.KeytoolException; -import com.android.sdklib.internal.build.SignedJarBuilder.IZipEntryFilter; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IFolder; @@ -45,7 +47,6 @@ import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IStatus; -import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.Status; import org.eclipse.jdt.core.IClasspathEntry; import org.eclipse.jdt.core.IJavaProject; @@ -54,73 +55,30 @@ import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jface.preference.IPreferenceStore; import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintStream; -import java.security.GeneralSecurityException; -import java.security.PrivateKey; -import java.security.cert.X509Certificate; -import java.text.DateFormat; import java.util.ArrayList; -import java.util.Date; import java.util.List; +/** + * Helper with methods for the last 3 steps of the generation of an APK. + * + * {@link #packageResources(IFile, IProject[], String, int, String, String)} packages the + * application resources using aapt into a zip file that is ready to be integrated into the apk. + * + * {@link #executeDx(IJavaProject, String, String, IJavaProject[])} will convert the Java byte + * code into the Dalvik bytecode. + * + * {@link #finalPackage(String, String, String, boolean, IJavaProject, IProject[], IJavaProject[], String, boolean)} + * will make the apk from all the previous components. + * + */ public class ApkBuilderHelper { - final static String GDBSERVER_NAME = "gdbserver"; //$NON-NLS-1$ - private final IProject mProject; private final PrintStream mOutStream; private final PrintStream mErrStream; - /** - * Custom {@link IZipEntryFilter} to filter out everything that is not a standard java - * resources, and also record whether the zip file contains native libraries. - * <p/>Used in {@link SignedJarBuilder#writeZip(java.io.InputStream, IZipEntryFilter)} when - * we only want the java resources from external jars. - */ - private final static class JavaAndNativeResourceFilter extends JavaResourceFilter { - private final List<String> mNativeLibs = new ArrayList<String>(); - private boolean mNativeLibInteference = false; - - @Override - public boolean checkEntry(String name) { - boolean value = super.checkEntry(name); - - // only do additional checks if the file passes the default checks. - if (value) { - if (name.endsWith(".so")) { - mNativeLibs.add(name); - - // only .so located in lib/ will interfer with the installation - if (name.startsWith("lib/")) { - mNativeLibInteference = true; - } - } else if (name.endsWith(".jnilib")) { - mNativeLibs.add(name); - } - } - - return value; - } - - List<String> getNativeLibs() { - return mNativeLibs; - } - - boolean getNativeLibInterefence() { - return mNativeLibInteference; - } - - void clear() { - mNativeLibs.clear(); - mNativeLibInteference = false; - } - } - - private final JavaAndNativeResourceFilter mResourceFilter = new JavaAndNativeResourceFilter(); - public ApkBuilderHelper(IProject project, PrintStream outStream, PrintStream errStream) { mProject = project; mOutStream = outStream; @@ -190,7 +148,6 @@ public class ApkBuilderHelper { } return true; - } /** @@ -199,6 +156,7 @@ public class ApkBuilderHelper { * @param intermediateApk The path to the temporary resource file. * @param dex The path to the dex file. * @param output The path to the final package file to create. + * @param debugSign whether the apk must be signed with the debug key. * @param javaProject the java project being compiled * @param libProjects an optional list of library projects (can be null) * @param referencedJavaProjects referenced projects. @@ -209,128 +167,96 @@ public class ApkBuilderHelper { * @return true if success, false otherwise. */ public boolean finalPackage(String intermediateApk, String dex, String output, - final IJavaProject javaProject, IProject[] libProjects, + boolean debugSign, final IJavaProject javaProject, IProject[] libProjects, IJavaProject[] referencedJavaProjects, String abiFilter, boolean debuggable) { - FileOutputStream fos = null; - try { + IProject project = javaProject.getProject(); + + String keystoreOsPath = null; + if (debugSign) { IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore(); - String osKeyPath = store.getString(AdtPrefs.PREFS_CUSTOM_DEBUG_KEYSTORE); - if (osKeyPath == null || new File(osKeyPath).exists() == false) { - osKeyPath = DebugKeyProvider.getDefaultKeyStoreOsPath(); - AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, mProject, - Messages.ApkBuilder_Using_Default_Key); - } else { - AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, mProject, - String.format(Messages.ApkBuilder_Using_s_To_Sign, osKeyPath)); - } + keystoreOsPath = store.getString(AdtPrefs.PREFS_CUSTOM_DEBUG_KEYSTORE); + if (keystoreOsPath == null || new File(keystoreOsPath).isFile() == false) { + try { + keystoreOsPath = DebugKeyProvider.getDefaultKeyStoreOsPath(); + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, mProject, + Messages.ApkBuilder_Using_Default_Key); + } catch (KeytoolException e) { + String eMessage = e.getMessage(); + + // mark the project with the standard message + String msg = String.format(Messages.Final_Archive_Error_s, eMessage); + BaseProjectHelper.markResource(mProject, AndroidConstants.MARKER_PACKAGING, msg, + IMarker.SEVERITY_ERROR); - // TODO: get the store type from somewhere else. - DebugKeyProvider provider = new DebugKeyProvider(osKeyPath, null /* storeType */, - new IKeyGenOutput() { - public void err(String message) { - AdtPlugin.printErrorToConsole(mProject, - Messages.ApkBuilder_Signing_Key_Creation_s + message); - } + // output more info in the console + AdtPlugin.printErrorToConsole(mProject, + msg, + String.format(Messages.ApkBuilder_JAVA_HOME_is_s, e.getJavaHome()), + Messages.ApkBuilder_Update_or_Execute_manually_s, + e.getCommandLine()); - public void out(String message) { - AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, - mProject, - Messages.ApkBuilder_Signing_Key_Creation_s + message); - } - }); - PrivateKey key = provider.getDebugKey(); - X509Certificate certificate = (X509Certificate)provider.getCertificate(); - - if (key == null) { - String msg = String.format(Messages.Final_Archive_Error_s, - Messages.ApkBuilder_Unable_To_Gey_Key); - AdtPlugin.printErrorToConsole(mProject, msg); - BaseProjectHelper.markResource(mProject, AndroidConstants.MARKER_PACKAGING, msg, - IMarker.SEVERITY_ERROR); - return false; - } + return false; + } catch (AndroidLocationException e) { + String eMessage = e.getMessage(); - // compare the certificate expiration date - if (certificate != null && certificate.getNotAfter().compareTo(new Date()) < 0) { - // TODO, regenerate a new one. - String msg = String.format(Messages.Final_Archive_Error_s, - String.format(Messages.ApkBuilder_Certificate_Expired_on_s, - DateFormat.getInstance().format(certificate.getNotAfter()))); - AdtPlugin.printErrorToConsole(mProject, msg); - BaseProjectHelper.markResource(mProject, AndroidConstants.MARKER_PACKAGING, msg, - IMarker.SEVERITY_ERROR); - return false; - } + // mark the project with the standard message + String msg = String.format(Messages.Final_Archive_Error_s, eMessage); + BaseProjectHelper.markResource(mProject, AndroidConstants.MARKER_PACKAGING, msg, + IMarker.SEVERITY_ERROR); - // create the jar builder. - fos = new FileOutputStream(output); - SignedJarBuilder builder = new SignedJarBuilder(fos, key, certificate); - - // add the intermediate file containing the compiled resources. - AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, mProject, - String.format(Messages.ApkBuilder_Packaging_s, intermediateApk)); - FileInputStream fis = new FileInputStream(intermediateApk); - try { - builder.writeZip(fis, null /* filter */); - } finally { - fis.close(); + return false; + } + } else { + AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, mProject, + String.format(Messages.ApkBuilder_Using_s_To_Sign, keystoreOsPath)); } + } - // Now we add the new file to the zip archive for the classes.dex file. - AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, mProject, - String.format(Messages.ApkBuilder_Packaging_s, - AndroidConstants.FN_CLASSES_DEX)); - File entryFile = new File(dex); - builder.writeFile(entryFile, AndroidConstants.FN_CLASSES_DEX); + + try { + ApkBuilder apkBuilder = new ApkBuilder(output, intermediateApk, dex, keystoreOsPath, + AdtPrefs.getPrefs().getBuildVerbosity() == BuildVerbosity.VERBOSE ? + AdtPlugin.getOutPrintStream(project, null): null); + apkBuilder.setDebugMode(debuggable); // Now we write the standard resources from the project and the referenced projects. - writeStandardResources(builder, javaProject, referencedJavaProjects); + writeStandardResources(apkBuilder, javaProject, referencedJavaProjects); - // Now we write the standard resources from the external libraries + // Now we write the standard resources from the external jars for (String libraryOsPath : getExternalJars()) { - AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, mProject, - String.format(Messages.ApkBuilder_Packaging_s, libraryOsPath)); - try { - fis = new FileInputStream(libraryOsPath); - mResourceFilter.clear(); - builder.writeZip(fis, mResourceFilter); - - // check if we found native libraries in the external library. This - // constitutes an error or warning depending on if they are in lib/ - List<String> nativeLibs = mResourceFilter.getNativeLibs(); - boolean nativeInterference = mResourceFilter.getNativeLibInterefence(); - if (nativeLibs.size() > 0) { - String libName = new File(libraryOsPath).getName(); - String msg = String.format("Native libraries detected in '%1$s'. See console for more information.", - libName); - - - BaseProjectHelper.markResource(mProject, AndroidConstants.MARKER_PACKAGING, - msg, - nativeInterference || - AdtPrefs.getPrefs().getBuildForceErrorOnNativeLibInJar() ? - IMarker.SEVERITY_ERROR : IMarker.SEVERITY_WARNING); - - ArrayList<String> consoleMsgs = new ArrayList<String>(); - consoleMsgs.add(String.format( - "The library '%1$s' contains native libraries that will not run on the device.", - libName)); - if (nativeInterference) { - consoleMsgs.add("Additionally some of those libraries will interfer with the installation of the application because of their location in lib/"); - consoleMsgs.add("lib/ is reserved for NDK libraries."); - } - consoleMsgs.add("The following libraries were found:"); - for (String lib : nativeLibs) { - consoleMsgs.add(" - " + lib); - } - AdtPlugin.printErrorToConsole(mProject, - consoleMsgs.toArray()); + JarStatus status = apkBuilder.addResourcesFromJar(new File(libraryOsPath)); + + // check if we found native libraries in the external library. This + // constitutes an error or warning depending on if they are in lib/ + if (status.getNativeLibs().size() > 0) { + String libName = new File(libraryOsPath).getName(); + String msg = String.format( + "Native libraries detected in '%1$s'. See console for more information.", + libName); - return false; + BaseProjectHelper.markResource(mProject, AndroidConstants.MARKER_PACKAGING, + msg, + status.hasNativeLibsConflicts() || + AdtPrefs.getPrefs().getBuildForceErrorOnNativeLibInJar() ? + IMarker.SEVERITY_ERROR : IMarker.SEVERITY_WARNING); + + ArrayList<String> consoleMsgs = new ArrayList<String>(); + consoleMsgs.add(String.format( + "The library '%1$s' contains native libraries that will not run on the device.", + libName)); + if (status.hasNativeLibsConflicts()) { + consoleMsgs.add("Additionally some of those libraries will interfer with the installation of the application because of their location in lib/"); + consoleMsgs.add("lib/ is reserved for NDK libraries."); + } + consoleMsgs.add("The following libraries were found:"); + for (String lib : status.getNativeLibs()) { + consoleMsgs.add(" - " + lib); } - } finally { - fis.close(); + AdtPlugin.printErrorToConsole(mProject, + consoleMsgs.toArray()); + + return false; } } @@ -339,8 +265,8 @@ public class ApkBuilderHelper { IResource libFolder = mProject.findMember(SdkConstants.FD_NATIVE_LIBS); if (libFolder != null && libFolder.exists() && libFolder.getType() == IResource.FOLDER) { - // look inside and put .so in lib/* by keeping the relative folder path. - writeNativeLibraries((IFolder) libFolder, builder, abiFilter, debuggable); + // get a File for the folder. + apkBuilder.addNativeLibraries(libFolder.getLocation().toFile(), abiFilter); } // write the native libraries for the library projects. @@ -349,59 +275,36 @@ public class ApkBuilderHelper { libFolder = lib.findMember(SdkConstants.FD_NATIVE_LIBS); if (libFolder != null && libFolder.exists() && libFolder.getType() == IResource.FOLDER) { - // look inside and put .so in lib/* by keeping the relative folder path. - writeNativeLibraries((IFolder) libFolder, builder, abiFilter, debuggable); + apkBuilder.addNativeLibraries(libFolder.getLocation().toFile(), abiFilter); } } } - // close the jar file and write the manifest and sign it. - builder.close(); - } catch (GeneralSecurityException e1) { - // mark project and return - String msg = String.format(Messages.Final_Archive_Error_s, e1.getMessage()); - AdtPlugin.printErrorToConsole(mProject, msg); - BaseProjectHelper.markResource(mProject, AndroidConstants.MARKER_PACKAGING, msg, - IMarker.SEVERITY_ERROR); - return false; - } catch (IOException e1) { + // seal the APK. + apkBuilder.sealApk(); + return true; + } catch (CoreException e) { // mark project and return - String msg = String.format(Messages.Final_Archive_Error_s, e1.getMessage()); + String msg = String.format(Messages.Final_Archive_Error_s, e.getMessage()); AdtPlugin.printErrorToConsole(mProject, msg); BaseProjectHelper.markResource(mProject, AndroidConstants.MARKER_PACKAGING, msg, IMarker.SEVERITY_ERROR); - return false; - } catch (KeytoolException e) { - String eMessage = e.getMessage(); - - // mark the project with the standard message - String msg = String.format(Messages.Final_Archive_Error_s, eMessage); - BaseProjectHelper.markResource(mProject, AndroidConstants.MARKER_PACKAGING, msg, - IMarker.SEVERITY_ERROR); - - // output more info in the console - AdtPlugin.printErrorToConsole(mProject, - msg, - String.format(Messages.ApkBuilder_JAVA_HOME_is_s, e.getJavaHome()), - Messages.ApkBuilder_Update_or_Execute_manually_s, - e.getCommandLine()); - } catch (AndroidLocationException e) { - String eMessage = e.getMessage(); - - // mark the project with the standard message - String msg = String.format(Messages.Final_Archive_Error_s, eMessage); - BaseProjectHelper.markResource(mProject, AndroidConstants.MARKER_PACKAGING, msg, - IMarker.SEVERITY_ERROR); - - // and also output it in the console - AdtPlugin.printErrorToConsole(mProject, msg); - } catch (CoreException e) { + } catch (ApkCreationException e) { // mark project and return String msg = String.format(Messages.Final_Archive_Error_s, e.getMessage()); AdtPlugin.printErrorToConsole(mProject, msg); BaseProjectHelper.markResource(mProject, AndroidConstants.MARKER_PACKAGING, msg, IMarker.SEVERITY_ERROR); - return false; + } catch (DuplicateFileException e) { + String msg1 = String.format( + "Found duplicate file for APK: %1$s\nOrigin 1: %2$s\nOrigin 2: %3$s", + e.getArchivePath(), e.getFile1(), e.getFile2()); + String msg2 = String.format(Messages.Final_Archive_Error_s, msg1); + AdtPlugin.printErrorToConsole(mProject, msg2); + BaseProjectHelper.markResource(mProject, AndroidConstants.MARKER_PACKAGING, msg2, + IMarker.SEVERITY_ERROR); + } catch (SealedApkException e) { + // this won't happen as we control when the apk is sealed. } catch (Exception e) { // try to catch other exception to actually display an error. This will be useful // if we get an NPE or something so that we can at least notify the user that something @@ -416,18 +319,9 @@ public class ApkBuilderHelper { AdtPlugin.printErrorToConsole(mProject, msg); BaseProjectHelper.markResource(mProject, AndroidConstants.MARKER_PACKAGING, msg, IMarker.SEVERITY_ERROR); - return false; - } finally { - if (fos != null) { - try { - fos.close(); - } catch (IOException e) { - // pass. - } - } } - return true; + return false; } /** @@ -639,80 +533,29 @@ public class ApkBuilderHelper { /** - * Writes native libraries into a {@link SignedJarBuilder}. - * <p/>The native libraries must be located in a given main folder. Under this folder, it is - * expected that the libraries are under a sub-folder that represents the ABI of the library. - * - * The path in the archive is based on the ABI folder name, and located under a main - * folder called "lib". - * - * This method also packages any "gdbserver" executable it finds in the ABI folders, if - * <var>debuggable</var> is set to true. - * - * @param rootFolder The folder containing the native libraries. - * @param jarBuilder the {@link SignedJarBuilder} used to create the archive. - * @param abiFilter an optional filter. If not null, then only the matching ABI is included in - * the final archive - * @param debuggable whether the application is debuggable. If <code>true</code> then gdbserver - * executables will be packaged as well. - * @throws CoreException - * @throws IOException - */ - private void writeNativeLibraries(IFolder rootFolder, SignedJarBuilder jarBuilder, - String abiFilter, boolean debuggable) throws CoreException, IOException { - // the native files must be under a single sub-folder under the main root folder. - // the sub-folder represents the abi for the native libs - IResource[] abis = rootFolder.members(); - for (IResource abi : abis) { - if (abi.getType() == IResource.FOLDER) { // ignore non folders. - - // check the abi filter and reject all other ABIs - if (abiFilter != null && abiFilter.equals(abi.getName()) == false) { - continue; - } - - IResource[] libs = ((IFolder)abi).members(); - - for (IResource lib : libs) { - if (lib.getType() == IResource.FILE) { // ignore non files. - IPath path = lib.getFullPath(); - - // check the extension. - String ext = path.getFileExtension(); - if (AndroidConstants.EXT_NATIVE_LIB.equalsIgnoreCase(ext) || - (debuggable && GDBSERVER_NAME.equals(lib.getName()))) { - // compute the path inside the archive. - IPath apkPath = new Path(SdkConstants.FD_APK_NATIVE_LIBS); - apkPath = apkPath.append(abi.getName()).append(lib.getName()); - - // writes the file in the apk. - jarBuilder.writeFile(lib.getLocation().toFile(), apkPath.toString()); - } - } - } - } - } - } - - /** * Writes the standard resources of a project and its referenced projects * into a {@link SignedJarBuilder}. * Standard resources are non java/aidl files placed in the java package folders. - * @param jarBuilder the {@link SignedJarBuilder}. + * @param apkBuilder the {@link ApkBuilder}. * @param javaProject the javaProject object. * @param referencedJavaProjects the java projects that this project references. - * @throws IOException + * @throws ApkCreationException if an error occurred + * @throws SealedApkException if the APK is already sealed. + * @throws DuplicateFileException if a file conflicts with another already added to the APK + * at the same location inside the APK archive. * @throws CoreException */ - private void writeStandardResources(SignedJarBuilder jarBuilder, IJavaProject javaProject, - IJavaProject[] referencedJavaProjects) throws IOException, CoreException { + private void writeStandardResources(ApkBuilder apkBuilder, IJavaProject javaProject, + IJavaProject[] referencedJavaProjects) + throws DuplicateFileException, ApkCreationException, SealedApkException, + CoreException { IWorkspace ws = ResourcesPlugin.getWorkspace(); IWorkspaceRoot wsRoot = ws.getRoot(); // create a list of path already put into the archive, in order to detect conflict ArrayList<String> list = new ArrayList<String>(); - writeStandardProjectResources(jarBuilder, javaProject, wsRoot, list); + writeStandardProjectResources(apkBuilder, javaProject, wsRoot, list); for (IJavaProject referencedJavaProject : referencedJavaProjects) { // only include output from non android referenced project @@ -720,7 +563,7 @@ public class ApkBuilderHelper { // instrumentation projects that need to reference the projects to be tested). if (referencedJavaProject.getProject().hasNature( AndroidConstants.NATURE_DEFAULT) == false) { - writeStandardProjectResources(jarBuilder, referencedJavaProject, wsRoot, list); + writeStandardProjectResources(apkBuilder, referencedJavaProject, wsRoot, list); } } } @@ -728,15 +571,18 @@ public class ApkBuilderHelper { /** * Writes the standard resources of a {@link IJavaProject} into a {@link SignedJarBuilder}. * Standard resources are non java/aidl files placed in the java package folders. - * @param jarBuilder the {@link SignedJarBuilder}. + * @param jarBuilder the {@link ApkBuilder}. * @param javaProject the javaProject object. * @param wsRoot the {@link IWorkspaceRoot}. * @param list a list of files already added to the archive, to detect conflicts. - * @throws IOException + * @throws ApkCreationException if an error occurred + * @throws SealedApkException if the APK is already sealed. + * @throws DuplicateFileException if a file conflicts with another already added to the APK + * at the same location inside the APK archive. */ - private void writeStandardProjectResources(SignedJarBuilder jarBuilder, + private void writeStandardProjectResources(ApkBuilder apkBuilder, IJavaProject javaProject, IWorkspaceRoot wsRoot, ArrayList<String> list) - throws IOException { + throws DuplicateFileException, ApkCreationException, SealedApkException { // get the source pathes ArrayList<IPath> sourceFolders = BaseProjectHelper.getSourceClasspaths(javaProject); @@ -744,74 +590,12 @@ public class ApkBuilderHelper { for (IPath sourcePath : sourceFolders) { IResource sourceResource = wsRoot.findMember(sourcePath); if (sourceResource != null && sourceResource.getType() == IResource.FOLDER) { - writeStandardSourceFolderResources(jarBuilder, sourcePath, (IFolder)sourceResource, - list); - } - } - } - - /** - * Recursively writes the standard resources of a source folder into a {@link SignedJarBuilder}. - * Standard resources are non java/aidl files placed in the java package folders. - * @param jarBuilder the {@link SignedJarBuilder}. - * @param sourceFolder the {@link IPath} of the source folder. - * @param currentFolder The current folder we're recursively processing. - * @param list a list of files already added to the archive, to detect conflicts. - * @throws IOException - */ - private void writeStandardSourceFolderResources(SignedJarBuilder jarBuilder, IPath sourceFolder, - IFolder currentFolder, ArrayList<String> list) throws IOException { - try { - IResource[] members = currentFolder.members(); - - for (IResource member : members) { - int type = member.getType(); - if (type == IResource.FILE && member.exists()) { - if (checkFileForPackaging((IFile)member)) { - // this files must be added to the archive. - IPath fullPath = member.getFullPath(); - - // We need to create its path inside the archive. - // This path is relative to the source folder. - IPath relativePath = fullPath.removeFirstSegments( - sourceFolder.segmentCount()); - String zipPath = relativePath.toString(); - - // lets check it's not already in the list of path added to the archive - if (list.indexOf(zipPath) != -1) { - AdtPlugin.printErrorToConsole(mProject, - String.format( - Messages.ApkBuilder_s_Conflict_with_file_s, - fullPath, zipPath)); - } else { - // get the File object - File entryFile = member.getLocation().toFile(); - - AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, mProject, - String.format( - Messages.ApkBuilder_Packaging_s_into_s, - fullPath, zipPath)); - - // write it in the zip archive - jarBuilder.writeFile(entryFile, zipPath); - - // and add it to the list of entries - list.add(zipPath); - } - } - } else if (type == IResource.FOLDER) { - if (checkFolderForPackaging((IFolder)member)) { - writeStandardSourceFolderResources(jarBuilder, sourceFolder, - (IFolder)member, list); - } - } + // get a File from the IResource + apkBuilder.addSourceFolder(sourceResource.getLocation().toFile()); } - } catch (CoreException e) { - // if we can't get the members of the folder, we just don't do anything. } } - /** * Returns an array of external jar files used by the project. * @return an array of OS-specific absolute file paths @@ -838,15 +622,11 @@ public class ApkBuilderHelper { // check the name ends with .jar if (AndroidConstants.EXT_JAR.equalsIgnoreCase(path.getFileExtension())) { - boolean local = false; IResource resource = wsRoot.findMember(path); if (resource != null && resource.exists() && resource.getType() == IResource.FILE) { - local = true; oslibraryList.add(resource.getLocation().toOSString()); - } - - if (local == false) { + } else { // if the jar path doesn't match a workspace resource, // then we get an OSString and check if this links to a valid file. String osFullPath = path.toOSString(); @@ -922,7 +702,7 @@ public class ApkBuilderHelper { String name = file.getName(); String ext = file.getFileExtension(); - return JavaResourceFilter.checkFileForPackaging(name, ext); + return ApkBuilder.checkFileForPackaging(name, ext); } /** @@ -932,7 +712,7 @@ public class ApkBuilderHelper { */ static boolean checkFolderForPackaging(IFolder folder) { String name = folder.getName(); - return JavaResourceFilter.checkFolderForPackaging(name); + return ApkBuilder.checkFolderForPackaging(name); } /** diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ApkDeltaVisitor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ApkDeltaVisitor.java index 643fee5..81f06c1 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ApkDeltaVisitor.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ApkDeltaVisitor.java @@ -193,7 +193,7 @@ public class ApkDeltaVisitor extends BaseDeltaVisitor if (mOutputPath.equals(parentPath)) { String resourceName = resource.getName(); // check if classes.dex was removed - if (resourceName.equalsIgnoreCase(AndroidConstants.FN_CLASSES_DEX)) { + if (resourceName.equalsIgnoreCase(SdkConstants.FN_APK_CLASSES_DEX)) { mConvertToDex = true; mMakeFinalPackage = true; } else if (resourceName.equalsIgnoreCase( @@ -237,7 +237,7 @@ public class ApkDeltaVisitor extends BaseDeltaVisitor // inside the native library folder. Test if the changed resource is a .so file. if (type == IResource.FILE && (AndroidConstants.EXT_NATIVE_LIB.equalsIgnoreCase(path.getFileExtension()) - || ApkBuilderHelper.GDBSERVER_NAME.equals(resource.getName()))) { + || SdkConstants.FN_GDBSERVER.equals(resource.getName()))) { mMakeFinalPackage = true; return false; // return false for file. } diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/SdkConstants.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/SdkConstants.java index 36afc5b..824719a 100644 --- a/sdkmanager/libs/sdklib/src/com/android/sdklib/SdkConstants.java +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/SdkConstants.java @@ -48,6 +48,9 @@ public final class SdkConstants { /** An SDK Project's AndroidManifest.xml file */ public static final String FN_ANDROID_MANIFEST_XML= "AndroidManifest.xml"; + /** Dex filename inside the APK. i.e. "classes.dex" */ + public final static String FN_APK_CLASSES_DEX = "classes.dex"; //$NON-NLS-1$ + /** An SDK Project's build.xml file */ public final static String FN_BUILD_XML = "build.xml"; @@ -132,6 +135,11 @@ public final class SdkConstants { /** properties file for the SDK */ public final static String FN_SDK_PROP = "sdk.properties"; //$NON-NLS-1$ + /** + * filename for gdbserver. + */ + public final static String FN_GDBSERVER = "gdbserver"; + /* Folder Names for Android Projects . */ /** Resources folder name, i.e. "res". */ diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/build/ApkBuilder.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/build/ApkBuilder.java new file mode 100644 index 0000000..9f1fc84 --- /dev/null +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/build/ApkBuilder.java @@ -0,0 +1,831 @@ +/* + * Copyright (C) 2010 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.sdklib.build; + +import com.android.sdklib.SdkConstants; +import com.android.sdklib.internal.build.DebugKeyProvider; +import com.android.sdklib.internal.build.SignedJarBuilder; +import com.android.sdklib.internal.build.DebugKeyProvider.IKeyGenOutput; +import com.android.sdklib.internal.build.DebugKeyProvider.KeytoolException; +import com.android.sdklib.internal.build.SignedJarBuilder.IZipEntryFilter; +import com.android.sdklib.internal.build.SignedJarBuilder.IZipEntryFilter.ZipAbortException; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.text.DateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.regex.Pattern; + +/** + * Class making the final apk packaging. + * The inputs are: + * - packaged resources (output of aapt) + * - code file (ouput of dx) + * - Java resources coming from the project, its libraries, and its jar files + * - Native libraries from the project or its library. + * + */ +public final class ApkBuilder { + + private final static Pattern PATTERN_NATIVELIB_EXT = Pattern.compile("^.+\\.so$", + Pattern.CASE_INSENSITIVE); + + /** + * A No-op zip filter. It's used to detect conflicts. + * + */ + private final class NullZipFilter implements IZipEntryFilter { + private File mInputFile; + + void reset(File inputFile) { + mInputFile = inputFile; + } + + public boolean checkEntry(String archivePath) throws ZipAbortException { + verbosePrintln("=> %s", archivePath); + + File duplicate = checkFileForDuplicate(archivePath); + if (duplicate != null) { + throw new DuplicateFileException(archivePath, duplicate, mInputFile); + } else { + mAddedFiles.put(archivePath, mInputFile); + } + + return true; + } + } + + /** + * Custom {@link IZipEntryFilter} to filter out everything that is not a standard java + * resources, and also record whether the zip file contains native libraries. + * <p/>Used in {@link SignedJarBuilder#writeZip(java.io.InputStream, IZipEntryFilter)} when + * we only want the java resources from external jars. + */ + private final class JavaAndNativeResourceFilter implements IZipEntryFilter { + private final List<String> mNativeLibs = new ArrayList<String>(); + private boolean mNativeLibsConflict = false; + private File mInputFile; + + public boolean checkEntry(String archivePath) throws ZipAbortException { + // split the path into segments. + String[] segments = archivePath.split("/"); + + // empty path? skip to next entry. + if (segments.length == 0) { + return false; + } + + // Check each folders to make sure they should be included. + // Folders like CVS, .svn, etc.. should already have been excluded from the + // jar file, but we need to exclude some other folder (like /META-INF) so + // we check anyway. + for (int i = 0 ; i < segments.length - 1; i++) { + if (checkFolderForPackaging(segments[i]) == false) { + return false; + } + } + + // get the file name from the path + String fileName = segments[segments.length-1]; + + boolean check = checkFileForPackaging(fileName); + + // only do additional checks if the file passes the default checks. + if (check) { + verbosePrintln("=> %s", archivePath); + + File duplicate = checkFileForDuplicate(archivePath); + if (duplicate != null) { + throw new DuplicateFileException(archivePath, duplicate, mInputFile); + } else { + mAddedFiles.put(archivePath, mInputFile); + } + + if (archivePath.endsWith(".so")) { + mNativeLibs.add(archivePath); + + // only .so located in lib/ will interfere with the installation + if (archivePath.startsWith(SdkConstants.FD_APK_NATIVE_LIBS + "/")) { + mNativeLibsConflict = true; + } + } else if (archivePath.endsWith(".jnilib")) { + mNativeLibs.add(archivePath); + } + } + + return check; + } + + List<String> getNativeLibs() { + return mNativeLibs; + } + + boolean getNativeLibsConflict() { + return mNativeLibsConflict; + } + + void reset(File inputFile) { + mInputFile = inputFile; + mNativeLibs.clear(); + mNativeLibsConflict = false; + } + } + + private final File mApkFile; + private final File mResFile; + private final File mDexFile; + private final PrintStream mVerboseStream; + private final SignedJarBuilder mBuilder; + private boolean mDebugMode = false; + private boolean mIsSealed = false; + + private final NullZipFilter mNullFilter = new NullZipFilter(); + private final JavaAndNativeResourceFilter mFilter = new JavaAndNativeResourceFilter(); + private final HashMap<String, File> mAddedFiles = new HashMap<String, File>(); + + /** + * An exception thrown during packaging of an APK file. + */ + public final static class ApkCreationException extends Exception { + private static final long serialVersionUID = 1L; + + public ApkCreationException(String format, Object... args) { + super(String.format(format, args)); + } + + public ApkCreationException(Throwable cause, String format, Object... args) { + super(String.format(format, args), cause); + } + + public ApkCreationException(Throwable cause) { + super(cause); + } + } + + /** + * An exception thrown during packaging of an APK file. + */ + public final static class DuplicateFileException extends ZipAbortException { + private static final long serialVersionUID = 1L; + private final String mArchivePath; + private final File mFile1; + private final File mFile2; + + public DuplicateFileException(String archivePath, File file1, File file2) { + super(); + mArchivePath = archivePath; + mFile1 = file1; + mFile2 = file2; + } + + public String getArchivePath() { + return mArchivePath; + } + + public File getFile1() { + return mFile1; + } + + public File getFile2() { + return mFile2; + } + + @Override + public String getMessage() { + return "Duplicate files at the same path inside the APK"; + } + } + + /** + * An exception thrown when trying to add files to a sealed APK. + */ + public final static class SealedApkException extends Exception { + private static final long serialVersionUID = 1L; + + public SealedApkException(String format, Object... args) { + super(String.format(format, args)); + } + + public SealedApkException(Throwable cause, String format, Object... args) { + super(String.format(format, args), cause); + } + + public SealedApkException(Throwable cause) { + super(cause); + } + } + + /** + * Status for the addition of a jar file resources into the APK. + * This indicates possible issues with native library inside the jar file. + */ + public interface JarStatus { + /** + * Returns the list of native libraries found in the jar file. + */ + List<String> getNativeLibs(); + + /** + * Returns whether some of those libraries were located in the location that Android + * expects its native libraries. + */ + boolean hasNativeLibsConflicts(); + + } + + /** Internal implementation of {@link JarStatus}. */ + private final static class JarStatusImpl implements JarStatus { + public final List<String> mLibs; + public final boolean mNativeLibsConflict; + + private JarStatusImpl(List<String> libs, boolean nativeLibsConflict) { + mLibs = libs; + mNativeLibsConflict = nativeLibsConflict; + } + + public List<String> getNativeLibs() { + return mLibs; + } + + public boolean hasNativeLibsConflicts() { + return mNativeLibsConflict; + } + } + + /** + * Creates a new instance. + * @param apkOsPath the OS path of the file to create. + * @param resOsPath the OS path of the packaged resource file. + * @param dexOsPath the OS path of the dex file. This can be null for apk with no code. + * @throws ApkCreationException + */ + public ApkBuilder(String apkOsPath, String resOsPath, String dexOsPath, String storeOsPath, + PrintStream verboseStream) throws ApkCreationException { + this(new File(apkOsPath), + new File(resOsPath), + dexOsPath != null ? new File(dexOsPath) : null, + storeOsPath, + verboseStream); + } + + /** + * Creates a new instance. + * + * This creates a new builder that will create the specified output file, using the two + * mandatory given input files. + * + * An optional debug keystore can be provided. If set, it is expected that the store password + * is 'android' and the key alias and password are 'androiddebugkey' and 'android'. + * + * An optional {@link PrintStream} can also be provided for verbose output. If null, there will + * be no output. + * + * @param apkFile the file to create + * @param resFile the file representing the packaged resource file. + * @param dexFile the file representing the dex file. This can be null for apk with no code. + * @param storeOsPath the OS path to the debug keystore, if needed or null. + * @param verboseStream the stream to which verbose output should go. If null, verbose mode + * is not enabled. + * @throws ApkCreationException + */ + public ApkBuilder(File apkFile, File resFile, File dexFile, String storeOsPath, + PrintStream verboseStream) throws ApkCreationException { + checkOutputFile(mApkFile = apkFile); + checkInputFile(mResFile = resFile); + if (dexFile != null) { + checkInputFile(mDexFile = dexFile); + } else { + mDexFile = null; + } + mVerboseStream = verboseStream; + + try { + File storeFile = null; + if (storeOsPath != null) { + storeFile = new File(storeOsPath); + checkInputFile(storeFile); + } + + if (storeFile != null) { + // get the debug key + verbosePrintln("Using keystore: %s", storeOsPath); + + IKeyGenOutput keygenOutput = null; + if (mVerboseStream != null) { + keygenOutput = new IKeyGenOutput() { + public void out(String message) { + mVerboseStream.println(message); + } + + public void err(String message) { + mVerboseStream.println(message); + } + }; + } + + DebugKeyProvider keyProvider = new DebugKeyProvider( + storeOsPath, null /*store type*/, keygenOutput); + + 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())); + } + + mBuilder = new SignedJarBuilder( + new FileOutputStream(mApkFile, false /* append */), key, + certificate); + } else { + mBuilder = new SignedJarBuilder( + new FileOutputStream(mApkFile, false /* append */), + null /* key */, null /* certificate */); + } + + verbosePrintln("Packaging %s", mApkFile.getName()); + + // add the resources + addZipFile(mResFile); + + // add the class dex file at the root of the apk + if (mDexFile != null) { + addFile(mDexFile, SdkConstants.FN_APK_CLASSES_DEX); + } + + } 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 (Exception e) { + throw new ApkCreationException(e); + } + } + + /** + * Sets the debug mode. In debug mode, when native libraries are present, the packaging + * will also include one or more copies of gdbserver in the final APK file. + * + * These are used for debugging native code, to ensure that gdbserver is accessible to the + * application. + * + * There will be one version of gdbserver for each ABI supported by the application. + * + * the gbdserver files are placed in the libs/abi/ folders automatically by the NDK. + * + * @param debugMode the debug mode flag. + */ + public void setDebugMode(boolean debugMode) { + mDebugMode = debugMode; + } + + /** + * Adds a file to the APK at a given path + * @param file the file to add + * @param archivePath the path of the file inside the APK archive. + * @throws ApkCreationException if an error occurred + * @throws SealedApkException if the APK is already sealed. + * @throws DuplicateFileException if a file conflicts with another already added to the APK + * at the same location inside the APK archive. + */ + public void addFile(File file, String archivePath) throws ApkCreationException, + SealedApkException, DuplicateFileException { + if (mIsSealed) { + throw new SealedApkException("APK is already sealed"); + } + + try { + doAddFile(file, archivePath); + } catch (DuplicateFileException e) { + throw e; + } catch (Exception e) { + throw new ApkCreationException(e, "Failed to add %s", file); + } + } + + /** + * Adds the content from a zip file. + * All file keep the same path inside the archive. + * @param zipFile the zip File. + * @throws ApkCreationException if an error occurred + * @throws SealedApkException if the APK is already sealed. + * @throws DuplicateFileException if a file conflicts with another already added to the APK + * at the same location inside the APK archive. + */ + public void addZipFile(File zipFile) throws ApkCreationException, SealedApkException, + DuplicateFileException { + if (mIsSealed) { + throw new SealedApkException("APK is already sealed"); + } + + try { + verbosePrintln("%s:", zipFile); + + // reset the filter with this input. + mNullFilter.reset(zipFile); + + // ask the builder to add the content of the file. + FileInputStream fis = new FileInputStream(zipFile); + mBuilder.writeZip(fis, mNullFilter); + } catch (DuplicateFileException e) { + throw e; + } catch (Exception e) { + throw new ApkCreationException(e, "Failed to add %s", zipFile); + } + } + + /** + * Adds the resources from a jar file. + * @param jarFile the jar File. + * @return a {@link JarStatus} object indicating if native libraries where found in + * the jar file. + * @throws ApkCreationException if an error occurred + * @throws SealedApkException if the APK is already sealed. + * @throws DuplicateFileException if a file conflicts with another already added to the APK + * at the same location inside the APK archive. + */ + public JarStatus addResourcesFromJar(File jarFile) throws ApkCreationException, + SealedApkException, DuplicateFileException { + if (mIsSealed) { + throw new SealedApkException("APK is already sealed"); + } + + try { + verbosePrintln("%s:", jarFile); + + // reset the filter with this input. + mFilter.reset(jarFile); + + // ask the builder to add the content of the file, filtered to only let through + // the java resources. + FileInputStream fis = new FileInputStream(jarFile); + mBuilder.writeZip(fis, mFilter); + + // check if native libraries were found in the external library. This should + // constitutes an error or warning depending on if they are in lib/ + return new JarStatusImpl(mFilter.getNativeLibs(), mFilter.getNativeLibsConflict()); + } catch (DuplicateFileException e) { + throw e; + } catch (Exception e) { + throw new ApkCreationException(e, "Failed to add %s", jarFile); + } + } + + /** + * Adds the resources from a source folder. + * @param sourceFolder the source folder. + * @throws ApkCreationException if an error occurred + * @throws SealedApkException if the APK is already sealed. + * @throws DuplicateFileException if a file conflicts with another already added to the APK + * at the same location inside the APK archive. + */ + public void addSourceFolder(File sourceFolder) throws ApkCreationException, SealedApkException, + DuplicateFileException { + if (mIsSealed) { + throw new SealedApkException("APK is already sealed"); + } + + if (sourceFolder.isDirectory()) { + try { + // file is a directory, process its content. + File[] files = sourceFolder.listFiles(); + for (File file : files) { + processFileForResource(file, null); + } + } catch (DuplicateFileException e) { + throw e; + } catch (Exception e) { + throw new ApkCreationException(e, "Failed to add %s", sourceFolder); + } + } else { + // not a directory? check if it's a file or doesn't exist + if (sourceFolder.exists()) { + throw new ApkCreationException("%s is not a folder", sourceFolder); + } else { + throw new ApkCreationException("%s does not exist", sourceFolder); + } + } + } + + /** + * Adds the native libraries from the top native folder. + * The content of this folder must be the various ABI folders. + * + * This may or may not copy gdbserver into the apk based on whether the debug mode is set. + * + * @param nativeFolder the native folder. + * @param abiFilter an optional filter. If not null, then only the matching ABI is included in + * the final archive + * @throws ApkCreationException if an error occurred + * @throws SealedApkException if the APK is already sealed. + * @throws DuplicateFileException if a file conflicts with another already added to the APK + * at the same location inside the APK archive. + * + * @see #setDebugMode(boolean) + */ + public void addNativeLibraries(File nativeFolder, String abiFilter) + throws ApkCreationException, SealedApkException, DuplicateFileException { + if (mIsSealed) { + throw new SealedApkException("APK is already sealed"); + } + + if (nativeFolder.isDirectory() == false) { + // not a directory? check if it's a file or doesn't exist + if (nativeFolder.exists()) { + throw new ApkCreationException("%s is not a folder", nativeFolder); + } else { + throw new ApkCreationException("%s does not exist", nativeFolder); + } + } + + File[] abiList = nativeFolder.listFiles(); + + if (abiFilter != null) { + verbosePrintln("Native folder: %1$s with filter %2$ss", nativeFolder, abiFilter); + } else { + verbosePrintln("Native folder: %s", nativeFolder); + } + + if (abiList != null) { + for (File abi : abiList) { + if (abi.isDirectory()) { // ignore files + + // check the abi filter and reject all other ABIs + if (abiFilter != null && abiFilter.equals(abi.getName()) == false) { + continue; + } + + File[] libs = abi.listFiles(); + if (libs != null) { + for (File lib : libs) { + // only consider files that are .so or, if in debug mode, that + // are gdbserver executables + if (lib.isFile() && + (PATTERN_NATIVELIB_EXT.matcher(lib.getName()).matches() || + (mDebugMode && + SdkConstants.FN_GDBSERVER.equals( + lib.getName())))) { + String path = + SdkConstants.FD_APK_NATIVE_LIBS + "/" + + abi.getName() + "/" + lib.getName(); + + try { + doAddFile(lib, path); + } catch (IOException e) { + throw new ApkCreationException(e, "Failed to add %s", lib); + } + } + } + } + } + } + } + } + + /** + * Seals the APK, and signs it if necessary. + * @throws ApkCreationException + * @throws ApkCreationException if an error occurred + * @throws SealedApkException if the APK is already sealed. + */ + public void sealApk() throws ApkCreationException, SealedApkException { + if (mIsSealed) { + throw new SealedApkException("APK is already sealed"); + } + + // close and sign the application package. + try { + mBuilder.close(); + mIsSealed = true; + } catch (Exception e) { + throw new ApkCreationException(e, "Failed to seal APK"); + } + } + + /** + * Output a given message if the verbose mode is enabled. + * @param format the format string for {@link String#format(String, Object...)} + * @param args the string arguments + */ + private void verbosePrintln(String format, Object... args) { + if (mVerboseStream != null) { + mVerboseStream.println(String.format(format, args)); + } + } + + private void doAddFile(File file, String archivePath) throws DuplicateFileException, + IOException { + verbosePrintln("%1$s => %2$s", file, archivePath); + + File duplicate = checkFileForDuplicate(archivePath); + if (duplicate != null) { + throw new DuplicateFileException(archivePath, duplicate, file); + } + + mAddedFiles.put(archivePath, file); + mBuilder.writeFile(file, archivePath); + } + + /** + * 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. + * @throws IOException + * @throws DuplicateFileException if a file conflicts with another already added to the APK + * at the same location inside the APK archive. + */ + private void processFileForResource(File file, String path) + throws IOException, DuplicateFileException { + if (file.isDirectory()) { + // a directory? we check it + if (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); + } + } + } else { + // a file? we check it to make sure it should be added + if (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 apk + doAddFile(file, path); + } + } + } + + /** + * Checks if the given path in the APK archive has not already been used and if it has been, + * then returns a {@link File} object for the source of the duplicate + * @param archivePath the archive path to test. + * @return A File object of either a file at the same location or an archive that contains a + * file that was put at the same location. + */ + private File checkFileForDuplicate(String archivePath) { + return mAddedFiles.get(archivePath); + } + + /** + * Checks an output {@link File} object. + * This checks the following: + * - the file is not an existing directory. + * - if the file exists, that it can be modified. + * - if it doesn't exists, that a new file can be created. + * @param file the File to check + * @throws ApkCreationException If the check fails + */ + private void checkOutputFile(File file) throws ApkCreationException { + if (file.isDirectory()) { + throw new ApkCreationException("%s is a directory!", file); + } + + if (file.exists()) { // will be a file in this case. + if (file.canWrite() == false) { + throw new ApkCreationException("Cannot write %s", file); + } + } else { + try { + if (file.createNewFile() == false) { + throw new ApkCreationException("Failed to create %s", file); + } + } catch (IOException e) { + throw new ApkCreationException( + "Failed to create '%1$ss': %2$s", file, e.getMessage()); + } + } + } + + /** + * Checks an input {@link File} object. + * This checks the following: + * - the file is not an existing directory. + * - that the file exists and can be read. + * @param file the File to check + * @throws ApkCreationException If the check fails + */ + private void checkInputFile(File file) throws ApkCreationException { + if (file.isDirectory()) { + throw new ApkCreationException("%s is a directory!", file); + } + + if (file.exists()) { + if (file.canRead() == false) { + throw new ApkCreationException("Cannot read %s", file); + } + } else { + throw new ApkCreationException("%s does not exist", file); + } + } + + public static String getDebugKeystore() throws ApkCreationException { + try { + return DebugKeyProvider.getDefaultKeyStoreOsPath(); + } catch (Exception e) { + throw new ApkCreationException(e, e.getMessage()); + } + } + + /** + * Checks whether a folder and its content is valid for packaging into the .apk as + * standard Java resource. + * @param folderName the name of the folder. + */ + public static boolean checkFolderForPackaging(String folderName) { + return folderName.equalsIgnoreCase("CVS") == false && + folderName.equalsIgnoreCase(".svn") == false && + folderName.equalsIgnoreCase("SCCS") == false && + folderName.equalsIgnoreCase("META-INF") == false && + folderName.startsWith("_") == false; + } + + /** + * Checks a file to make sure it should be packaged as standard resources. + * @param fileName the name of the file (including extension) + * @return true if the file should be packaged as standard java resources. + */ + public static boolean checkFileForPackaging(String fileName) { + String[] fileSegments = fileName.split("\\."); + String fileExt = ""; + if (fileSegments.length > 1) { + fileExt = fileSegments[fileSegments.length-1]; + } + + return checkFileForPackaging(fileName, fileExt); + } + + /** + * Checks a file to make sure it should be packaged as standard resources. + * @param fileName the name of the file (including extension) + * @param extension the extension of the file (excluding '.') + * @return true if the file should be packaged as standard java resources. + */ + public static boolean checkFileForPackaging(String fileName, String extension) { + // Note: this method is used by com.android.ide.eclipse.adt.internal.build.ApkBuilder + if (fileName.charAt(0) == '.') { // ignore hidden files. + return false; + } + + return "aidl".equalsIgnoreCase(extension) == false && // Aidl files + "java".equalsIgnoreCase(extension) == false && // Java files + "class".equalsIgnoreCase(extension) == false && // Java class files + "scc".equalsIgnoreCase(extension) == false && // VisualSourceSafe + "swp".equalsIgnoreCase(extension) == false && // vi swap file + "package.html".equalsIgnoreCase(fileName) == false && // Javadoc + "overview.html".equalsIgnoreCase(fileName) == false && // Javadoc + ".cvsignore".equalsIgnoreCase(fileName) == false && // CVS + ".DS_Store".equals(fileName) == false && // Mac resources + fileName.charAt(fileName.length()-1) != '~'; // Backup files + } +} diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/build/ApkBuilderMain.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/build/ApkBuilderMain.java index fb6ee6a..626e285 100644 --- a/sdkmanager/libs/sdklib/src/com/android/sdklib/build/ApkBuilderMain.java +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/build/ApkBuilderMain.java @@ -16,14 +16,14 @@ package com.android.sdklib.build; -import com.android.sdklib.internal.build.ApkBuilderHelper; -import com.android.sdklib.internal.build.ApkBuilderHelper.ApkCreationException; -import com.android.sdklib.internal.build.ApkBuilderHelper.ApkFile; +import com.android.sdklib.build.ApkBuilder.ApkCreationException; +import com.android.sdklib.build.ApkBuilder.DuplicateFileException; +import com.android.sdklib.build.ApkBuilder.SealedApkException; import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; +import java.io.FilenameFilter; import java.util.ArrayList; +import java.util.regex.Pattern; /** @@ -31,13 +31,8 @@ import java.util.ArrayList; */ public final class ApkBuilderMain { - public final static class WrongOptionException extends Exception { - private static final long serialVersionUID = 1L; - - public WrongOptionException(String message) { - super(message); - } - } + private final static Pattern PATTERN_JAR_EXT = Pattern.compile("^.+\\.jar$", + Pattern.CASE_INSENSITIVE); /** * Main method. This is meant to be called from the command line through an exec. @@ -49,98 +44,146 @@ public final class ApkBuilderMain { printUsageAndQuit(); } - try { - ApkBuilderHelper helper = new ApkBuilderHelper(); + System.err.println("\nTHIS TOOL IS DEPRECATED. See --help for more information.\n"); + try { + File outApk = new File(args[0]); - // read the first args that should be a file path - File outFile = helper.getOutFile(args[0]); + File dexFile = null; + ArrayList<File> zipArchives = new ArrayList<File>(); + ArrayList<File> sourceFolders = new ArrayList<File>(); + ArrayList<File> jarFiles = new ArrayList<File>(); + ArrayList<File> nativeFolders = new ArrayList<File>(); - 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>(); + boolean verbose = false; + boolean signed = true; + boolean debug = false; int index = 1; do { String argument = args[index++]; if ("-v".equals(argument)) { - helper.setVerbose(true); + verbose = true; + } else if ("-d".equals(argument)) { - helper.setDebugMode(true); + debug = true; + } else if ("-u".equals(argument)) { - helper.setSignedPackage(false); + signed = false; + } else if ("-z".equals(argument)) { // quick check on the next argument. if (index == args.length) { printAndExit("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"); - } + zipArchives.add(new File(args[index++])); } else if ("-f". equals(argument)) { + if (dexFile != null) { + // can't have more than one dex file. + printAndExit("Can't have more than one dex file (-f)"); + } // quick check on the next argument. if (index == args.length) { printAndExit("Missing value for -f"); } - archiveFiles.add(ApkBuilderHelper.getInputFile(args[index++])); + dexFile = new File(args[index++]); } else if ("-rf". equals(argument)) { // quick check on the next argument. if (index == args.length) { printAndExit("Missing value for -rf"); } - ApkBuilderHelper.processSourceFolderForResource( - new File(args[index++]), javaResources); + sourceFolders.add(new File(args[index++])); } else if ("-rj". equals(argument)) { // quick check on the next argument. if (index == args.length) { printAndExit("Missing value for -rj"); } - ApkBuilderHelper.processJar(new File(args[index++]), resourcesJars); + jarFiles.add(new File(args[index++])); } else if ("-nf".equals(argument)) { // quick check on the next argument. if (index == args.length) { printAndExit("Missing value for -nf"); } - ApkBuilderHelper.processNativeFolder(new File(args[index++]), - helper.getDebugMode(), nativeLibraries, - helper.isVerbose(), null /*abiFilter*/); + nativeFolders.add(new File(args[index++])); } else if ("-storetype".equals(argument)) { // quick check on the next argument. if (index == args.length) { printAndExit("Missing value for -storetype"); } - helper.setStoreType(args[index++]); + // FIXME } else { printAndExit("Unknown argument: " + argument); } } while (index < args.length); - helper.createPackage(outFile, zipArchives, archiveFiles, javaResources, resourcesJars, - nativeLibraries); + if (zipArchives.size() == 0) { + printAndExit("No zip archive, there must be one for the resources"); + } + + // create the builder with the basic files. + ApkBuilder builder = new ApkBuilder(outApk, zipArchives.get(0), dexFile, + signed ? ApkBuilder.getDebugKeystore() : null, + verbose ? System.out : null); + builder.setDebugMode(debug); + + // add the rest of the files. + // first zip Archive was used in the constructor. + for (int i = 1 ; i < zipArchives.size() ; i++) { + builder.addZipFile(zipArchives.get(i)); + } + + 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 /*abiFilter*/); + } + + // seal the apk + builder.sealApk(); + - } catch (FileNotFoundException e) { - printAndExit(e.getMessage()); } catch (ApkCreationException e) { printAndExit(e.getMessage()); + } catch (DuplicateFileException e) { + printAndExit(String.format( + "Found duplicate file for APK: %1$s\nOrigin 1: %2$s\nOrigin 2: %3$s", + e.getArchivePath(), e.getFile1(), e.getFile2())); + } catch (SealedApkException e) { + printAndExit(e.getMessage()); + } catch (Exception e) { + e.printStackTrace(); } } private static void printUsageAndQuit() { // 80 cols marker: 01234567890123456789012345678901234567890123456789012345678901234567890123456789 System.err.println("\n\n<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"); - System.err.println("THIS TOOL IS DEPRECATED!\n"); + System.err.println("THIS TOOL IS DEPRECATED and may stop working at any time!\n"); System.err.println("If you wish to use apkbuilder for a custom build system, please look at the"); System.err.println("com.android.sdklib.build.ApkBuilder which provides support for"); System.err.println("recent build improvements including library projects."); diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/build/ApkBuilderHelper.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/build/ApkBuilderHelper.java deleted file mode 100644 index ba1c878..0000000 --- a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/build/ApkBuilderHelper.java +++ /dev/null @@ -1,433 +0,0 @@ -/* - * 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.sdklib.internal.build; - -import com.android.prefs.AndroidLocation.AndroidLocationException; -import com.android.sdklib.internal.build.DebugKeyProvider.KeytoolException; - -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 ApkBuilderHelper { - - 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/"; - private final static String GDBSERVER_NAME = "gdbserver"; - - 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); - } - } - - - /** - * 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; - private boolean mDebugMode = false; - /** 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 boolean isVerbose() { - return mVerbose; - } - - public void setSignedPackage(boolean signedPackage) { - mSignedPackage = signedPackage; - } - - public void setDebugMode(boolean debugMode) { - mDebugMode = debugMode; - } - - public boolean getDebugMode() { - return mDebugMode; - } - - /** - * Set the type of the keystore. <code>null</code> means the default. - * @param storeType - */ - public void setStoreType(String storeType) { - mStoreType = storeType; - } - - /** - * Returns the store type. <code>null</code> means default. - * @return - */ - public String getStoreType() { - return mStoreType; - } - - public 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; - } - - /** - * Returns a {@link File} representing a given file path. The path must represent - * an actual existing file (not a directory). The path may be relative. - * @param filepath the path to a file. - * @return the File representing the path. - * @throws ApkCreationException if the path represents a directory or if the file does not - * exist, or cannot be read. - */ - 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 folder the folder representing the source folder. - * @param javaResources the list of {@link ApkFile} to fill. - * @throws ApkCreationException - */ - public static void processSourceFolderForResource(File folder, - ArrayList<ApkFile> javaResources) throws ApkCreationException { - 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(folder.getAbsolutePath() + " is not a folder!"); - } else { - throw new ApkCreationException(folder.getAbsolutePath() + " does not exist!"); - } - } - } - - /** - * Process a jar file or a jar folder - * @param file the {@link File} to process - * @param resourcesJars the collection of FileInputStream to fill up with jar files. - * @throws FileNotFoundException - * @throws ApkCreationException - */ - public static void processJar(File file, Collection<FileInputStream> resourcesJars) - throws FileNotFoundException, ApkCreationException { - if (file.isDirectory()) { - String[] filenames = file.list(new FilenameFilter() { - public boolean accept(File dir, String name) { - return PATTERN_JAR_EXT.matcher(name).matches(); - } - }); - - for (String filename : filenames) { - File f = new File(file, filename); - processJarFile(f, resourcesJars); - } - } else if (file.isFile()) { - processJarFile(file, resourcesJars); - } else { - throw new ApkCreationException(file.getAbsolutePath()+ " does not exist!"); - } - } - - public static void processJarFile(File file, Collection<FileInputStream> resourcesJars) - throws FileNotFoundException { - FileInputStream input = new FileInputStream(file); - 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. - * <p/>The root folder must include folders that include .so files. - * @param root the native root folder. - * @param nativeLibraries the collection to add native libraries to. - * @param verbose verbose mode. - * @param abiFilter optional ABI filter. If non-null only the given ABI is included. - * @throws ApkCreationException - */ - public static void processNativeFolder(File root, boolean debugMode, - Collection<ApkFile> nativeLibraries, boolean verbose, String abiFilter) - throws ApkCreationException { - if (root.isDirectory() == false) { - throw new ApkCreationException(root.getAbsolutePath() + " is not a folder!"); - } - - File[] abiList = root.listFiles(); - - if (verbose) { - System.out.println("Processing native folder: " + root.getAbsolutePath()); - if (abiFilter != null) { - System.out.println("ABI Filter: " + abiFilter); - } - } - - if (abiList != null) { - for (File abi : abiList) { - if (abi.isDirectory()) { // ignore files - - // check the abi filter and reject all other ABIs - if (abiFilter != null && abiFilter.equals(abi.getName()) == false) { - if (verbose) { - System.out.println("Rejecting ABI " + abi.getName()); - } - continue; - } - - File[] libs = abi.listFiles(); - if (libs != null) { - for (File lib : libs) { - // only consider files that are .so or, if in debug mode, that - // are gdbserver executables - if (lib.isFile() && - (PATTERN_NATIVELIB_EXT.matcher(lib.getName()).matches() || - (debugMode && GDBSERVER_NAME.equals(lib.getName())))) { - String path = - NATIVE_LIB_ROOT + abi.getName() + "/" + lib.getName(); - - nativeLibraries.add(new ApkFile(lib, 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); - } - } -} diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/build/JavaResourceFilter.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/build/JavaResourceFilter.java deleted file mode 100644 index aac1e55..0000000 --- a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/build/JavaResourceFilter.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * 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.sdklib.internal.build; - -import com.android.sdklib.internal.build.SignedJarBuilder.IZipEntryFilter; - -/** - * A basic implementation of {@link IZipEntryFilter} to filter out anything that is not a - * java resource. - */ -public class JavaResourceFilter implements IZipEntryFilter { - - public boolean checkEntry(String name) { - // split the path into segments. - String[] segments = name.split("/"); - - // empty path? skip to next entry. - if (segments.length == 0) { - return false; - } - - // Check each folders to make sure they should be included. - // Folders like CVS, .svn, etc.. should already have been excluded from the - // jar file, but we need to exclude some other folder (like /META-INF) so - // we check anyway. - for (int i = 0 ; i < segments.length - 1; i++) { - if (checkFolderForPackaging(segments[i]) == false) { - return false; - } - } - - // get the file name from the path - String fileName = segments[segments.length-1]; - - return checkFileForPackaging(fileName); - } - - /** - * Checks whether a folder and its content is valid for packaging into the .apk as - * standard Java resource. - * @param folderName the name of the folder. - */ - public static boolean checkFolderForPackaging(String folderName) { - return folderName.equals("CVS") == false && - folderName.equals(".svn") == false && - folderName.equals("SCCS") == false && - folderName.equals("META-INF") == false && - folderName.startsWith("_") == false; - } - - /** - * Checks a file to make sure it should be packaged as standard resources. - * @param fileName the name of the file (including extension) - * @return true if the file should be packaged as standard java resources. - */ - public static boolean checkFileForPackaging(String fileName) { - String[] fileSegments = fileName.split("\\."); - String fileExt = ""; - if (fileSegments.length > 1) { - fileExt = fileSegments[fileSegments.length-1]; - } - - return checkFileForPackaging(fileName, fileExt); - } - - /** - * Checks a file to make sure it should be packaged as standard resources. - * @param fileName the name of the file (including extension) - * @param extension the extension of the file (excluding '.') - * @return true if the file should be packaged as standard java resources. - */ - public static boolean checkFileForPackaging(String fileName, String extension) { - // Note: this method is used by com.android.ide.eclipse.adt.internal.build.ApkBuilder - if (fileName.charAt(0) == '.') { // ignore hidden files. - return false; - } - - return "aidl".equalsIgnoreCase(extension) == false && // Aidl files - "java".equalsIgnoreCase(extension) == false && // Java files - "class".equalsIgnoreCase(extension) == false && // Java class files - "scc".equalsIgnoreCase(extension) == false && // VisualSourceSafe - "swp".equalsIgnoreCase(extension) == false && // vi swap file - "package.html".equalsIgnoreCase(fileName) == false && // Javadoc - "overview.html".equalsIgnoreCase(fileName) == false && // Javadoc - ".cvsignore".equalsIgnoreCase(fileName) == false && // CVS - ".DS_Store".equals(fileName) == false && // Mac resources - fileName.charAt(fileName.length()-1) != '~'; // Backup files - } -} diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/build/SignedJarBuilder.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/build/SignedJarBuilder.java index 79e4be2..81131bc 100644 --- a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/build/SignedJarBuilder.java +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/build/SignedJarBuilder.java @@ -16,6 +16,8 @@ package com.android.sdklib.internal.build; +import com.android.sdklib.internal.build.SignedJarBuilder.IZipEntryFilter.ZipAbortException; + import sun.misc.BASE64Encoder; import sun.security.pkcs.ContentInfo; import sun.security.pkcs.PKCS7; @@ -100,14 +102,42 @@ public class SignedJarBuilder { * be added to a Jar file. */ public interface IZipEntryFilter { + + /** + * An exception thrown during packaging of a zip file into APK file. + * This is typically thrown by implementations of + * {@link IZipEntryFilter#checkEntry(String)}. + */ + public static class ZipAbortException extends Exception { + private static final long serialVersionUID = 1L; + + public ZipAbortException() { + super(); + } + + public ZipAbortException(String format, Object... args) { + super(String.format(format, args)); + } + + public ZipAbortException(Throwable cause, String format, Object... args) { + super(String.format(format, args), cause); + } + + public ZipAbortException(Throwable cause) { + super(cause); + } + } + + /** * Checks a file for inclusion in a Jar archive. - * @param name the archive file path of the entry + * @param archivePath the archive file path of the entry * @return <code>true</code> if the file should be included. + * @throws ZipAbortException if writing the file should be aborted. */ - public boolean checkEntry(String name); + public boolean checkEntry(String archivePath) throws ZipAbortException; } - + /** * Creates a {@link SignedJarBuilder} with a given output stream, and signing information. * <p/>If either <code>key</code> or <code>certificate</code> is <code>null</code> then @@ -125,18 +155,18 @@ public class SignedJarBuilder { mOutputJar.setLevel(9); mKey = key; mCertificate = certificate; - + if (mKey != null && mCertificate != null) { mManifest = new Manifest(); Attributes main = mManifest.getMainAttributes(); main.putValue("Manifest-Version", "1.0"); main.putValue("Created-By", "1.0 (Android)"); - + mBase64Encoder = new BASE64Encoder(); mMessageDigest = MessageDigest.getInstance(DIGEST_ALGORITHM); } } - + /** * Writes a new {@link File} into the archive. * @param inputFile the {@link File} to write. @@ -147,7 +177,7 @@ public class SignedJarBuilder { // Get an input stream on the file. FileInputStream fis = new FileInputStream(inputFile); try { - + // create the zip entry JarEntry entry = new JarEntry(jarPath); entry.setTime(inputFile.lastModified()); @@ -166,8 +196,11 @@ public class SignedJarBuilder { * @param input the {@link InputStream} for the Jar/Zip to copy. * @param filter the filter or <code>null</code> * @throws IOException + * @throws ZipAbortException if the {@link IZipEntryFilter} filter indicated that the write + * must be aborted. */ - public void writeZip(InputStream input, IZipEntryFilter filter) throws IOException { + public void writeZip(InputStream input, IZipEntryFilter filter) + throws IOException, ZipAbortException { ZipInputStream zis = new ZipInputStream(input); try { @@ -175,19 +208,19 @@ public class SignedJarBuilder { ZipEntry entry; while ((entry = zis.getNextEntry()) != null) { String name = entry.getName(); - + // do not take directories or anything inside a potential META-INF folder. if (entry.isDirectory() || name.startsWith("META-INF/")) { continue; } - + // if we have a filter, we check the entry against it if (filter != null && filter.checkEntry(name) == false) { continue; } - + JarEntry newEntry; - + // Preserve the STORED method of the input entry. if (entry.getMethod() == JarEntry.STORED) { newEntry = new JarEntry(entry); @@ -195,9 +228,9 @@ public class SignedJarBuilder { // Create a new entry so that the compressed len is recomputed. newEntry = new JarEntry(name); } - + writeEntry(zis, newEntry); - + zis.closeEntry(); } } finally { @@ -206,7 +239,7 @@ public class SignedJarBuilder { } /** - * Closes the Jar archive by creating the manifest, and signing the archive. + * Closes the Jar archive by creating the manifest, and signing the archive. * @throws IOException * @throws GeneralSecurityException */ @@ -215,21 +248,21 @@ public class SignedJarBuilder { // write the manifest to the jar file mOutputJar.putNextEntry(new JarEntry(JarFile.MANIFEST_NAME)); mManifest.write(mOutputJar); - + // CERT.SF Signature signature = Signature.getInstance("SHA1with" + mKey.getAlgorithm()); signature.initSign(mKey); mOutputJar.putNextEntry(new JarEntry("META-INF/CERT.SF")); writeSignatureFile(new SignatureOutputStream(mOutputJar, signature)); - + // CERT.* mOutputJar.putNextEntry(new JarEntry("META-INF/CERT." + mKey.getAlgorithm())); writeSignatureBlock(signature, mCertificate, mKey); } - + mOutputJar.close(); } - + /** * Adds an entry to the output jar, and write its content from the {@link InputStream} * @param input The input stream from where to write the entry content. @@ -241,10 +274,10 @@ public class SignedJarBuilder { mOutputJar.putNextEntry(entry); // read the content of the entry from the input stream, and write it into the archive. - int count; + int count; while ((count = input.read(mBuffer)) != -1) { mOutputJar.write(mBuffer, 0, count); - + // update the digest if (mMessageDigest != null) { mMessageDigest.update(mBuffer, 0, count); @@ -253,7 +286,7 @@ public class SignedJarBuilder { // close the entry for this file mOutputJar.closeEntry(); - + if (mManifest != null) { // update the manifest for this entry. Attributes attr = mManifest.getAttributes(entry.getName()); @@ -264,7 +297,7 @@ public class SignedJarBuilder { attr.putValue(DIGEST_ATTR, mBase64Encoder.encode(mMessageDigest.digest())); } } - + /** Writes a .SF file with a digest to the manifest. */ private void writeSignatureFile(OutputStream out) throws IOException, GeneralSecurityException { |