2 * Copyright (C) 2008 The Android Open Source Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package com.android.apkbuilder.internal;
19 import com.android.apkbuilder.ApkBuilder.WrongOptionException;
20 import com.android.apkbuilder.ApkBuilder.ApkCreationException;
21 import com.android.jarutils.DebugKeyProvider;
22 import com.android.jarutils.JavaResourceFilter;
23 import com.android.jarutils.SignedJarBuilder;
24 import com.android.jarutils.DebugKeyProvider.KeytoolException;
25 import com.android.prefs.AndroidLocation.AndroidLocationException;
28 import java.io.FileInputStream;
29 import java.io.FileNotFoundException;
30 import java.io.FileOutputStream;
31 import java.io.FilenameFilter;
32 import java.io.IOException;
33 import java.security.PrivateKey;
34 import java.security.cert.X509Certificate;
35 import java.text.DateFormat;
36 import java.util.ArrayList;
37 import java.util.Collection;
38 import java.util.Date;
39 import java.util.regex.Pattern;
42 * Command line APK builder with signing support.
44 public final class ApkBuilderImpl {
46 private final static Pattern PATTERN_JAR_EXT = Pattern.compile("^.+\\.jar$",
47 Pattern.CASE_INSENSITIVE);
48 private final static Pattern PATTERN_NATIVELIB_EXT = Pattern.compile("^.+\\.so$",
49 Pattern.CASE_INSENSITIVE);
51 private final static String NATIVE_LIB_ROOT = "lib/";
54 * A File to be added to the APK archive.
55 * <p/>This includes the {@link File} representing the file and its path in the archive.
57 public final static class ApkFile {
61 ApkFile(File file, String path) {
63 this.archivePath = path;
67 private JavaResourceFilter mResourceFilter = new JavaResourceFilter();
68 private boolean mVerbose = false;
69 private boolean mSignedPackage = true;
70 /** the optional type of the debug keystore. If <code>null</code>, the default */
71 private String mStoreType = null;
73 public void setVerbose(boolean verbose) {
77 public void setSignedPackage(boolean signedPackage) {
78 mSignedPackage = signedPackage;
81 public void run(String[] args) throws WrongOptionException, FileNotFoundException,
82 ApkCreationException {
83 if (args.length < 1) {
84 throw new WrongOptionException("No options specified");
87 // read the first args that should be a file path
88 File outFile = getOutFile(args[0]);
90 ArrayList<FileInputStream> zipArchives = new ArrayList<FileInputStream>();
91 ArrayList<File> archiveFiles = new ArrayList<File>();
92 ArrayList<ApkFile> javaResources = new ArrayList<ApkFile>();
93 ArrayList<FileInputStream> resourcesJars = new ArrayList<FileInputStream>();
94 ArrayList<ApkFile> nativeLibraries = new ArrayList<ApkFile>();
98 String argument = args[index++];
100 if ("-v".equals(argument)) {
102 } else if ("-u".equals(argument)) {
103 mSignedPackage = false;
104 } else if ("-z".equals(argument)) {
105 // quick check on the next argument.
106 if (index == args.length) {
107 throw new WrongOptionException("Missing value for -z");
111 FileInputStream input = new FileInputStream(args[index++]);
112 zipArchives.add(input);
113 } catch (FileNotFoundException e) {
114 throw new ApkCreationException("-z file is not found");
116 } else if ("-f". equals(argument)) {
117 // quick check on the next argument.
118 if (index == args.length) {
119 throw new WrongOptionException("Missing value for -f");
122 archiveFiles.add(getInputFile(args[index++]));
123 } else if ("-rf". equals(argument)) {
124 // quick check on the next argument.
125 if (index == args.length) {
126 throw new WrongOptionException("Missing value for -rf");
129 processSourceFolderForResource(new File(args[index++]), javaResources);
130 } else if ("-rj". equals(argument)) {
131 // quick check on the next argument.
132 if (index == args.length) {
133 throw new WrongOptionException("Missing value for -rj");
136 processJar(new File(args[index++]), resourcesJars);
137 } else if ("-nf".equals(argument)) {
138 // quick check on the next argument.
139 if (index == args.length) {
140 throw new WrongOptionException("Missing value for -nf");
143 processNativeFolder(new File(args[index++]), nativeLibraries);
144 } else if ("-storetype".equals(argument)) {
145 // quick check on the next argument.
146 if (index == args.length) {
147 throw new WrongOptionException("Missing value for -storetype");
150 mStoreType = args[index++];
152 throw new WrongOptionException("Unknown argument: " + argument);
154 } while (index < args.length);
156 createPackage(outFile, zipArchives, archiveFiles, javaResources, resourcesJars,
160 private File getOutFile(String filepath) throws ApkCreationException {
161 File f = new File(filepath);
163 if (f.isDirectory()) {
164 throw new ApkCreationException(filepath + " is a directory!");
167 if (f.exists()) { // will be a file in this case.
168 if (f.canWrite() == false) {
169 throw new ApkCreationException("Cannot write " + filepath);
173 if (f.createNewFile() == false) {
174 throw new ApkCreationException("Failed to create " + filepath);
176 } catch (IOException e) {
177 throw new ApkCreationException(
178 "Failed to create '" + filepath + "' : " + e.getMessage());
186 * Returns a {@link File} representing a given file path. The path must represent
187 * an actual existing file (not a directory). The path may be relative.
188 * @param filepath the path to a file.
189 * @return the File representing the path.
190 * @throws ApkCreationException if the path represents a directory or if the file does not
191 * exist, or cannot be read.
193 public static File getInputFile(String filepath) throws ApkCreationException {
194 File f = new File(filepath);
196 if (f.isDirectory()) {
197 throw new ApkCreationException(filepath + " is a directory!");
201 if (f.canRead() == false) {
202 throw new ApkCreationException("Cannot read " + filepath);
205 throw new ApkCreationException(filepath + " does not exists!");
212 * Processes a source folder and adds its java resources to a given list of {@link ApkFile}.
213 * @param folder the folder representing the source folder.
214 * @param javaResources the list of {@link ApkFile} to fill.
215 * @throws ApkCreationException
217 public static void processSourceFolderForResource(File folder,
218 ArrayList<ApkFile> javaResources) throws ApkCreationException {
219 if (folder.isDirectory()) {
220 // file is a directory, process its content.
221 File[] files = folder.listFiles();
222 for (File file : files) {
223 processFileForResource(file, null, javaResources);
226 // not a directory? output error and quit.
227 if (folder.exists()) {
228 throw new ApkCreationException(folder.getAbsolutePath() + " is not a folder!");
230 throw new ApkCreationException(folder.getAbsolutePath() + " does not exist!");
236 * Process a jar file or a jar folder
237 * @param file the {@link File} to process
238 * @param resourcesJars the collection of FileInputStream to fill up with jar files.
239 * @throws FileNotFoundException
241 public static void processJar(File file, Collection<FileInputStream> resourcesJars)
242 throws FileNotFoundException {
243 if (file.isDirectory()) {
244 String[] filenames = file.list(new FilenameFilter() {
245 public boolean accept(File dir, String name) {
246 return PATTERN_JAR_EXT.matcher(name).matches();
250 for (String filename : filenames) {
251 File f = new File(file, filename);
252 processJarFile(f, resourcesJars);
255 processJarFile(file, resourcesJars);
259 public static void processJarFile(File file, Collection<FileInputStream> resourcesJars)
260 throws FileNotFoundException {
261 FileInputStream input = new FileInputStream(file);
262 resourcesJars.add(input);
266 * Processes a {@link File} that could be a {@link ApkFile}, or a folder containing
268 * @param file the {@link File} to process.
269 * @param path the relative path of this file to the source folder. Can be <code>null</code> to
270 * identify a root file.
271 * @param javaResources the Collection of {@link ApkFile} object to fill.
273 private static void processFileForResource(File file, String path,
274 Collection<ApkFile> javaResources) {
275 if (file.isDirectory()) {
276 // a directory? we check it
277 if (JavaResourceFilter.checkFolderForPackaging(file.getName())) {
278 // if it's valid, we append its name to the current path.
280 path = file.getName();
282 path = path + "/" + file.getName();
285 // and process its content.
286 File[] files = file.listFiles();
287 for (File contentFile : files) {
288 processFileForResource(contentFile, path, javaResources);
292 // a file? we check it
293 if (JavaResourceFilter.checkFileForPackaging(file.getName())) {
294 // we append its name to the current path
296 path = file.getName();
298 path = path + "/" + file.getName();
301 // and add it to the list.
302 javaResources.add(new ApkFile(file, path));
308 * Process a {@link File} for native library inclusion.
309 * <p/>The root folder must include folders that include .so files.
310 * @param root the native root folder.
311 * @param nativeLibraries the collection to add native libraries to.
312 * @throws ApkCreationException
314 public static void processNativeFolder(File root, Collection<ApkFile> nativeLibraries)
315 throws ApkCreationException {
316 if (root.isDirectory() == false) {
317 throw new ApkCreationException(root.getAbsolutePath() + " is not a folder!");
320 File[] abiList = root.listFiles();
322 if (abiList != null) {
323 for (File abi : abiList) {
324 if (abi.isDirectory()) { // ignore files
325 File[] libs = abi.listFiles();
327 for (File lib : libs) {
328 if (lib.isFile() && // ignore folders
329 PATTERN_NATIVELIB_EXT.matcher(lib.getName()).matches()) {
331 NATIVE_LIB_ROOT + abi.getName() + "/" + lib.getName();
333 nativeLibraries.add(new ApkFile(lib, path));
343 * Creates the application package
344 * @param outFile the package file to create
345 * @param zipArchives the list of zip archive
346 * @param files the list of files to include in the archive
347 * @param javaResources the list of java resources from the source folders.
348 * @param resourcesJars the list of jar files from which to take java resources
349 * @throws ApkCreationException
351 public void createPackage(File outFile, Iterable<? extends FileInputStream> zipArchives,
352 Iterable<? extends File> files, Iterable<? extends ApkFile> javaResources,
353 Iterable<? extends FileInputStream> resourcesJars,
354 Iterable<? extends ApkFile> nativeLibraries) throws ApkCreationException {
358 SignedJarBuilder builder;
360 if (mSignedPackage) {
361 System.err.println(String.format("Using keystore: %s",
362 DebugKeyProvider.getDefaultKeyStoreOsPath()));
365 DebugKeyProvider keyProvider = new DebugKeyProvider(
366 null /* osKeyPath: use default */,
367 mStoreType, null /* IKeyGenOutput */);
368 PrivateKey key = keyProvider.getDebugKey();
369 X509Certificate certificate = (X509Certificate)keyProvider.getCertificate();
372 throw new ApkCreationException("Unable to get debug signature key");
375 // compare the certificate expiration date
376 if (certificate != null && certificate.getNotAfter().compareTo(new Date()) < 0) {
377 // TODO, regenerate a new one.
378 throw new ApkCreationException("Debug Certificate expired on " +
379 DateFormat.getInstance().format(certificate.getNotAfter()));
382 builder = new SignedJarBuilder(
383 new FileOutputStream(outFile.getAbsolutePath(), false /* append */), key,
386 builder = new SignedJarBuilder(
387 new FileOutputStream(outFile.getAbsolutePath(), false /* append */),
388 null /* key */, null /* certificate */);
392 for (FileInputStream input : zipArchives) {
393 builder.writeZip(input, null /* filter */);
396 // add the single files
397 for (File input : files) {
398 // always put the file at the root of the archive in this case
399 builder.writeFile(input, input.getName());
401 System.err.println(String.format("%1$s => %2$s", input.getAbsolutePath(),
406 // add the java resource from the source folders.
407 for (ApkFile resource : javaResources) {
408 builder.writeFile(resource.file, resource.archivePath);
410 System.err.println(String.format("%1$s => %2$s",
411 resource.file.getAbsolutePath(), resource.archivePath));
415 // add the java resource from jar files.
416 for (FileInputStream input : resourcesJars) {
417 builder.writeZip(input, mResourceFilter);
420 // add the native files
421 for (ApkFile file : nativeLibraries) {
422 builder.writeFile(file.file, file.archivePath);
424 System.err.println(String.format("%1$s => %2$s", file.file.getAbsolutePath(),
429 // close and sign the application package.
431 } catch (KeytoolException e) {
432 if (e.getJavaHome() == null) {
433 throw new ApkCreationException(e.getMessage() +
434 "\nJAVA_HOME seems undefined, setting it will help locating keytool automatically\n" +
435 "You can also manually execute the following command\n:" +
438 throw new ApkCreationException(e.getMessage() +
439 "\nJAVA_HOME is set to: " + e.getJavaHome() +
440 "\nUpdate it if necessary, or manually execute the following command:\n" +
443 } catch (AndroidLocationException e) {
444 throw new ApkCreationException(e);
445 } catch (Exception e) {
446 throw new ApkCreationException(e);