OSDN Git Service

Fix issue with absolute path in -nf parameter of apkbuilder.
[android-x86/sdk.git] / apkbuilder / src / com / android / apkbuilder / internal / ApkBuilderImpl.java
1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
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
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16
17 package com.android.apkbuilder.internal;
18
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;
26
27 import java.io.File;
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;
40
41 /**
42  * Command line APK builder with signing support.
43  */
44 public final class ApkBuilderImpl {
45
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);
50
51     private final static String NATIVE_LIB_ROOT = "lib/";
52
53     /**
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.
56      */
57     public final static class ApkFile {
58         String archivePath;
59         File file;
60
61         ApkFile(File file, String path) {
62             this.file = file;
63             this.archivePath = path;
64         }
65     }
66
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;
72
73     public void setVerbose(boolean verbose) {
74         mVerbose = verbose;
75     }
76
77     public void setSignedPackage(boolean signedPackage) {
78         mSignedPackage = signedPackage;
79     }
80
81     public void run(String[] args) throws WrongOptionException, FileNotFoundException,
82             ApkCreationException {
83         if (args.length < 1) {
84             throw new WrongOptionException("No options specified");
85         }
86
87         // read the first args that should be a file path
88         File outFile = getOutFile(args[0]);
89
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>();
95
96         int index = 1;
97         do {
98             String argument = args[index++];
99
100             if ("-v".equals(argument)) {
101                 mVerbose = true;
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");
108                 }
109
110                 try {
111                     FileInputStream input = new FileInputStream(args[index++]);
112                     zipArchives.add(input);
113                 } catch (FileNotFoundException e) {
114                     throw new ApkCreationException("-z file is not found");
115                 }
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");
120                 }
121
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");
127                 }
128
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");
134                 }
135
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");
141                 }
142
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");
148                 }
149
150                 mStoreType  = args[index++];
151             } else {
152                 throw new WrongOptionException("Unknown argument: " + argument);
153             }
154         } while (index < args.length);
155
156         createPackage(outFile, zipArchives, archiveFiles, javaResources, resourcesJars,
157                 nativeLibraries);
158     }
159
160     private File getOutFile(String filepath) throws ApkCreationException {
161         File f = new File(filepath);
162
163         if (f.isDirectory()) {
164             throw new ApkCreationException(filepath + " is a directory!");
165         }
166
167         if (f.exists()) { // will be a file in this case.
168             if (f.canWrite() == false) {
169                 throw new ApkCreationException("Cannot write " + filepath);
170             }
171         } else {
172             try {
173                 if (f.createNewFile() == false) {
174                     throw new ApkCreationException("Failed to create " + filepath);
175                 }
176             } catch (IOException e) {
177                 throw new ApkCreationException(
178                         "Failed to create '" + filepath + "' : " + e.getMessage());
179             }
180         }
181
182         return f;
183     }
184
185     /**
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.
192      */
193     public static File getInputFile(String filepath) throws ApkCreationException {
194         File f = new File(filepath);
195
196         if (f.isDirectory()) {
197             throw new ApkCreationException(filepath + " is a directory!");
198         }
199
200         if (f.exists()) {
201             if (f.canRead() == false) {
202                 throw new ApkCreationException("Cannot read " + filepath);
203             }
204         } else {
205             throw new ApkCreationException(filepath + " does not exists!");
206         }
207
208         return f;
209     }
210
211     /**
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
216      */
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);
224             }
225         } else {
226             // not a directory? output error and quit.
227             if (folder.exists()) {
228                 throw new ApkCreationException(folder.getAbsolutePath() + " is not a folder!");
229             } else {
230                 throw new ApkCreationException(folder.getAbsolutePath() + " does not exist!");
231             }
232         }
233     }
234
235     /**
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
240      */
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();
247                 }
248             });
249
250             for (String filename : filenames) {
251                 File f = new File(file, filename);
252                 processJarFile(f, resourcesJars);
253             }
254         } else {
255             processJarFile(file, resourcesJars);
256         }
257     }
258
259     public static void processJarFile(File file, Collection<FileInputStream> resourcesJars)
260             throws FileNotFoundException {
261         FileInputStream input = new FileInputStream(file);
262         resourcesJars.add(input);
263     }
264
265     /**
266      * Processes a {@link File} that could be a {@link ApkFile}, or a folder containing
267      * java resources.
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.
272      */
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.
279                 if (path == null) {
280                     path = file.getName();
281                 } else {
282                     path = path + "/" + file.getName();
283                 }
284
285                 // and process its content.
286                 File[] files = file.listFiles();
287                 for (File contentFile : files) {
288                     processFileForResource(contentFile, path, javaResources);
289                 }
290             }
291         } else {
292             // a file? we check it
293             if (JavaResourceFilter.checkFileForPackaging(file.getName())) {
294                 // we append its name to the current path
295                 if (path == null) {
296                     path = file.getName();
297                 } else {
298                     path = path + "/" + file.getName();
299                 }
300
301                 // and add it to the list.
302                 javaResources.add(new ApkFile(file, path));
303             }
304         }
305     }
306
307     /**
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
313      */
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!");
318         }
319
320         File[] abiList = root.listFiles();
321
322         if (abiList != null) {
323             for (File abi : abiList) {
324                 if (abi.isDirectory()) { // ignore files
325                     File[] libs = abi.listFiles();
326                     if (libs != null) {
327                         for (File lib : libs) {
328                             if (lib.isFile() && // ignore folders
329                                     PATTERN_NATIVELIB_EXT.matcher(lib.getName()).matches()) {
330                                 String path =
331                                     NATIVE_LIB_ROOT + abi.getName() + "/" + lib.getName();
332
333                                 nativeLibraries.add(new ApkFile(lib, path));
334                             }
335                         }
336                     }
337                 }
338             }
339         }
340     }
341
342     /**
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
350      */
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 {
355
356         // get the debug key
357         try {
358             SignedJarBuilder builder;
359
360             if (mSignedPackage) {
361                 System.err.println(String.format("Using keystore: %s",
362                         DebugKeyProvider.getDefaultKeyStoreOsPath()));
363
364
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();
370
371                 if (key == null) {
372                     throw new ApkCreationException("Unable to get debug signature key");
373                 }
374
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()));
380                 }
381
382                 builder = new SignedJarBuilder(
383                         new FileOutputStream(outFile.getAbsolutePath(), false /* append */), key,
384                         certificate);
385             } else {
386                 builder = new SignedJarBuilder(
387                         new FileOutputStream(outFile.getAbsolutePath(), false /* append */),
388                         null /* key */, null /* certificate */);
389             }
390
391             // add the archives
392             for (FileInputStream input : zipArchives) {
393                 builder.writeZip(input, null /* filter */);
394             }
395
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());
400                 if (mVerbose) {
401                     System.err.println(String.format("%1$s => %2$s", input.getAbsolutePath(),
402                             input.getName()));
403                 }
404             }
405
406             // add the java resource from the source folders.
407             for (ApkFile resource : javaResources) {
408                 builder.writeFile(resource.file, resource.archivePath);
409                 if (mVerbose) {
410                     System.err.println(String.format("%1$s => %2$s",
411                             resource.file.getAbsolutePath(), resource.archivePath));
412                 }
413             }
414
415             // add the java resource from jar files.
416             for (FileInputStream input : resourcesJars) {
417                 builder.writeZip(input, mResourceFilter);
418             }
419
420             // add the native files
421             for (ApkFile file : nativeLibraries) {
422                 builder.writeFile(file.file, file.archivePath);
423                 if (mVerbose) {
424                     System.err.println(String.format("%1$s => %2$s", file.file.getAbsolutePath(),
425                             file.archivePath));
426                 }
427             }
428
429             // close and sign the application package.
430             builder.close();
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:" +
436                         e.getCommandLine());
437             } else {
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" +
441                         e.getCommandLine());
442             }
443         } catch (AndroidLocationException e) {
444             throw new ApkCreationException(e);
445         } catch (Exception e) {
446             throw new ApkCreationException(e);
447         }
448     }
449 }