// Copyright 2015 The Bazel Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package com.google.devtools.build.android; import static java.util.stream.Collectors.toList; import com.android.builder.core.VariantType; import com.android.ide.common.internal.AaptCruncher; import com.android.ide.common.internal.LoggedErrorException; import com.android.ide.common.internal.PngCruncher; import com.android.ide.common.process.DefaultProcessExecutor; import com.android.ide.common.process.LoggedProcessOutputHandler; import com.android.ide.common.xml.AndroidManifestParser; import com.android.ide.common.xml.ManifestData.Instrumentation; import com.android.io.StreamException; import com.android.utils.StdLogger; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Stopwatch; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.devtools.build.android.AndroidDataMerger.MergeConflictException; import com.google.devtools.build.android.AndroidResourceMerger.MergingException; import com.google.devtools.build.android.AndroidResourceProcessor.AaptConfigOptions; import com.google.devtools.build.android.AndroidResourceProcessor.FlagAaptOptions; import com.google.devtools.build.android.Converters.DependencyAndroidDataListConverter; import com.google.devtools.build.android.Converters.PathConverter; import com.google.devtools.build.android.Converters.SerializedAndroidDataListConverter; import com.google.devtools.build.android.Converters.UnvalidatedAndroidDataConverter; import com.google.devtools.build.android.Converters.VariantTypeConverter; import com.google.devtools.build.android.SplitConfigurationFilter.UnrecognizedSplitsException; import com.google.devtools.common.options.Converters; import com.google.devtools.common.options.Converters.CommaSeparatedOptionListConverter; import com.google.devtools.common.options.Option; import com.google.devtools.common.options.OptionDocumentationCategory; import com.google.devtools.common.options.OptionEffectTag; import com.google.devtools.common.options.OptionsBase; import com.google.devtools.common.options.OptionsParser; import com.google.devtools.common.options.ShellQuotedParamsFilePreProcessor; import com.google.devtools.common.options.TriState; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; import javax.annotation.Nullable; import javax.xml.parsers.ParserConfigurationException; import org.xml.sax.SAXException; /** * Provides an entry point for the resource processing using the AOSP build tools. * *
 * Example Usage:
 *   java/com/google/build/android/AndroidResourceProcessingAction\
 *      --sdkRoot path/to/sdk\
 *      --aapt path/to/sdk/aapt\
 *      --adb path/to/sdk/adb\
 *      --zipAlign path/to/sdk/zipAlign\
 *      --androidJar path/to/sdk/androidJar\
 *      --manifestOutput path/to/manifest\
 *      --primaryData path/to/resources:path/to/assets:path/to/manifest\
 *      --data p/t/res1:p/t/assets1:p/t/1/AndroidManifest.xml:p/t/1/R.txt:symbols,\
 *             p/t/res2:p/t/assets2:p/t/2/AndroidManifest.xml:p/t/2/R.txt:symbols\
 *      --packagePath path/to/write/archive.ap_\
 *      --srcJarOutput path/to/write/archive.srcjar
 * 
