blob: 9b9b87f308f6053a5e8d34ab4baf7ea459570d34 [file] [log] [blame]
// 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.mergeData(
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,
packageType,
options.packageForR,
options.primaryManifest,
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) {}
}
}