Fix noCompress support in aapt.
[android/platform/tools/build.git] / builder / src / main / java / com / android / builder / AndroidBuilder.java
1 /*
2  * Copyright (C) 2012 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.builder;
18
19 import com.android.annotations.NonNull;
20 import com.android.annotations.Nullable;
21 import com.android.annotations.VisibleForTesting;
22 import com.android.builder.packaging.DuplicateFileException;
23 import com.android.builder.packaging.JavaResourceProcessor;
24 import com.android.builder.packaging.Packager;
25 import com.android.builder.packaging.PackagerException;
26 import com.android.builder.packaging.SealedPackageException;
27 import com.android.builder.signing.DebugKeyHelper;
28 import com.android.builder.signing.KeystoreHelper;
29 import com.android.builder.signing.KeytoolException;
30 import com.android.builder.signing.SigningInfo;
31 import com.android.manifmerger.ManifestMerger;
32 import com.android.manifmerger.MergerLog;
33 import com.android.prefs.AndroidLocation.AndroidLocationException;
34 import com.android.sdklib.IAndroidTarget;
35 import com.android.sdklib.IAndroidTarget.IOptionalLibrary;
36 import com.android.sdklib.io.FileOp;
37 import com.android.utils.ILogger;
38
39 import java.io.File;
40 import java.io.FileNotFoundException;
41 import java.io.IOException;
42 import java.util.ArrayList;
43 import java.util.List;
44 import java.util.Set;
45
46 /**
47  * This is the main builder class. It is given all the data to process the build (such as
48  * {@link ProductFlavor}s, {@link BuildType} and dependencies) and use them when doing specific
49  * build steps.
50  *
51  * To use:
52  * create a builder with {@link #AndroidBuilder(SdkParser, ILogger, boolean)},
53  * configure compile target with {@link #setTarget(String)}
54  * configure build variant with {@link #setBuildVariant(VariantConfiguration, VariantConfiguration)}
55  *
56  * then build steps can be done with
57  * {@link #generateBuildConfig(String, java.util.List)}
58  * {@link #processManifest(String)}
59  * {@link #processResources(String, String, String, String, String, AaptOptions)}
60  * {@link #convertBytecode(java.util.List, String, DexOptions)}
61  * {@link #packageApk(String, String, String, String)}
62  *
63  * Java compilation is not handled but the builder provides the runtime classpath with
64  * {@link #getRuntimeClasspath()}.
65  */
66 public class AndroidBuilder {
67
68     private final SdkParser mSdkParser;
69     private final ILogger mLogger;
70     private final CommandLineRunner mCmdLineRunner;
71     private final boolean mVerboseExec;
72
73     private IAndroidTarget mTarget;
74
75     // config for the main app.
76     private VariantConfiguration mVariant;
77     // config for the tested app.
78     private VariantConfiguration mTestedVariant;
79
80     /**
81      * Creates an AndroidBuilder
82      * <p/>
83      * This receives an {@link SdkParser} to provide the build with information about the SDK, as
84      * well as an {@link ILogger} to display output.
85      * <p/>
86      * <var>verboseExec</var> is needed on top of the ILogger due to remote exec tools not being
87      * able to output info and verbose messages separately.
88      *
89      * @param sdkParser
90      * @param logger
91      * @param verboseExec
92      */
93     public AndroidBuilder(@NonNull SdkParser sdkParser, ILogger logger, boolean verboseExec) {
94         mSdkParser = sdkParser;
95         mLogger = logger;
96         mVerboseExec = verboseExec;
97         mCmdLineRunner = new CommandLineRunner(mLogger);
98     }
99
100     @VisibleForTesting
101     AndroidBuilder(
102             @NonNull SdkParser sdkParser,
103             @NonNull ManifestParser manifestParser,
104             @NonNull CommandLineRunner cmdLineRunner,
105             @NonNull ILogger logger,
106             boolean verboseExec) {
107         mSdkParser = sdkParser;
108         mLogger = logger;
109         mVerboseExec = verboseExec;
110         mCmdLineRunner = cmdLineRunner;
111     }
112
113     /**
114      * Sets the compilation target hash string.
115      *
116      * @param target the compilation target
117      *
118      * @see IAndroidTarget#hashString()
119      */
120     public void setTarget(@NonNull String target) {
121         mTarget = mSdkParser.resolveTarget(target, mLogger);
122
123         if (mTarget == null) {
124             throw new RuntimeException("Unknown target: " + target);
125         }
126     }
127
128     /**
129      * Sets the build variant.
130      *
131      * @param variant the configuration of the variant
132      * @param testedVariant the configuration of the tested variant. only applicable if
133      *                      the main config is a test variant.
134      *
135      */
136     public void setBuildVariant(@NonNull VariantConfiguration variant,
137                                 @Nullable VariantConfiguration testedVariant) {
138         mVariant = variant;
139         mTestedVariant = testedVariant;
140     }
141
142     /**
143      * Returns the runtime classpath to be used during compilation.
144      */
145     public List<String> getRuntimeClasspath() {
146         if (mTarget == null) {
147             throw new IllegalArgumentException("Target not set.");
148         }
149
150         List<String> classpath = new ArrayList<String>();
151
152         classpath.add(mTarget.getPath(IAndroidTarget.ANDROID_JAR));
153
154         // add optional libraries if any
155         IOptionalLibrary[] libs = mTarget.getOptionalLibraries();
156         if (libs != null) {
157             for (IOptionalLibrary lib : libs) {
158                 classpath.add(lib.getJarPath());
159             }
160         }
161
162         // add annotations.jar if needed.
163         if (mTarget.getVersion().getApiLevel() <= 15) {
164             classpath.add(mSdkParser.getAnnotationsJar());
165         }
166
167         return classpath;
168     }
169
170     /**
171      * Generate the BuildConfig class for the project.
172      * @param sourceOutputDir directory where to put this. This is the source folder, not the
173      *                        package folder.
174      * @param additionalLines additional lines to put in the class. These must be valid Java lines.
175      * @throws IOException
176      */
177     public void generateBuildConfig(
178             @NonNull String sourceOutputDir,
179             @Nullable List<String> additionalLines) throws IOException {
180         if (mVariant == null) {
181             throw new IllegalArgumentException("No Variant Configuration has been set.");
182         }
183         if (mTarget == null) {
184             throw new IllegalArgumentException("Target not set.");
185         }
186
187         String packageName;
188         if (mVariant.getType() == VariantConfiguration.Type.TEST) {
189             packageName = mVariant.getPackageName(mTestedVariant);
190         } else {
191             packageName = mVariant.getPackageFromManifest();
192         }
193
194         BuildConfigGenerator generator = new BuildConfigGenerator(
195                 sourceOutputDir, packageName, mVariant.getBuildType().isDebuggable());
196         generator.generate(additionalLines);
197     }
198
199     /**
200      * Pre-process resources. This crunches images and process 9-patches before they can
201      * be packaged.
202      * This is incremental.
203      *
204      * @param resOutputDir where the processed resources are stored.
205      * @throws IOException
206      * @throws InterruptedException
207      */
208     public void preprocessResources(@NonNull String resOutputDir)
209             throws IOException, InterruptedException {
210         if (mVariant == null) {
211             throw new IllegalArgumentException("No Variant Configuration has been set.");
212         }
213         if (mTarget == null) {
214             throw new IllegalArgumentException("Target not set.");
215         }
216
217         // launch aapt: create the command line
218         ArrayList<String> command = new ArrayList<String>();
219
220         @SuppressWarnings("deprecation")
221         String aaptPath = mTarget.getPath(IAndroidTarget.AAPT);
222
223         command.add(aaptPath);
224         command.add("crunch");
225
226         if (mVerboseExec) {
227             command.add("-v");
228         }
229
230         boolean hasResToCrunch = false;
231
232         if (mVariant.getBuildTypeSourceSet() != null) {
233             File typeResLocation = mVariant.getBuildTypeSourceSet().getAndroidResources();
234             if (typeResLocation != null && typeResLocation.isDirectory()) {
235                 command.add("-S");
236                 command.add(typeResLocation.getAbsolutePath());
237                 hasResToCrunch = true;
238             }
239         }
240
241         for (SourceSet sourceSet : mVariant.getFlavorSourceSets()) {
242             File flavorResLocation = sourceSet.getAndroidResources();
243             if (flavorResLocation != null && flavorResLocation.isDirectory()) {
244                 command.add("-S");
245                 command.add(flavorResLocation.getAbsolutePath());
246                 hasResToCrunch = true;
247             }
248         }
249
250         File mainResLocation = mVariant.getDefaultSourceSet().getAndroidResources();
251         if (mainResLocation != null && mainResLocation.isDirectory()) {
252             command.add("-S");
253             command.add(mainResLocation.getAbsolutePath());
254             hasResToCrunch = true;
255         }
256
257         command.add("-C");
258         command.add(resOutputDir);
259
260         if (hasResToCrunch) {
261             mCmdLineRunner.runCmdLine(command);
262         }
263     }
264
265     /**
266      * Merges all the manifest from the BuildType and ProductFlavor(s) into a single manifest.
267      *
268      * TODO: figure out the order. Libraries first or buildtype/flavors first?
269      *
270      * @param outManifestLocation the output location for the merged manifest
271      */
272     public void processManifest(@NonNull String outManifestLocation) {
273         if (mVariant == null) {
274             throw new IllegalArgumentException("No Variant Configuration has been set.");
275         }
276         if (mTarget == null) {
277             throw new IllegalArgumentException("Target not set.");
278         }
279
280         if (mTestedVariant != null) {
281             generateTestManifest(outManifestLocation);
282         } else {
283             mergeManifest(outManifestLocation);
284         }
285     }
286
287     private void generateTestManifest(String outManifestLocation) {
288         TestManifestGenerator generator = new TestManifestGenerator(outManifestLocation,
289                 mVariant.getPackageName(mTestedVariant),
290                 mTestedVariant.getPackageName(null),
291                 mTestedVariant.getInstrumentationRunner());
292
293         try {
294             generator.generate();
295         } catch (IOException e) {
296             throw new RuntimeException(e);
297         }
298     }
299
300     private void mergeManifest(String outManifestLocation) {
301         try {
302             File mainLocation = mVariant.getDefaultSourceSet().getAndroidManifest();
303             File typeLocation = mVariant.getBuildTypeSourceSet().getAndroidManifest();
304             if (typeLocation != null && typeLocation.isDirectory() == false) {
305                 typeLocation = null;
306             }
307
308             List<File> flavorManifests = new ArrayList<File>();
309             for (SourceSet sourceSet : mVariant.getFlavorSourceSets()) {
310                 File f = sourceSet.getAndroidManifest();
311                 if (f != null && f.isFile()) {
312                     flavorManifests.add(f);
313                 }
314             }
315
316             // if no manifest to merge, just copy to location
317             if (typeLocation == null && flavorManifests.isEmpty() && !mVariant.hasLibraries()) {
318                 new FileOp().copyFile(mainLocation, new File(outManifestLocation));
319             } else {
320                 if (!mVariant.hasLibraries()) {
321
322                     File appMergeOut = new File(outManifestLocation);
323
324                     List<File> manifests = new ArrayList<File>();
325                     if (typeLocation != null) {
326                         manifests.add(typeLocation);
327                     }
328                     manifests.addAll(flavorManifests);
329
330                     ManifestMerger merger = new ManifestMerger(MergerLog.wrapSdkLog(mLogger));
331                     if (merger.process(
332                             appMergeOut,
333                             mainLocation,
334                             manifests.toArray(new File[manifests.size()])) == false) {
335                         throw new RuntimeException();
336                     }
337                 } else {
338
339                     File appMergeOut = File.createTempFile("manifestMerge", ".xml");
340                     appMergeOut.deleteOnExit();
341
342                     // recursively merge all manifests starting with the leaves and up toward the
343                     // root (the app)
344                     mergeLibraryManifests(appMergeOut, mVariant.getDirectLibraries(),
345                             new File(outManifestLocation));
346                     }
347             }
348         } catch (IOException e) {
349             throw new RuntimeException(e);
350         }
351     }
352
353     private void mergeLibraryManifests(
354             File mainManifest,
355             Iterable<AndroidDependency> directLibraries,
356             File outManifest) throws IOException {
357
358         List<File> manifests = new ArrayList<File>();
359         for (AndroidDependency library : directLibraries) {
360             List<AndroidDependency> subLibraries = library.getDependencies();
361             if (subLibraries == null || subLibraries.size() == 0) {
362                 manifests.add(new File(library.getManifest()));
363             } else {
364                 File mergeLibManifest = File.createTempFile("manifestMerge", ".xml");
365                 mergeLibManifest.deleteOnExit();
366
367                 mergeLibraryManifests(
368                         new File(library.getManifest()), subLibraries, mergeLibManifest);
369
370                 manifests.add(mergeLibManifest);
371             }
372         }
373
374         ManifestMerger merger = new ManifestMerger(MergerLog.wrapSdkLog(mLogger));
375         if (merger.process(
376                 outManifest,
377                 mainManifest,
378                 manifests.toArray(new File[manifests.size()])) == false) {
379             throw new RuntimeException();
380         }
381     }
382
383     public void processResources(
384             @NonNull String manifestFile,
385             @Nullable String preprocessResDir,
386             @Nullable String sourceOutputDir,
387             @Nullable String resPackageOutput,
388             @Nullable String proguardOutput,
389             @NonNull AaptOptions options) throws IOException, InterruptedException {
390         if (mVariant == null) {
391             throw new IllegalArgumentException("No Variant Configuration has been set.");
392         }
393         if (mTarget == null) {
394             throw new IllegalArgumentException("Target not set.");
395         }
396
397         // if both output types are empty, then there's nothing to do and this is an error
398         if (sourceOutputDir == null && resPackageOutput == null) {
399             throw new IllegalArgumentException("no output provided for aapt task");
400         }
401
402         // launch aapt: create the command line
403         ArrayList<String> command = new ArrayList<String>();
404
405         @SuppressWarnings("deprecation")
406         String aaptPath = mTarget.getPath(IAndroidTarget.AAPT);
407
408         command.add(aaptPath);
409         command.add("package");
410
411         if (mVerboseExec) {
412             command.add("-v");
413         }
414
415         command.add("-f");
416         command.add("--no-crunch");
417
418         // inputs
419         command.add("-I");
420         command.add(mTarget.getPath(IAndroidTarget.ANDROID_JAR));
421
422         command.add("-M");
423         command.add(manifestFile);
424
425         // TODO: handle libraries!
426         boolean useOverlay =  false;
427         if (preprocessResDir != null) {
428             File preprocessResFile = new File(preprocessResDir);
429             if (preprocessResFile.isDirectory()) {
430                 command.add("-S");
431                 command.add(preprocessResDir);
432                 useOverlay = true;
433             }
434         }
435
436         if (mVariant.getBuildTypeSourceSet() != null) {
437             File typeResLocation = mVariant.getBuildTypeSourceSet().getAndroidResources();
438             if (typeResLocation != null && typeResLocation.isDirectory()) {
439                 command.add("-S");
440                 command.add(typeResLocation.getAbsolutePath());
441                 useOverlay = true;
442             }
443         }
444
445         for (SourceSet sourceSet : mVariant.getFlavorSourceSets()) {
446             File flavorResLocation = sourceSet.getAndroidResources();
447             if (flavorResLocation != null && flavorResLocation.isDirectory()) {
448                 command.add("-S");
449                 command.add(flavorResLocation.getAbsolutePath());
450                 useOverlay = true;
451             }
452         }
453
454         File mainResLocation = mVariant.getDefaultSourceSet().getAndroidResources();
455         if (mainResLocation != null && mainResLocation.isDirectory()) {
456             command.add("-S");
457             command.add(mainResLocation.getAbsolutePath());
458         }
459
460         if (useOverlay) {
461             command.add("--auto-add-overlay");
462         }
463
464         // TODO support 2+ assets folders.
465 //        if (typeAssetsLocation != null) {
466 //            command.add("-A");
467 //            command.add(typeAssetsLocation);
468 //        }
469 //
470 //        if (flavorAssetsLocation != null) {
471 //            command.add("-A");
472 //            command.add(flavorAssetsLocation);
473 //        }
474
475         File mainAssetsLocation = mVariant.getDefaultSourceSet().getAndroidAssets();
476         if (mainAssetsLocation != null && mainAssetsLocation.isDirectory()) {
477             command.add("-A");
478             command.add(mainAssetsLocation.getAbsolutePath());
479         }
480
481         // outputs
482
483         if (sourceOutputDir != null) {
484             command.add("-m");
485             command.add("-J");
486             command.add(sourceOutputDir);
487         }
488
489         if (resPackageOutput != null) {
490             command.add("-F");
491             command.add(resPackageOutput);
492
493             if (proguardOutput != null) {
494                 command.add("-G");
495                 command.add(proguardOutput);
496             }
497         }
498
499         // options controlled by build variants
500
501         if (mVariant.getBuildType().isDebuggable()) {
502             command.add("--debug-mode");
503         }
504
505         if (mVariant.getType() == VariantConfiguration.Type.DEFAULT) {
506             String packageOverride = mVariant.getPackageOverride();
507             if (packageOverride != null) {
508                 command.add("--rename-manifest-package");
509                 command.add(packageOverride);
510                 mLogger.verbose("Inserting package '%s' in AndroidManifest.xml", packageOverride);
511             }
512
513             boolean forceErrorOnReplace = false;
514
515             ProductFlavor mergedFlavor = mVariant.getMergedFlavor();
516
517             int versionCode = mergedFlavor.getVersionCode();
518             if (versionCode != -1) {
519                 command.add("--version-code");
520                 command.add(Integer.toString(versionCode));
521                 mLogger.verbose("Inserting versionCode '%d' in AndroidManifest.xml", versionCode);
522                 forceErrorOnReplace = true;
523             }
524
525             String versionName = mergedFlavor.getVersionName();
526             if (versionName != null) {
527                 command.add("--version-name");
528                 command.add(versionName);
529                 mLogger.verbose("Inserting versionName '%s' in AndroidManifest.xml", versionName);
530                 forceErrorOnReplace = true;
531             }
532
533             int minSdkVersion = mergedFlavor.getMinSdkVersion();
534             if (minSdkVersion != -1) {
535                 command.add("--min-sdk-version");
536                 command.add(Integer.toString(minSdkVersion));
537                 mLogger.verbose("Inserting minSdkVersion '%d' in AndroidManifest.xml",
538                         minSdkVersion);
539                 forceErrorOnReplace = true;
540             }
541
542             int targetSdkVersion = mergedFlavor.getTargetSdkVersion();
543             if (targetSdkVersion != -1) {
544                 command.add("--target-sdk-version");
545                 command.add(Integer.toString(targetSdkVersion));
546                 mLogger.verbose("Inserting targetSdkVersion '%d' in AndroidManifest.xml",
547                         targetSdkVersion);
548                 forceErrorOnReplace = true;
549             }
550
551             if (forceErrorOnReplace) {
552                 // TODO: force aapt to fail if replace of versionCode/Name or min/targetSdkVersion fails
553                 // Need to add the options to aapt first.
554             }
555         }
556
557         // AAPT options
558         String ignoreAssets = options.getIgnoreAssets();
559         if (ignoreAssets != null) {
560             command.add("---ignore-assets");
561             command.add(ignoreAssets);
562         }
563
564         List<String> noCompressList = options.getNoCompress();
565         if (noCompressList != null) {
566             for (String noCompress : noCompressList) {
567                 command.add("-0");
568                 command.add(noCompress);
569             }
570         }
571
572         mLogger.verbose("aapt command: %s", command.toString());
573
574         mCmdLineRunner.runCmdLine(command);
575     }
576
577     public void convertBytecode(
578             @NonNull List<String> classesLocation,
579             @NonNull String outDexFile,
580             @NonNull DexOptions dexOptions) throws IOException, InterruptedException {
581         if (mVariant == null) {
582             throw new IllegalArgumentException("No Variant Configuration has been set.");
583         }
584         if (mTarget == null) {
585             throw new IllegalArgumentException("Target not set.");
586         }
587
588         // launch dx: create the command line
589         ArrayList<String> command = new ArrayList<String>();
590
591         @SuppressWarnings("deprecation")
592         String dxPath = mTarget.getPath(IAndroidTarget.DX);
593         command.add(dxPath);
594
595         command.add("--dex");
596
597         if (mVerboseExec) {
598             command.add("--verbose");
599         }
600
601         command.add("--output");
602         command.add(outDexFile);
603
604         // TODO: handle dependencies
605         // TODO: handle dex options
606
607         mLogger.verbose("Input: " + classesLocation);
608
609         command.addAll(classesLocation);
610
611         mCmdLineRunner.runCmdLine(command);
612     }
613
614     /**
615      * Packages the apk.
616      * @param androidResPkgLocation
617      * @param classesDexLocation
618      * @param jniLibsLocation
619      * @param outApkLocation
620      */
621     public void packageApk(
622             @NonNull String androidResPkgLocation,
623             @NonNull String classesDexLocation,
624             @Nullable String jniLibsLocation,
625             @NonNull String outApkLocation) throws DuplicateFileException {
626         if (mVariant == null) {
627             throw new IllegalArgumentException("No Variant Configuration has been set.");
628         }
629         if (mTarget == null) {
630             throw new IllegalArgumentException("Target not set.");
631         }
632
633         BuildType buildType = mVariant.getBuildType();
634
635         SigningInfo signingInfo = null;
636         try {
637             if (buildType.isDebugSigned()) {
638                 String storeLocation = DebugKeyHelper.defaultDebugKeyStoreLocation();
639                 File storeFile = new File(storeLocation);
640                 if (storeFile.isDirectory()) {
641                     throw new RuntimeException(
642                             String.format("A folder is in the way of the debug keystore: %s",
643                                     storeLocation));
644                 } else if (storeFile.exists() == false) {
645                     if (DebugKeyHelper.createNewStore(
646                             storeLocation, null /*storeType*/, mLogger) == false) {
647                         throw new RuntimeException();
648                     }
649                 }
650
651                 // load the key
652                 signingInfo = DebugKeyHelper.getDebugKey(storeLocation, null /*storeStype*/);
653             } else if (mVariant.getMergedFlavor().isSigningReady()) {
654                 ProductFlavor flavor = mVariant.getMergedFlavor();
655                 signingInfo = KeystoreHelper.getSigningInfo(
656                         flavor.getSigningStoreLocation(),
657                         flavor.getSigningStorePassword(),
658                         null, /*storeStype*/
659                         flavor.getSigningKeyAlias(),
660                         flavor.getSigningKeyPassword());
661             }
662         } catch (AndroidLocationException e) {
663             throw new RuntimeException(e);
664         } catch (KeytoolException e) {
665             throw new RuntimeException(e);
666         } catch (FileNotFoundException e) {
667             // this shouldn't happen as we have checked ahead of calling getDebugKey.
668             throw new RuntimeException(e);
669         }
670
671         try {
672             Packager packager = new Packager(
673                     outApkLocation, androidResPkgLocation, classesDexLocation,
674                     signingInfo, mLogger);
675
676             packager.setDebugJniMode(buildType.isDebugJniBuild());
677
678             // figure out conflicts!
679             JavaResourceProcessor resProcessor = new JavaResourceProcessor(packager);
680
681             if (mVariant.getBuildTypeSourceSet() != null) {
682                 Set<File> buildTypeJavaResLocations =
683                         mVariant.getBuildTypeSourceSet().getJavaResources();
684                 for (File buildTypeJavaResLocation : buildTypeJavaResLocations) {
685                     if (buildTypeJavaResLocation != null &&
686                             buildTypeJavaResLocation.isDirectory()) {
687                         resProcessor.addSourceFolder(buildTypeJavaResLocation.getAbsolutePath());
688                     }
689                 }
690             }
691
692             for (SourceSet sourceSet : mVariant.getFlavorSourceSets()) {
693
694                 Set<File> flavorJavaResLocations = sourceSet.getJavaResources();
695                 for (File flavorJavaResLocation : flavorJavaResLocations) {
696                     if (flavorJavaResLocation != null && flavorJavaResLocation.isDirectory()) {
697                         resProcessor.addSourceFolder(flavorJavaResLocation.getAbsolutePath());
698                     }
699                 }
700             }
701
702             Set<File> mainJavaResLocations = mVariant.getDefaultSourceSet().getJavaResources();
703             for (File mainJavaResLocation : mainJavaResLocations) {
704                 if (mainJavaResLocation != null && mainJavaResLocation.isDirectory()) {
705                     resProcessor.addSourceFolder(mainJavaResLocation.getAbsolutePath());
706                 }
707             }
708
709             // also add resources from library projects and jars
710             if (jniLibsLocation != null) {
711                 packager.addNativeLibraries(jniLibsLocation);
712             }
713
714             packager.sealApk();
715         } catch (PackagerException e) {
716             throw new RuntimeException(e);
717         } catch (SealedPackageException e) {
718             throw new RuntimeException(e);
719         }
720     }
721 }