*/ public class AndroidResourceProcessingAction { private static final StdLogger STD_LOGGER = new StdLogger(com.android.utils.StdLogger.Level.WARNING); private static final Logger logger = Logger.getLogger(AndroidResourceProcessingAction.class.getName()); /** Flag specifications for this action. */ public static final class Options extends OptionsBase { @Option( name = "primaryData", defaultValue = "null", converter = UnvalidatedAndroidDataConverter.class, category = "input", documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, effectTags = {OptionEffectTag.UNKNOWN}, help = "The directory containing the primary resource directory. The contents will override " + "the contents of any other resource directories during merging. The expected " + "format is " + UnvalidatedAndroidData.EXPECTED_FORMAT ) public UnvalidatedAndroidData primaryData; @Option( name = "data", defaultValue = "", converter = DependencyAndroidDataListConverter.class, category = "input", documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, effectTags = {OptionEffectTag.UNKNOWN}, help = "Transitive Data dependencies. These values will be used if not defined in the " + "primary resources. The expected format is " + DependencyAndroidData.EXPECTED_FORMAT + "[,...]" ) public List transitiveData; @Option( name = "directData", defaultValue = "", converter = DependencyAndroidDataListConverter.class, category = "input", documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, effectTags = {OptionEffectTag.UNKNOWN}, help = "Direct Data dependencies. These values will be used if not defined in the " + "primary resources. The expected format is " + DependencyAndroidData.EXPECTED_FORMAT + "[,...]" ) public List directData; @Option( name = "assets", defaultValue = "", converter = SerializedAndroidDataListConverter.class, category = "input", documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, effectTags = {OptionEffectTag.UNKNOWN}, help = "Transitive asset dependencies. These can also be specified together with resources" + " using --data. Expected format: " + SerializedAndroidData.EXPECTED_FORMAT + "[,...]" ) public List transitiveAssets; @Option( name = "directAssets", defaultValue = "", converter = SerializedAndroidDataListConverter.class, category = "input", documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, effectTags = {OptionEffectTag.UNKNOWN}, help = "Direct asset dependencies. These can also be specified together with resources using " + "--directData. Expected format: " + SerializedAndroidData.EXPECTED_FORMAT + "[,...]" ) public List directAssets; @Option( name = "rOutput", defaultValue = "null", converter = PathConverter.class, category = "output", documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, effectTags = {OptionEffectTag.UNKNOWN}, help = "Path to where the R.txt should be written." ) public Path rOutput; @Option( name = "symbolsOut", oldName = "symbolsTxtOut", defaultValue = "null", converter = PathConverter.class, category = "output", documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, effectTags = {OptionEffectTag.UNKNOWN}, help = "Path to where the symbols should be written." ) public Path symbolsOut; @Option( name = "dataBindingInfoOut", defaultValue = "null", converter = PathConverter.class, category = "output", documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, effectTags = {OptionEffectTag.UNKNOWN}, help = "Path to where data binding's layout info output should be written." ) public Path dataBindingInfoOut; @Option( name = "packagePath", defaultValue = "null", converter = PathConverter.class, category = "output", documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, effectTags = {OptionEffectTag.UNKNOWN}, help = "Path to the write the archive." ) public Path packagePath; @Option( name = "resourcesOutput", defaultValue = "null", converter = PathConverter.class, category = "output", documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, effectTags = {OptionEffectTag.UNKNOWN}, help = "Path to the write merged resources archive." ) public Path resourcesOutput; @Option( name = "proguardOutput", defaultValue = "null", converter = PathConverter.class, category = "output", documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, effectTags = {OptionEffectTag.UNKNOWN}, help = "Path for the proguard file." ) public Path proguardOutput; @Option( name = "mainDexProguardOutput", defaultValue = "null", converter = PathConverter.class, category = "output", documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, effectTags = {OptionEffectTag.UNKNOWN}, help = "Path for the main dex proguard file." ) public Path mainDexProguardOutput; @Option( name = "manifestOutput", defaultValue = "null", converter = PathConverter.class, category = "output", documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, effectTags = {OptionEffectTag.UNKNOWN}, help = "Path for the modified manifest." ) public Path manifestOutput; @Option( name = "srcJarOutput", defaultValue = "null", converter = PathConverter.class, category = "output", documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, effectTags = {OptionEffectTag.UNKNOWN}, help = "Path for the generated java source jar." ) public Path srcJarOutput; @Option( name = "packageType", defaultValue = "DEFAULT", converter = VariantTypeConverter.class, category = "config", documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, effectTags = {OptionEffectTag.UNKNOWN}, help = "Variant configuration type for packaging the resources." + " Acceptible values DEFAULT, LIBRARY, ANDROID_TEST, UNIT_TEST" ) public VariantType packageType; @Option( name = "densities", defaultValue = "", converter = CommaSeparatedOptionListConverter.class, category = "config", documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, effectTags = {OptionEffectTag.UNKNOWN}, help = "A list of densities to filter the resource drawables by." ) public List densities; @Option( name = "packageForR", defaultValue = "null", category = "config", documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, effectTags = {OptionEffectTag.UNKNOWN}, help = "Custom java package to generate the R symbols files." ) public String packageForR; @Option( name = "applicationId", defaultValue = "null", category = "config", documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, effectTags = {OptionEffectTag.UNKNOWN}, help = "Custom application id (package manifest) for the packaged manifest." ) public String applicationId; @Option( name = "versionName", defaultValue = "null", category = "config", documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, effectTags = {OptionEffectTag.UNKNOWN}, help = "Version name to stamp into the packaged manifest." ) public String versionName; @Option( name = "versionCode", defaultValue = "-1", category = "config", documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, effectTags = {OptionEffectTag.UNKNOWN}, help = "Version code to stamp into the packaged manifest." ) public int versionCode; @Option( name = "prefilteredResources", defaultValue = "", converter = Converters.CommaSeparatedOptionListConverter.class, category = "config", documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, effectTags = {OptionEffectTag.UNKNOWN}, help = "A list of resources that were filtered out in analysis." ) public List prefilteredResources; @Option( name = "throwOnResourceConflict", defaultValue = "false", category = "config", documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, effectTags = {OptionEffectTag.UNKNOWN}, help = "If passed, resource merge conflicts will be treated as errors instead of warnings" ) public boolean throwOnResourceConflict; @Option( name = "packageUnderTest", defaultValue = "null", category = "config", documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, effectTags = {OptionEffectTag.UNKNOWN}, help = "When building a test APK, the package of the binary being tested. Android resources can" + " only be provided if there is no package under test or if the test instrumentation" + " is in a different package." ) public String packageUnderTest; @Option( name = "isTestWithResources", defaultValue = "false", category = "config", documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, effectTags = {OptionEffectTag.UNKNOWN}, help = "Indicates that these resources are being processed for a test APK. Tests can only have" + "resources if they are not instrumented or they instrument only themselves." ) public boolean isTestWithResources; } private static AaptConfigOptions aaptConfigOptions; private static Options options; public static void main(String[] args) throws Exception { final Stopwatch timer = Stopwatch.createStarted(); OptionsParser optionsParser = OptionsParser.newOptionsParser(Options.class, AaptConfigOptions.class); optionsParser.enableParamsFileSupport( new ShellQuotedParamsFilePreProcessor(FileSystems.getDefault())); optionsParser.parseAndExitUponError(args); aaptConfigOptions = optionsParser.getOptions(AaptConfigOptions.class); options = optionsParser.getOptions(Options.class); final AndroidResourceProcessor resourceProcessor = new AndroidResourceProcessor(STD_LOGGER); try (ScopedTemporaryDirectory scopedTmp = new ScopedTemporaryDirectory("android_resources_tmp")) { final Path tmp = scopedTmp.getPath(); final Path mergedAssets = tmp.resolve("merged_assets"); final Path mergedResources = tmp.resolve("merged_resources"); final Path filteredResources = tmp.resolve("resources-filtered"); final Path densityManifest = tmp.resolve("manifest-filtered/AndroidManifest.xml"); final Path processedManifest = tmp.resolve("manifest-processed/AndroidManifest.xml"); final Path dummyManifest = tmp.resolve("manifest-aapt-dummy/AndroidManifest.xml"); final Path publicXmlOut = tmp.resolve("public-resources/public.xml"); Path generatedSources = null; if (options.srcJarOutput != null || options.rOutput != null || options.symbolsOut != null) { generatedSources = tmp.resolve("generated_resources"); } logger.fine(String.format("Setup finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS))); List resourceData = ImmutableSet.builder() .addAll(options.directData) .addAll(options.transitiveData) .build() .asList(); final MergedAndroidData mergedData = AndroidResourceMerger.mergeData( options.primaryData, ImmutableList.builder() .addAll(options.directData) .addAll(options.directAssets) .build(), ImmutableList.builder() .addAll(options.transitiveData) .addAll(options.transitiveAssets) .build(), mergedResources, mergedAssets, selectPngCruncher(), options.packageType, options.symbolsOut, options.prefilteredResources, options.throwOnResourceConflict); logger.fine(String.format("Merging finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS))); final DensityFilteredAndroidData filteredData = mergedData.filter( // Even if filtering was done in analysis, we still need to filter by density again // in execution since Fileset contents are not available in analysis. new DensitySpecificResourceFilter( options.densities, filteredResources, mergedResources), new DensitySpecificManifestProcessor(options.densities, densityManifest)); logger.fine( String.format( "Density filtering finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS))); MergedAndroidData processedData = AndroidManifestProcessor.with(STD_LOGGER) .processManifest( options.packageType, options.packageForR, options.applicationId, options.versionCode, options.versionName, filteredData, processedManifest); // Write manifestOutput now before the dummy manifest is created. if (options.manifestOutput != null) { AndroidResourceOutputs.copyManifestToOutput(processedData, options.manifestOutput); } if (options.packageType == VariantType.LIBRARY) { AndroidResourceProcessor.writeDummyManifestForAapt(dummyManifest, options.packageForR); processedData = new MergedAndroidData( processedData.getResourceDir(), processedData.getAssetDir(), dummyManifest); } if (hasConflictWithPackageUnderTest( options.packageUnderTest, processedData.getManifest(), timer)) { logger.log( Level.SEVERE, "Android resources cannot be provided if the instrumentation package is the same as " + "the package under test, but the instrumentation package (in the manifest) and " + "the package under test both had the same package: " + options.packageUnderTest); System.exit(1); } MergedAndroidData processedAndroidData = resourceProcessor.processResources( tmp, aaptConfigOptions.aapt, aaptConfigOptions.androidJar, aaptConfigOptions.buildToolsVersion, options.packageType, aaptConfigOptions.debug, options.packageForR, new FlagAaptOptions(aaptConfigOptions), aaptConfigOptions.resourceConfigs, aaptConfigOptions.splits, processedData, resourceData, generatedSources, options.packagePath, options.proguardOutput, options.mainDexProguardOutput, options.resourcesOutput != null ? publicXmlOut : null, options.dataBindingInfoOut); logger.fine(String.format("aapt finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS))); if (options.srcJarOutput != null) { AndroidResourceOutputs.createSrcJar( generatedSources, options.srcJarOutput, VariantType.LIBRARY == options.packageType); } if (options.rOutput != null) { AndroidResourceOutputs.copyRToOutput( generatedSources, options.rOutput, VariantType.LIBRARY == options.packageType); } if (options.resourcesOutput != null) { if (Files.exists(publicXmlOut)) { try (BufferedReader reader = Files.newBufferedReader(publicXmlOut, StandardCharsets.UTF_8)) { Path publicXml = processedAndroidData.getResourceDir().resolve("values").resolve("public.xml"); Files.createDirectories(publicXml.getParent()); Pattern xmlComment = Pattern.compile(""); Files.write( publicXml, // Remove aapt debugging comment lines to fix hermaticity with generated files. reader.lines().filter(l -> !xmlComment.matcher(l).find()).collect(toList()), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); } } ResourcesZip.from(processedAndroidData.getResourceDir(), processedAndroidData.getAssetDir()) .writeTo(options.resourcesOutput, /* compress= */ false); } logger.fine( String.format("Packaging finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS))); } catch (MergeConflictException e) { logger.severe(e.getMessage()); System.exit(1); } catch (MergingException e) { logger.log(java.util.logging.Level.SEVERE, "Error during merging resources", e); throw e; } catch (IOException | InterruptedException | LoggedErrorException | UnrecognizedSplitsException e) { logger.log(java.util.logging.Level.SEVERE, "Error during processing resources", e); throw e; } catch (AndroidManifestProcessor.ManifestProcessingException e) { System.exit(1); } catch (Exception e) { logger.log(java.util.logging.Level.SEVERE, "Unexpected", e); throw e; } finally { resourceProcessor.shutdown(); } logger.fine(String.format("Resources processed in %sms", timer.elapsed(TimeUnit.MILLISECONDS))); } /** * Checks if there is a conflict between the package under test and the package being built. * *

When testing Android code, the test can be run in the same or a different process as the * code being tested. If it's in the same process, we do not allow Android resources to be used by * the test, as they could overwrite the resources used by the code being tested. If this APK * won't be testing another APK, the test and code under test are in different processes, or no * resources are being used, this isn't a concern. * *

To determine whether the test and code under test are run in the same process, we check the * package of the code under test, passed into this function, against the target packages of any * instrumentation tags in this APK's manifest. * * @param packageUnderTest the package of the code under test, or null if no code is under test * @param processedManifest the processed manifest for this APK * @return true if there is a conflict, false otherwise */ @VisibleForTesting static boolean hasConflictWithPackageUnderTest( @Nullable String packageUnderTest, Path processedManifest, Stopwatch timer) throws SAXException, StreamException, ParserConfigurationException, IOException { if (packageUnderTest == null) { return false; } // We are building a test APK with resources. Validate instrumentation package is different // from the package under test. If it isn't, fail to prevent the test resources from // overriding the resources of the APK under test. try (InputStream stream = Files.newInputStream(processedManifest)) { for (Instrumentation instrumentation : AndroidManifestParser.parse(stream).getInstrumentations()) { if (packageUnderTest.equals(instrumentation.getTargetPackage())) { return true; } } } logger.fine( String.format( "Custom package and instrumentation verification finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS))); return false; } private static boolean usePngCruncher() { // If the value was set, use that. if (aaptConfigOptions.useAaptCruncher != TriState.AUTO) { return aaptConfigOptions.useAaptCruncher == TriState.YES; } // By default png cruncher shouldn't be invoked on a library -- the work is just thrown away. return options.packageType != VariantType.LIBRARY; } private static PngCruncher selectPngCruncher() { // Use the full cruncher if asked to do so. if (usePngCruncher()) { return new AaptCruncher( aaptConfigOptions.aapt.toString(), new DefaultProcessExecutor(STD_LOGGER), new LoggedProcessOutputHandler(STD_LOGGER)); } // Otherwise, if this is a binary, we need to at least process nine-patch PNGs. if (options.packageType != VariantType.LIBRARY) { return new NinePatchOnlyCruncher( aaptConfigOptions.aapt.toString(), new DefaultProcessExecutor(STD_LOGGER), new LoggedProcessOutputHandler(STD_LOGGER)); } return null; } }