| // Copyright 2016 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 com.android.builder.core.VariantConfiguration; | |
| import com.android.builder.core.VariantType; | |
| import com.android.ide.common.internal.PngCruncher; | |
| import com.android.ide.common.internal.PngException; | |
| import com.android.utils.StdLogger; | |
| import com.google.common.base.Preconditions; | |
| import com.google.common.base.Stopwatch; | |
| import com.google.common.base.Strings; | |
| import com.google.common.io.Files; | |
| 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.Converters.ExistingPathConverter; | |
| import com.google.devtools.build.android.Converters.PathConverter; | |
| import com.google.devtools.build.android.Converters.SerializedAndroidDataConverter; | |
| import com.google.devtools.build.android.Converters.SerializedAndroidDataListConverter; | |
| 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 java.io.File; | |
| import java.io.IOException; | |
| import java.nio.file.FileSystems; | |
| import java.nio.file.Path; | |
| import java.util.List; | |
| import java.util.concurrent.TimeUnit; | |
| import java.util.logging.Level; | |
| import java.util.logging.Logger; | |
| /** | |
| * Provides an entry point for the resource merging action. After merging, this action generates the | |
| * R.class files required to compile the rest of the java sources. | |
| * | |
| * <p>This action only generates the class jar. The R source jar is generated by AAPT at a later | |
| * time and off of the critical path, by {@link AndroidResourceValidatorAction}. That way, the | |
| * source will contain javadocs derived from comments in the .xml files. Ideally users wouldn't use | |
| * the javadoc, but instead generate documentation directly from the source .xml files. | |
| */ | |
| public class AndroidResourceMergingAction { | |
| private static final StdLogger stdLogger = new StdLogger(StdLogger.Level.WARNING); | |
| private static final Logger logger = | |
| Logger.getLogger(AndroidResourceMergingAction.class.getName()); | |
| /** Flag specifications for this action. */ | |
| public static final class Options extends OptionsBase { | |
| @Option( | |
| name = "primaryData", | |
| defaultValue = "null", | |
| converter = SerializedAndroidDataConverter.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 " | |
| + SerializedAndroidData.EXPECTED_FORMAT | |
| ) | |
| public SerializedAndroidData primaryData; | |
| @Option( | |
| name = "primaryManifest", | |
| defaultValue = "null", | |
| converter = ExistingPathConverter.class, | |
| category = "input", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.UNKNOWN}, | |
| help = "Path to primary resource's manifest file." | |
| ) | |
| public Path primaryManifest; | |
| @Option( | |
| name = "data", | |
| defaultValue = "", | |
| converter = SerializedAndroidDataListConverter.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 " | |
| + SerializedAndroidData.EXPECTED_FORMAT | |
| + "[&...]" | |
| ) | |
| public List<SerializedAndroidData> transitiveData; | |
| @Option( | |
| name = "directData", | |
| defaultValue = "", | |
| converter = SerializedAndroidDataListConverter.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 " | |
| + SerializedAndroidData.EXPECTED_FORMAT | |
| + "[&...]" | |
| ) | |
| public List<SerializedAndroidData> directData; | |
| @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 = "classJarOutput", | |
| defaultValue = "null", | |
| converter = PathConverter.class, | |
| category = "output", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.UNKNOWN}, | |
| help = "Path for the generated java class jar." | |
| ) | |
| public Path classJarOutput; | |
| @Option( | |
| name = "manifestOutput", | |
| defaultValue = "null", | |
| converter = PathConverter.class, | |
| category = "output", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.UNKNOWN}, | |
| help = "Path for the output processed AndroidManifest.xml." | |
| ) | |
| public Path manifestOutput; | |
| @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 = "symbolsBinOut", | |
| defaultValue = "null", | |
| converter = PathConverter.class, | |
| category = "config", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.UNKNOWN}, | |
| help = "Path to write the merged symbols binary." | |
| ) | |
| public Path symbolsBinOut; | |
| @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 = "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 = "targetLabel", | |
| defaultValue = "null", | |
| category = "input", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.UNKNOWN}, | |
| help = "A label to add to the output jar's manifest as 'Target-Label'" | |
| ) | |
| public String targetLabel; | |
| @Option( | |
| name = "injectingRuleKind", | |
| defaultValue = "null", | |
| category = "input", | |
| documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, | |
| effectTags = {OptionEffectTag.UNKNOWN}, | |
| help = "A string to add to the output jar's manifest as 'Injecting-Rule-Kind'" | |
| ) | |
| public String injectingRuleKind; | |
| } | |
| 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 aaptConfigOptions = optionsParser.getOptions(AaptConfigOptions.class); | |
| Options options = optionsParser.getOptions(Options.class); | |
| Preconditions.checkNotNull(options.primaryData); | |
| Preconditions.checkNotNull(options.primaryManifest); | |
| try (ScopedTemporaryDirectory scopedTmp = | |
| new ScopedTemporaryDirectory("android_resource_merge_tmp"); | |
| ExecutorServiceCloser executorService = ExecutorServiceCloser.createWithFixedPoolOf(15)) { | |
| Path tmp = scopedTmp.getPath(); | |
| Path mergedAssets = tmp.resolve("merged_assets"); | |
| Path mergedResources = tmp.resolve("merged_resources"); | |
| Path generatedSources = tmp.resolve("generated_resources"); | |
| Path processedManifest = tmp.resolve("manifest-processed/AndroidManifest.xml"); | |
| logger.fine(String.format("Setup finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS))); | |
| VariantType packageType = VariantType.LIBRARY; | |
| String packageForR = options.packageForR; | |
| if (packageForR == null) { | |
| packageForR = | |
| Strings.nullToEmpty( | |
| VariantConfiguration.getManifestPackage(options.primaryManifest.toFile())); | |
| } | |
| AndroidResourceClassWriter resourceClassWriter = | |
| AndroidResourceClassWriter.createWith( | |
| aaptConfigOptions.androidJar, generatedSources, packageForR); | |
| resourceClassWriter.setIncludeClassFile(true); | |
| resourceClassWriter.setIncludeJavaFile(false); | |
| final MergedAndroidData mergedData = | |
| AndroidResourceMerger.mergeDataAndWrite( | |
| options.primaryData, | |
| options.primaryManifest, | |
| options.directData, | |
| options.transitiveData, | |
| mergedResources, | |
| mergedAssets, | |
| new StubPngCruncher(), | |
| packageType, | |
| options.symbolsBinOut, | |
| resourceClassWriter, | |
| options.throwOnResourceConflict, | |
| executorService); | |
| logger.fine(String.format("Merging finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS))); | |
| // Until enough users with manifest placeholders migrate to the new manifest merger, | |
| // we need to replace ${applicationId} and ${packageName} with options.packageForR to make | |
| // the manifests compatible with the old manifest merger. | |
| if (options.manifestOutput != null) { | |
| MergedAndroidData processedData = | |
| AndroidManifestProcessor.with(stdLogger) | |
| .processManifest( | |
| packageType, | |
| options.packageForR, | |
| /* applicationId= */ null, | |
| /* versionCode= */ -1, | |
| /* versionName= */ null, | |
| mergedData, | |
| processedManifest); | |
| AndroidResourceOutputs.copyManifestToOutput(processedData, options.manifestOutput); | |
| } | |
| if (options.classJarOutput != null) { | |
| AndroidResourceOutputs.createClassJar( | |
| generatedSources, | |
| options.classJarOutput, | |
| options.targetLabel, | |
| options.injectingRuleKind); | |
| logger.fine( | |
| String.format( | |
| "Create classJar finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS))); | |
| } | |
| if (options.resourcesOutput != null) { | |
| Path resourcesDir = | |
| AndroidResourceProcessor.processDataBindings( | |
| tmp.resolve("res_no_binding"), | |
| mergedData.getResourceDir(), | |
| options.dataBindingInfoOut, | |
| options.packageForR, | |
| true); | |
| // For now, try compressing the library resources that we pass to the validator. This takes | |
| // extra CPU resources to pack and unpack (~2x), but can reduce the zip size (~4x). | |
| ResourcesZip.from(resourcesDir, mergedData.getAssetDir()) | |
| .writeTo(options.resourcesOutput, /* compress= */ true); | |
| logger.fine( | |
| String.format( | |
| "Create resources.zip finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS))); | |
| } | |
| } catch (MergeConflictException e) { | |
| logger.log(Level.SEVERE, e.getMessage()); | |
| System.exit(1); | |
| } catch (MergingException e) { | |
| logger.log(Level.SEVERE, "Error during merging resources", e); | |
| throw e; | |
| } catch (AndroidManifestProcessor.ManifestProcessingException e) { | |
| System.exit(1); | |
| } catch (Exception e) { | |
| logger.log(Level.SEVERE, "Unexpected", e); | |
| throw e; | |
| } | |
| logger.fine(String.format("Resources merged in %sms", timer.elapsed(TimeUnit.MILLISECONDS))); | |
| } | |
| /** | |
| * The merged {@link Options#resourcesOutput} is only used for validation and not for running | |
| * (unlike the final APK), so the image files do not need to be the true image files. We only need | |
| * the filenames to be the same. | |
| * | |
| * <p>Thus, we only create empty files for PNGs (convenient with a custom PngCruncher object). | |
| * This does miss out on other image files like .webp. | |
| */ | |
| private static final class StubPngCruncher implements PngCruncher { | |
| @Override | |
| public void crunchPng(int key, File from, File to) throws PngException { | |
| try { | |
| Files.touch(to); | |
| } catch (IOException e) { | |
| throw new PngException(e); | |
| } | |
| } | |
| @Override | |
| public int start() { | |
| return 0; | |
| } | |
| @Override | |
| public void end(int key) {} | |
| } | |
| } |