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