Package.cpp revision 1f5762e646bed2290934280464832782766ee68e
1//
2// Copyright 2006 The Android Open Source Project
3//
4// Package assets into Zip files.
5//
6#include "Main.h"
7#include "AaptAssets.h"
8#include "ResourceTable.h"
9#include "ResourceFilter.h"
10
11#include <androidfw/misc.h>
12
13#include <utils/Log.h>
14#include <utils/threads.h>
15#include <utils/List.h>
16#include <utils/Errors.h>
17#include <utils/misc.h>
18
19#include <sys/types.h>
20#include <dirent.h>
21#include <ctype.h>
22#include <errno.h>
23
24using namespace android;
25
26static const char* kExcludeExtension = ".EXCLUDE";
27
28/* these formats are already compressed, or don't compress well */
29static const char* kNoCompressExt[] = {
30    ".jpg", ".jpeg", ".png", ".gif",
31    ".wav", ".mp2", ".mp3", ".ogg", ".aac",
32    ".mpg", ".mpeg", ".mid", ".midi", ".smf", ".jet",
33    ".rtttl", ".imy", ".xmf", ".mp4", ".m4a",
34    ".m4v", ".3gp", ".3gpp", ".3g2", ".3gpp2",
35    ".amr", ".awb", ".wma", ".wmv"
36};
37
38/* fwd decls, so I can write this downward */
39ssize_t processAssets(Bundle* bundle, ZipFile* zip, const sp<AaptAssets>& assets);
40ssize_t processAssets(Bundle* bundle, ZipFile* zip, const sp<AaptDir>& dir,
41                        const AaptGroupEntry& ge, const ResourceFilter* filter);
42bool processFile(Bundle* bundle, ZipFile* zip,
43                        const sp<AaptGroup>& group, const sp<AaptFile>& file);
44bool okayToCompress(Bundle* bundle, const String8& pathName);
45ssize_t processJarFiles(Bundle* bundle, ZipFile* zip);
46
47/*
48 * The directory hierarchy looks like this:
49 * "outputDir" and "assetRoot" are existing directories.
50 *
51 * On success, "bundle->numPackages" will be the number of Zip packages
52 * we created.
53 */
54status_t writeAPK(Bundle* bundle, const sp<AaptAssets>& assets,
55                       const String8& outputFile)
56{
57    #if BENCHMARK
58    fprintf(stdout, "BENCHMARK: Starting APK Bundling \n");
59    long startAPKTime = clock();
60    #endif /* BENCHMARK */
61
62    status_t result = NO_ERROR;
63    ZipFile* zip = NULL;
64    int count;
65
66    //bundle->setPackageCount(0);
67
68    /*
69     * Prep the Zip archive.
70     *
71     * If the file already exists, fail unless "update" or "force" is set.
72     * If "update" is set, update the contents of the existing archive.
73     * Else, if "force" is set, remove the existing archive.
74     */
75    FileType fileType = getFileType(outputFile.string());
76    if (fileType == kFileTypeNonexistent) {
77        // okay, create it below
78    } else if (fileType == kFileTypeRegular) {
79        if (bundle->getUpdate()) {
80            // okay, open it below
81        } else if (bundle->getForce()) {
82            if (unlink(outputFile.string()) != 0) {
83                fprintf(stderr, "ERROR: unable to remove '%s': %s\n", outputFile.string(),
84                        strerror(errno));
85                goto bail;
86            }
87        } else {
88            fprintf(stderr, "ERROR: '%s' exists (use '-f' to force overwrite)\n",
89                    outputFile.string());
90            goto bail;
91        }
92    } else {
93        fprintf(stderr, "ERROR: '%s' exists and is not a regular file\n", outputFile.string());
94        goto bail;
95    }
96
97    if (bundle->getVerbose()) {
98        printf("%s '%s'\n", (fileType == kFileTypeNonexistent) ? "Creating" : "Opening",
99                outputFile.string());
100    }
101
102    status_t status;
103    zip = new ZipFile;
104    status = zip->open(outputFile.string(), ZipFile::kOpenReadWrite | ZipFile::kOpenCreate);
105    if (status != NO_ERROR) {
106        fprintf(stderr, "ERROR: unable to open '%s' as Zip file for writing\n",
107                outputFile.string());
108        goto bail;
109    }
110
111    if (bundle->getVerbose()) {
112        printf("Writing all files...\n");
113    }
114
115    count = processAssets(bundle, zip, assets);
116    if (count < 0) {
117        fprintf(stderr, "ERROR: unable to process assets while packaging '%s'\n",
118                outputFile.string());
119        result = count;
120        goto bail;
121    }
122
123    if (bundle->getVerbose()) {
124        printf("Generated %d file%s\n", count, (count==1) ? "" : "s");
125    }
126
127    count = processJarFiles(bundle, zip);
128    if (count < 0) {
129        fprintf(stderr, "ERROR: unable to process jar files while packaging '%s'\n",
130                outputFile.string());
131        result = count;
132        goto bail;
133    }
134
135    if (bundle->getVerbose())
136        printf("Included %d file%s from jar/zip files.\n", count, (count==1) ? "" : "s");
137
138    result = NO_ERROR;
139
140    /*
141     * Check for cruft.  We set the "marked" flag on all entries we created
142     * or decided not to update.  If the entry isn't already slated for
143     * deletion, remove it now.
144     */
145    {
146        if (bundle->getVerbose())
147            printf("Checking for deleted files\n");
148        int i, removed = 0;
149        for (i = 0; i < zip->getNumEntries(); i++) {
150            ZipEntry* entry = zip->getEntryByIndex(i);
151
152            if (!entry->getMarked() && entry->getDeleted()) {
153                if (bundle->getVerbose()) {
154                    printf("      (removing crufty '%s')\n",
155                        entry->getFileName());
156                }
157                zip->remove(entry);
158                removed++;
159            }
160        }
161        if (bundle->getVerbose() && removed > 0)
162            printf("Removed %d file%s\n", removed, (removed==1) ? "" : "s");
163    }
164
165    /* tell Zip lib to process deletions and other pending changes */
166    result = zip->flush();
167    if (result != NO_ERROR) {
168        fprintf(stderr, "ERROR: Zip flush failed, archive may be hosed\n");
169        goto bail;
170    }
171
172    /* anything here? */
173    if (zip->getNumEntries() == 0) {
174        if (bundle->getVerbose()) {
175            printf("Archive is empty -- removing %s\n", outputFile.getPathLeaf().string());
176        }
177        delete zip;        // close the file so we can remove it in Win32
178        zip = NULL;
179        if (unlink(outputFile.string()) != 0) {
180            fprintf(stderr, "warning: could not unlink '%s'\n", outputFile.string());
181        }
182    }
183
184    // If we've been asked to generate a dependency file for the .ap_ package,
185    // do so here
186    if (bundle->getGenDependencies()) {
187        // The dependency file gets output to the same directory
188        // as the specified output file with an additional .d extension.
189        // e.g. bin/resources.ap_.d
190        String8 dependencyFile = outputFile;
191        dependencyFile.append(".d");
192
193        FILE* fp = fopen(dependencyFile.string(), "a");
194        // Add this file to the dependency file
195        fprintf(fp, "%s \\\n", outputFile.string());
196        fclose(fp);
197    }
198
199    assert(result == NO_ERROR);
200
201bail:
202    delete zip;        // must close before remove in Win32
203    if (result != NO_ERROR) {
204        if (bundle->getVerbose()) {
205            printf("Removing %s due to earlier failures\n", outputFile.string());
206        }
207        if (unlink(outputFile.string()) != 0) {
208            fprintf(stderr, "warning: could not unlink '%s'\n", outputFile.string());
209        }
210    }
211
212    if (result == NO_ERROR && bundle->getVerbose())
213        printf("Done!\n");
214
215    #if BENCHMARK
216    fprintf(stdout, "BENCHMARK: End APK Bundling. Time Elapsed: %f ms \n",(clock() - startAPKTime)/1000.0);
217    #endif /* BENCHMARK */
218    return result;
219}
220
221ssize_t processAssets(Bundle* bundle, ZipFile* zip,
222                      const sp<AaptAssets>& assets)
223{
224    ResourceFilter filter;
225    status_t status = filter.parse(bundle->getConfigurations());
226    if (status != NO_ERROR) {
227        return -1;
228    }
229
230    ssize_t count = 0;
231
232    const size_t N = assets->getGroupEntries().size();
233    for (size_t i=0; i<N; i++) {
234        const AaptGroupEntry& ge = assets->getGroupEntries()[i];
235
236        ssize_t res = processAssets(bundle, zip, assets, ge, &filter);
237        if (res < 0) {
238            return res;
239        }
240
241        count += res;
242    }
243
244    return count;
245}
246
247ssize_t processAssets(Bundle* bundle, ZipFile* zip, const sp<AaptDir>& dir,
248        const AaptGroupEntry& ge, const ResourceFilter* filter)
249{
250    ssize_t count = 0;
251
252    const size_t ND = dir->getDirs().size();
253    size_t i;
254    for (i=0; i<ND; i++) {
255        const sp<AaptDir>& subDir = dir->getDirs().valueAt(i);
256
257        const bool filterable = filter != NULL && subDir->getLeaf().find("mipmap-") != 0;
258
259        if (filterable && subDir->getLeaf() != subDir->getPath() && !filter->match(ge.toParams())) {
260            continue;
261        }
262
263        ssize_t res = processAssets(bundle, zip, subDir, ge, filterable ? filter : NULL);
264        if (res < 0) {
265            return res;
266        }
267        count += res;
268    }
269
270    if (filter != NULL && !filter->match(ge.toParams())) {
271        return count;
272    }
273
274    const size_t NF = dir->getFiles().size();
275    for (i=0; i<NF; i++) {
276        sp<AaptGroup> gp = dir->getFiles().valueAt(i);
277        ssize_t fi = gp->getFiles().indexOfKey(ge);
278        if (fi >= 0) {
279            sp<AaptFile> fl = gp->getFiles().valueAt(fi);
280            if (!processFile(bundle, zip, gp, fl)) {
281                return UNKNOWN_ERROR;
282            }
283            count++;
284        }
285    }
286
287    return count;
288}
289
290/*
291 * Process a regular file, adding it to the archive if appropriate.
292 *
293 * If we're in "update" mode, and the file already exists in the archive,
294 * delete the existing entry before adding the new one.
295 */
296bool processFile(Bundle* bundle, ZipFile* zip,
297                 const sp<AaptGroup>& group, const sp<AaptFile>& file)
298{
299    const bool hasData = file->hasData();
300
301    String8 storageName(group->getPath());
302    storageName.convertToResPath();
303    ZipEntry* entry;
304    bool fromGzip = false;
305    status_t result;
306
307    /*
308     * See if the filename ends in ".EXCLUDE".  We can't use
309     * String8::getPathExtension() because the length of what it considers
310     * to be an extension is capped.
311     *
312     * The Asset Manager doesn't check for ".EXCLUDE" in Zip archives,
313     * so there's no value in adding them (and it makes life easier on
314     * the AssetManager lib if we don't).
315     *
316     * NOTE: this restriction has been removed.  If you're in this code, you
317     * should clean this up, but I'm in here getting rid of Path Name, and I
318     * don't want to make other potentially breaking changes --joeo
319     */
320    int fileNameLen = storageName.length();
321    int excludeExtensionLen = strlen(kExcludeExtension);
322    if (fileNameLen > excludeExtensionLen
323            && (0 == strcmp(storageName.string() + (fileNameLen - excludeExtensionLen),
324                            kExcludeExtension))) {
325        fprintf(stderr, "warning: '%s' not added to Zip\n", storageName.string());
326        return true;
327    }
328
329    if (strcasecmp(storageName.getPathExtension().string(), ".gz") == 0) {
330        fromGzip = true;
331        storageName = storageName.getBasePath();
332    }
333
334    if (bundle->getUpdate()) {
335        entry = zip->getEntryByName(storageName.string());
336        if (entry != NULL) {
337            /* file already exists in archive; there can be only one */
338            if (entry->getMarked()) {
339                fprintf(stderr,
340                        "ERROR: '%s' exists twice (check for with & w/o '.gz'?)\n",
341                        file->getPrintableSource().string());
342                return false;
343            }
344            if (!hasData) {
345                const String8& srcName = file->getSourceFile();
346                time_t fileModWhen;
347                fileModWhen = getFileModDate(srcName.string());
348                if (fileModWhen == (time_t) -1) { // file existence tested earlier,
349                    return false;                 //  not expecting an error here
350                }
351
352                if (fileModWhen > entry->getModWhen()) {
353                    // mark as deleted so add() will succeed
354                    if (bundle->getVerbose()) {
355                        printf("      (removing old '%s')\n", storageName.string());
356                    }
357
358                    zip->remove(entry);
359                } else {
360                    // version in archive is newer
361                    if (bundle->getVerbose()) {
362                        printf("      (not updating '%s')\n", storageName.string());
363                    }
364                    entry->setMarked(true);
365                    return true;
366                }
367            } else {
368                // Generated files are always replaced.
369                zip->remove(entry);
370            }
371        }
372    }
373
374    //android_setMinPriority(NULL, ANDROID_LOG_VERBOSE);
375
376    if (fromGzip) {
377        result = zip->addGzip(file->getSourceFile().string(), storageName.string(), &entry);
378    } else if (!hasData) {
379        /* don't compress certain files, e.g. PNGs */
380        int compressionMethod = bundle->getCompressionMethod();
381        if (!okayToCompress(bundle, storageName)) {
382            compressionMethod = ZipEntry::kCompressStored;
383        }
384        result = zip->add(file->getSourceFile().string(), storageName.string(), compressionMethod,
385                            &entry);
386    } else {
387        result = zip->add(file->getData(), file->getSize(), storageName.string(),
388                           file->getCompressionMethod(), &entry);
389    }
390    if (result == NO_ERROR) {
391        if (bundle->getVerbose()) {
392            printf("      '%s'%s", storageName.string(), fromGzip ? " (from .gz)" : "");
393            if (entry->getCompressionMethod() == ZipEntry::kCompressStored) {
394                printf(" (not compressed)\n");
395            } else {
396                printf(" (compressed %d%%)\n", calcPercent(entry->getUncompressedLen(),
397                            entry->getCompressedLen()));
398            }
399        }
400        entry->setMarked(true);
401    } else {
402        if (result == ALREADY_EXISTS) {
403            fprintf(stderr, "      Unable to add '%s': file already in archive (try '-u'?)\n",
404                    file->getPrintableSource().string());
405        } else {
406            fprintf(stderr, "      Unable to add '%s': Zip add failed\n",
407                    file->getPrintableSource().string());
408        }
409        return false;
410    }
411
412    return true;
413}
414
415/*
416 * Determine whether or not we want to try to compress this file based
417 * on the file extension.
418 */
419bool okayToCompress(Bundle* bundle, const String8& pathName)
420{
421    String8 ext = pathName.getPathExtension();
422    int i;
423
424    if (ext.length() == 0)
425        return true;
426
427    for (i = 0; i < NELEM(kNoCompressExt); i++) {
428        if (strcasecmp(ext.string(), kNoCompressExt[i]) == 0)
429            return false;
430    }
431
432    const android::Vector<const char*>& others(bundle->getNoCompressExtensions());
433    for (i = 0; i < (int)others.size(); i++) {
434        const char* str = others[i];
435        int pos = pathName.length() - strlen(str);
436        if (pos < 0) {
437            continue;
438        }
439        const char* path = pathName.string();
440        if (strcasecmp(path + pos, str) == 0) {
441            return false;
442        }
443    }
444
445    return true;
446}
447
448bool endsWith(const char* haystack, const char* needle)
449{
450    size_t a = strlen(haystack);
451    size_t b = strlen(needle);
452    if (a < b) return false;
453    return strcasecmp(haystack+(a-b), needle) == 0;
454}
455
456ssize_t processJarFile(ZipFile* jar, ZipFile* out)
457{
458    status_t err;
459    size_t N = jar->getNumEntries();
460    size_t count = 0;
461    for (size_t i=0; i<N; i++) {
462        ZipEntry* entry = jar->getEntryByIndex(i);
463        const char* storageName = entry->getFileName();
464        if (endsWith(storageName, ".class")) {
465            int compressionMethod = entry->getCompressionMethod();
466            size_t size = entry->getUncompressedLen();
467            const void* data = jar->uncompress(entry);
468            if (data == NULL) {
469                fprintf(stderr, "ERROR: unable to uncompress entry '%s'\n",
470                    storageName);
471                return -1;
472            }
473            out->add(data, size, storageName, compressionMethod, NULL);
474            free((void*)data);
475        }
476        count++;
477    }
478    return count;
479}
480
481ssize_t processJarFiles(Bundle* bundle, ZipFile* zip)
482{
483    status_t err;
484    ssize_t count = 0;
485    const android::Vector<const char*>& jars = bundle->getJarFiles();
486
487    size_t N = jars.size();
488    for (size_t i=0; i<N; i++) {
489        ZipFile jar;
490        err = jar.open(jars[i], ZipFile::kOpenReadOnly);
491        if (err != 0) {
492            fprintf(stderr, "ERROR: unable to open '%s' as a zip file: %d\n",
493                jars[i], err);
494            return err;
495        }
496        err += processJarFile(&jar, zip);
497        if (err < 0) {
498            fprintf(stderr, "ERROR: unable to process '%s'\n", jars[i]);
499            return err;
500        }
501        count += err;
502    }
503
504    return count;
505}
506