diff options
21 files changed, 898 insertions, 131 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidResourceMergingActionBuilder.java b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidResourceMergingActionBuilder.java new file mode 100644 index 0000000000..8a5310331b --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidResourceMergingActionBuilder.java @@ -0,0 +1,188 @@ +// 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.lib.rules.android; + +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.devtools.build.lib.actions.Artifact; +import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode; +import com.google.devtools.build.lib.analysis.RuleContext; +import com.google.devtools.build.lib.analysis.actions.ActionConstructionContext; +import com.google.devtools.build.lib.analysis.actions.CustomCommandLine; +import com.google.devtools.build.lib.analysis.actions.SpawnAction; +import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder; +import com.google.devtools.build.lib.rules.android.AndroidResourcesProvider.ResourceContainer; +import com.google.devtools.build.lib.rules.android.AndroidResourcesProvider.ResourceType; +import com.google.devtools.build.lib.rules.android.ResourceContainerConverter.Builder.SeparatorType; +import java.util.ArrayList; +import java.util.List; + +/** + * Builder for creating $android_resource_merger action. The action merges resources and generates + * the merged R classes for an android_library to hand off to java compilation of the library + * sources. It also generates a merged resources zip file to pass on to the + * $android_resource_validator action. For android_binary, see {@link + * AndroidResourcesProcessorBuilder}. + */ +class AndroidResourceMergingActionBuilder { + + private static final ResourceContainerConverter.ToArtifacts RESOURCE_CONTAINER_TO_ARTIFACTS = + ResourceContainerConverter.builder() + .includeResourceRoots() + .includeSymbolsBin() + .toArtifactConverter(); + private static final ResourceContainerConverter.ToArg RESOURCE_CONTAINER_TO_ARG = + ResourceContainerConverter.builder() + .includeResourceRoots() + .includeLabel() + .includeSymbolsBin() + .withSeparator(SeparatorType.SEMICOLON_AMPERSAND) + .toArgConverter(); + + private final RuleContext ruleContext; + private final AndroidSdkProvider sdk; + + // Inputs + private ResourceContainer primary; + private ResourceDependencies dependencies; + + // Outputs + private Artifact mergedResourcesOut; + private Artifact classJarOut; + private Artifact manifestOut; + + // Flags + private String customJavaPackage; + + /** @param ruleContext The RuleContext that was used to create the SpawnAction.Builder. */ + public AndroidResourceMergingActionBuilder(RuleContext ruleContext) { + this.ruleContext = ruleContext; + this.sdk = AndroidSdkProvider.fromRuleContext(ruleContext); + } + + /** + * The primary resource for merging. This resource will overwrite any resource or data value in + * the transitive closure. + */ + public AndroidResourceMergingActionBuilder withPrimary(ResourceContainer primary) { + this.primary = primary; + return this; + } + + public AndroidResourceMergingActionBuilder withDependencies(ResourceDependencies resourceDeps) { + this.dependencies = resourceDeps; + return this; + } + + public AndroidResourceMergingActionBuilder setMergedResourcesOut(Artifact mergedResourcesOut) { + this.mergedResourcesOut = mergedResourcesOut; + return this; + } + + public AndroidResourceMergingActionBuilder setClassJarOut(Artifact classJarOut) { + this.classJarOut = classJarOut; + return this; + } + + public AndroidResourceMergingActionBuilder setManifestOut(Artifact manifestOut) { + this.manifestOut = manifestOut; + return this; + } + + public AndroidResourceMergingActionBuilder setJavaPackage(String customJavaPackage) { + this.customJavaPackage = customJavaPackage; + return this; + } + + public ResourceContainer build(ActionConstructionContext context) { + CustomCommandLine.Builder builder = new CustomCommandLine.Builder(); + + // Use a FluentIterable to avoid flattening the NestedSets + NestedSetBuilder<Artifact> inputs = NestedSetBuilder.naiveLinkOrder(); + inputs.addAll( + ruleContext + .getExecutablePrerequisite("$android_resource_merger", Mode.HOST) + .getRunfilesSupport() + .getRunfilesArtifactsWithoutMiddlemen()); + + builder.addExecPath("--androidJar", sdk.getAndroidJar()); + inputs.add(sdk.getAndroidJar()); + + Preconditions.checkNotNull(primary); + builder.add("--primaryData").add(RESOURCE_CONTAINER_TO_ARG.apply(primary)); + inputs.addTransitive(RESOURCE_CONTAINER_TO_ARTIFACTS.apply(primary)); + + Preconditions.checkNotNull(primary.getManifest()); + builder.addExecPath("--primaryManifest", primary.getManifest()); + inputs.add(primary.getManifest()); + + ResourceContainerConverter.convertDependencies( + dependencies, builder, inputs, RESOURCE_CONTAINER_TO_ARG, RESOURCE_CONTAINER_TO_ARTIFACTS); + + Preconditions.checkNotNull(classJarOut); + List<Artifact> outs = new ArrayList<>(); + builder.addExecPath("--classJarOutput", classJarOut); + outs.add(classJarOut); + + if (mergedResourcesOut != null) { + builder.addExecPath("--resourcesOutput", mergedResourcesOut); + outs.add(mergedResourcesOut); + } + + // For now, do manifest processing to remove placeholders that aren't handled by the legacy + // manifest merger. Remove this once enough users migrate over to the new manifest merger. + if (manifestOut != null) { + builder.addExecPath("--manifestOutput", manifestOut); + outs.add(manifestOut); + } + + if (!Strings.isNullOrEmpty(customJavaPackage)) { + // Sets an alternative java package for the generated R.java + // this allows android rules to generate resources outside of the java{,tests} tree. + builder.add("--packageForR").add(customJavaPackage); + } + + SpawnAction.Builder spawnActionBuilder = new SpawnAction.Builder(); + // Create the spawn action. + ruleContext.registerAction( + spawnActionBuilder + .addTransitiveInputs(inputs.build()) + .addOutputs(ImmutableList.copyOf(outs)) + .setCommandLine(builder.build()) + .setExecutable( + ruleContext.getExecutablePrerequisite("$android_resource_merger", Mode.HOST)) + .setProgressMessage("Merging Android resources for " + ruleContext.getLabel()) + .setMnemonic("AndroidResourceMerger") + .build(context)); + + // Return the full set of processed transitive dependencies. + // TODO(jvoung): pass the classJar out -- once that is a field of ResourceContainer. + return new ResourceContainer( + primary.getLabel(), + primary.getJavaPackage(), + primary.getRenameManifestPackage(), + primary.getConstantsInlined(), + primary.getApk(), + manifestOut != null ? manifestOut : primary.getManifest(), + primary.getJavaSourceJar(), + primary.getArtifacts(ResourceType.ASSETS), + primary.getArtifacts(ResourceType.RESOURCES), + primary.getRoots(ResourceType.ASSETS), + primary.getRoots(ResourceType.RESOURCES), + primary.isManifestExported(), + primary.getRTxt(), + primary.getSymbolsTxt()); + } +} diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidResourcesProcessorBuilder.java b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidResourcesProcessorBuilder.java index 3c2b915920..789c3dcacf 100644 --- a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidResourcesProcessorBuilder.java +++ b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidResourcesProcessorBuilder.java @@ -24,6 +24,7 @@ import com.google.devtools.build.lib.analysis.actions.SpawnAction; import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder; import com.google.devtools.build.lib.rules.android.AndroidResourcesProvider.ResourceContainer; import com.google.devtools.build.lib.rules.android.AndroidResourcesProvider.ResourceType; +import com.google.devtools.build.lib.rules.android.ResourceContainerConverter.Builder.SeparatorType; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -50,6 +51,7 @@ public class AndroidResourcesProcessorBuilder { ResourceContainerConverter.builder() .includeResourceRoots() .includeManifest() + .withSeparator(SeparatorType.COLON_COMMA) .toArgConverter(); private static final ResourceContainerConverter.ToArg RESOURCE_DEP_TO_ARG = @@ -58,6 +60,7 @@ public class AndroidResourcesProcessorBuilder { .includeManifest() .includeRTxt() .includeSymbolsBin() + .withSeparator(SeparatorType.COLON_COMMA) .toArgConverter(); private ResourceContainer primary; diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidRuleClasses.java b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidRuleClasses.java index dc1312662e..6e14804023 100644 --- a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidRuleClasses.java +++ b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidRuleClasses.java @@ -169,6 +169,7 @@ public final class AndroidRuleClasses { public static final String DEFAULT_MANIFEST_MERGER = "//tools/android:manifest_merger"; public static final String DEFAULT_RCLASS_GENERATOR = "//tools/android:rclass_generator"; public static final String DEFAULT_RESOURCES_PROCESSOR = "//tools/android:resources_processor"; + public static final String DEFAULT_RESOURCE_MERGER = "//tools/android:resource_merger"; public static final String DEFAULT_RESOURCE_PARSER = "//tools/android:resource_parser"; public static final String DEFAULT_RESOURCE_SHRINKER = "//tools/android:resource_shrinker"; public static final String DEFAULT_SDK = "//tools/android:sdk"; @@ -408,6 +409,8 @@ public final class AndroidRuleClasses { env.getToolsLabel(DEFAULT_RCLASS_GENERATOR))) .add(attr("$android_resources_processor", LABEL).cfg(HOST).exec().value( env.getToolsLabel(DEFAULT_RESOURCES_PROCESSOR))) + .add(attr("$android_resource_merger", LABEL).cfg(HOST).exec().value( + env.getToolsLabel(DEFAULT_RESOURCE_MERGER))) .add(attr("$android_resource_parser", LABEL).cfg(HOST).exec().value( env.getToolsLabel(DEFAULT_RESOURCE_PARSER))) .add(attr("$android_resource_shrinker", LABEL).cfg(HOST).exec().value( diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/ResourceContainerConverter.java b/src/main/java/com/google/devtools/build/lib/rules/android/ResourceContainerConverter.java index b271e90861..d86ada91c5 100644 --- a/src/main/java/com/google/devtools/build/lib/rules/android/ResourceContainerConverter.java +++ b/src/main/java/com/google/devtools/build/lib/rules/android/ResourceContainerConverter.java @@ -17,6 +17,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; import com.google.common.base.Functions; import com.google.common.base.Joiner; +import com.google.common.base.Preconditions; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; @@ -42,24 +43,44 @@ public class ResourceContainerConverter { return new Builder(); } - interface ToArg extends Function<ResourceContainer, String> {} + interface ToArg extends Function<ResourceContainer, String> { - interface ToArtifacts extends Function<ResourceContainer, NestedSet<Artifact>> {} + String listSeparator(); + } + + interface ToArtifacts extends Function<ResourceContainer, NestedSet<Artifact>> { + + } static class Builder { private boolean includeResourceRoots; + private boolean includeLabel; private boolean includeManifest; private boolean includeRTxt; private boolean includeSymbolsBin; + private SeparatorType separatorType; + private Joiner argJoiner; + private Function<String, String> escaper = Functions.identity(); + + enum SeparatorType { + COLON_COMMA, + SEMICOLON_AMPERSAND + } - Builder() {} + Builder() { + } Builder includeResourceRoots() { includeResourceRoots = true; return this; } + Builder includeLabel() { + includeLabel = true; + return this; + } + Builder includeManifest() { includeManifest = true; return this; @@ -75,9 +96,35 @@ public class ResourceContainerConverter { return this; } - private static final Joiner ARG_JOINER = Joiner.on(":"); + Builder withSeparator(SeparatorType type) { + separatorType = type; + return this; + } ToArg toArgConverter() { + switch (separatorType) { + case COLON_COMMA: + argJoiner = Joiner.on(":"); + // We currently use ":" to separate components of an argument and "," to separate + // arguments in a list of arguments. Those characters require escaping if used in a label + // (part of the set of allowed characters in a label). + if (includeLabel) { + escaper = new Function<String, String>() { + @Override + public String apply(String input) { + return input.replace(":", "\\:").replace(",", "\\,"); + } + }; + } + break; + case SEMICOLON_AMPERSAND: + argJoiner = Joiner.on(";"); + break; + default: + Preconditions.checkState(false, "Unknown separator type " + separatorType); + break; + } + return new ToArg() { @Override public String apply(ResourceContainer container) { @@ -86,6 +133,9 @@ public class ResourceContainerConverter { cmdPieces.add(convertRoots(container, ResourceType.RESOURCES)); cmdPieces.add(convertRoots(container, ResourceType.ASSETS)); } + if (includeLabel) { + cmdPieces.add(escaper.apply(container.getLabel().toString())); + } if (includeManifest) { cmdPieces.add(container.getManifest().getExecPathString()); } @@ -99,7 +149,20 @@ public class ResourceContainerConverter { ? "" : container.getSymbolsTxt().getExecPathString()); } - return ARG_JOINER.join(cmdPieces.build()); + return argJoiner.join(cmdPieces.build()); + } + + @Override + public String listSeparator() { + switch (separatorType) { + case COLON_COMMA: + return ","; + case SEMICOLON_AMPERSAND: + return "&"; + default: + Preconditions.checkState(false, "Unknown separator type " + separatorType); + return null; + } } }; } @@ -161,7 +224,7 @@ public class ResourceContainerConverter { if (!dependencies.getTransitiveResources().isEmpty()) { cmdBuilder.addJoinStrings( "--data", - ",", + toArg.listSeparator(), Iterables.unmodifiableIterable( Iterables.transform(dependencies.getTransitiveResources(), toArg))); } @@ -170,7 +233,7 @@ public class ResourceContainerConverter { if (!dependencies.getDirectResources().isEmpty()) { cmdBuilder.addJoinStrings( "--directData", - ",", + toArg.listSeparator(), Iterables.unmodifiableIterable( Iterables.transform(dependencies.getDirectResources(), toArg))); } diff --git a/src/test/shell/bazel/android/BUILD b/src/test/shell/bazel/android/BUILD index ac44c592d9..3861a9e1f5 100644 --- a/src/test/shell/bazel/android/BUILD +++ b/src/test/shell/bazel/android/BUILD @@ -17,6 +17,7 @@ sh_test( "//external:android_ndk_for_testing", "//external:android_sdk_for_testing", "//src/tools/android/java/com/google/devtools/build/android:AarGeneratorAction_deploy.jar", + "//src/tools/android/java/com/google/devtools/build/android:AndroidResourceMergingAction_deploy.jar", "//src/tools/android/java/com/google/devtools/build/android:AndroidResourceParsingAction_deploy.jar", "//src/tools/android/java/com/google/devtools/build/android:AndroidResourceProcessingAction_deploy.jar", "//src/tools/android/java/com/google/devtools/build/android:RClassGeneratorAction_deploy.jar", diff --git a/src/tools/android/java/com/google/devtools/build/android/AarGeneratorAction.java b/src/tools/android/java/com/google/devtools/build/android/AarGeneratorAction.java index dcf0c56ab1..e1175115bc 100644 --- a/src/tools/android/java/com/google/devtools/build/android/AarGeneratorAction.java +++ b/src/tools/android/java/com/google/devtools/build/android/AarGeneratorAction.java @@ -82,8 +82,8 @@ public class AarGeneratorAction { category = "input", help = "Additional Data dependencies. These values will be used if not defined in " + "the primary resources. The expected format is " - + "resources[#resources]:assets[#assets]:manifest:r.txt" - + "[,resources[#resources]:assets[#assets]:manifest:r.txt]") + + DependencyAndroidData.EXPECTED_FORMAT + + "[,...]") public List<DependencyAndroidData> dependencyData; @Option(name = "manifest", diff --git a/src/tools/android/java/com/google/devtools/build/android/AndroidDataMerger.java b/src/tools/android/java/com/google/devtools/build/android/AndroidDataMerger.java index f330b95f9e..d682144ef7 100644 --- a/src/tools/android/java/com/google/devtools/build/android/AndroidDataMerger.java +++ b/src/tools/android/java/com/google/devtools/build/android/AndroidDataMerger.java @@ -52,12 +52,12 @@ public class AndroidDataMerger { private final AndroidDataSerializer serializer; - private final DependencyAndroidData dependency; + private final SerializedAndroidData dependency; private final Builder targetBuilder; private ParseDependencyDataTask( - AndroidDataSerializer serializer, DependencyAndroidData dependency, Builder targetBuilder) { + AndroidDataSerializer serializer, SerializedAndroidData dependency, Builder targetBuilder) { this.serializer = serializer; this.dependency = dependency; this.targetBuilder = targetBuilder; @@ -72,11 +72,10 @@ public class AndroidDataMerger { if (!e.isLegacy()) { throw new MergingException(e); } - //TODO(corysmith): List the offending target here. logger.fine( String.format( "\u001B[31mDEPRECATION:\u001B[0m Legacy resources used for %s", - dependency.getManifest())); + dependency.getLabel())); // Legacy android resources -- treat them as direct dependencies. dependency.walk(ParsedAndroidDataBuildingPathWalker.create(parsedDataBuilder)); } @@ -187,15 +186,17 @@ public class AndroidDataMerger { } /** - * Merges a list of {@link DependencyAndroidData} with a {@link UnvalidatedAndroidData}. + * Loads a list of dependency {@link SerializedAndroidData} and merge with the primary {@link + * ParsedAndroidData}. * * @see AndroidDataMerger#merge(ParsedAndroidData, ParsedAndroidData, UnvalidatedAndroidData, - * boolean) for details. + * boolean) for details. */ - UnwrittenMergedAndroidData merge( - List<DependencyAndroidData> transitive, - List<DependencyAndroidData> direct, - UnvalidatedAndroidData primary, + UnwrittenMergedAndroidData loadAndMerge( + List<? extends SerializedAndroidData> transitive, + List<? extends SerializedAndroidData> direct, + ParsedAndroidData primary, + Path primaryManifest, boolean allowPrimaryOverrideAll) throws MergingException { Stopwatch timer = Stopwatch.createStarted(); @@ -204,12 +205,12 @@ public class AndroidDataMerger { final ParsedAndroidData.Builder transitiveBuilder = ParsedAndroidData.Builder.newBuilder(); final AndroidDataSerializer serializer = AndroidDataSerializer.create(); final List<ListenableFuture<Boolean>> tasks = new ArrayList<>(); - for (final DependencyAndroidData dependency : direct) { + for (final SerializedAndroidData dependency : direct) { tasks.add( executorService.submit( new ParseDependencyDataTask(serializer, dependency, directBuilder))); } - for (final DependencyAndroidData dependency : transitive) { + for (final SerializedAndroidData dependency : transitive) { tasks.add( executorService.submit( new ParseDependencyDataTask(serializer, dependency, transitiveBuilder))); @@ -222,8 +223,12 @@ public class AndroidDataMerger { logger.fine( String.format("Merged dependencies read in %sms", timer.elapsed(TimeUnit.MILLISECONDS))); timer.reset().start(); - return merge( - transitiveBuilder.build(), directBuilder.build(), primary, allowPrimaryOverrideAll); + return doMerge( + transitiveBuilder.build(), + directBuilder.build(), + primary, + primaryManifest, + allowPrimaryOverrideAll); } finally { logger.fine(String.format("Resources merged in %sms", timer.elapsed(TimeUnit.MILLISECONDS))); } @@ -288,7 +293,7 @@ public class AndroidDataMerger { * @return An UnwrittenMergedAndroidData, containing DataResource objects that can be written to * disk for aapt processing or serialized for future merge passes. * @throws MergingException if there are merge conflicts or issues with parsing resources from - * Primary. + * primaryData. */ UnwrittenMergedAndroidData merge( ParsedAndroidData transitive, @@ -296,18 +301,30 @@ public class AndroidDataMerger { UnvalidatedAndroidData primaryData, boolean allowPrimaryOverrideAll) throws MergingException { - try { // Extract the primary resources. ParsedAndroidData parsedPrimary = ParsedAndroidData.from(primaryData); + return doMerge( + transitive, direct, parsedPrimary, primaryData.getManifest(), allowPrimaryOverrideAll); + } catch (IOException e) { + throw new MergingException(e); + } + } + private UnwrittenMergedAndroidData doMerge( + ParsedAndroidData transitive, + ParsedAndroidData direct, + ParsedAndroidData parsedPrimary, + Path primaryManifest, + boolean allowPrimaryOverrideAll) + throws MergingException { + try { // Create the builders for the final parsed data. final ParsedAndroidData.Builder primaryBuilder = ParsedAndroidData.Builder.newBuilder(); final ParsedAndroidData.Builder transitiveBuilder = ParsedAndroidData.Builder.newBuilder(); final KeyValueConsumers transitiveConsumers = transitiveBuilder.consumers(); final KeyValueConsumers primaryConsumers = primaryBuilder.consumers(); - final Set<MergeConflict> conflicts = new HashSet<>(); conflicts.addAll(parsedPrimary.conflicts()); for (MergeConflict conflict : Iterables.concat(direct.conflicts(), transitive.conflicts())) { @@ -420,7 +437,7 @@ public class AndroidDataMerger { } return UnwrittenMergedAndroidData.of( - primaryData.getManifest(), + primaryManifest, primaryBuilder.build(), transitiveBuilder.build()); } catch (IOException e) { diff --git a/src/tools/android/java/com/google/devtools/build/android/AndroidResourceClassWriter.java b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceClassWriter.java index 4c3118e61b..9140cb7ec4 100644 --- a/src/tools/android/java/com/google/devtools/build/android/AndroidResourceClassWriter.java +++ b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceClassWriter.java @@ -62,6 +62,8 @@ public class AndroidResourceClassWriter implements Flushable { private final AndroidFrameworkAttrIdProvider androidIdProvider; private final Path outputBasePath; private final String packageName; + private boolean includeClassFile = true; + private boolean includeJavaFile = true; private final Map<ResourceType, Set<String>> innerClasses = new EnumMap<>(ResourceType.class); private final Map<String, Map<String, Boolean>> styleableAttrs = new HashMap<>(); @@ -79,6 +81,14 @@ public class AndroidResourceClassWriter implements Flushable { this.packageName = packageName; } + public void setIncludeClassFile(boolean include) { + this.includeClassFile = include; + } + + public void setIncludeJavaFile(boolean include) { + this.includeJavaFile = include; + } + public void writeSimpleResource(ResourceType type, String name) { Set<String> fields = innerClasses.get(type); if (fields == null) { @@ -140,8 +150,12 @@ public class AndroidResourceClassWriter implements Flushable { throw new IOException(e); } - writeAsJava(initializers); - writeAsClass(initializers); + if (includeClassFile) { + writeAsClass(initializers); + } + if (includeJavaFile) { + writeAsJava(initializers); + } } /** diff --git a/src/tools/android/java/com/google/devtools/build/android/AndroidResourceMergingAction.java b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceMergingAction.java new file mode 100644 index 0000000000..3d70fd07cc --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceMergingAction.java @@ -0,0 +1,250 @@ +// 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.VariantConfiguration.Type; +import com.android.ide.common.internal.LoggedErrorException; +import com.android.ide.common.internal.PngCruncher; +import com.android.ide.common.res2.MergingException; +import com.android.utils.StdLogger; +import com.google.common.base.Preconditions; +import com.google.common.base.Stopwatch; +import com.google.common.io.Files; +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.OptionsBase; +import com.google.devtools.common.options.OptionsParser; +import java.io.File; +import java.io.IOException; +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", + 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", + help = "Path to primary resource's manifest file." + ) + public Path primaryManifest; + + @Option( + name = "data", + defaultValue = "", + converter = SerializedAndroidDataListConverter.class, + category = "input", + 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", + 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", + help = "Path to the write merged resources archive." + ) + public Path resourcesOutput; + + @Option( + name = "classJarOutput", + defaultValue = "null", + converter = PathConverter.class, + category = "output", + help = "Path for the generated java class jar." + ) + public Path classJarOutput; + + @Option( + name = "manifestOutput", + defaultValue = "null", + converter = PathConverter.class, + category = "output", + help = "Path for the output processed AndroidManifest.xml." + ) + public Path manifestOutput; + + @Option( + name = "packageForR", + defaultValue = "null", + category = "config", + help = "Custom java package to generate the R symbols files." + ) + public String packageForR; + } + + public static void main(String[] args) throws Exception { + final Stopwatch timer = Stopwatch.createStarted(); + OptionsParser optionsParser = + OptionsParser.newOptionsParser(Options.class, AaptConfigOptions.class); + optionsParser.parseAndExitUponError(args); + AaptConfigOptions aaptConfigOptions = optionsParser.getOptions(AaptConfigOptions.class); + Options options = optionsParser.getOptions(Options.class); + + final AndroidResourceProcessor resourceProcessor = new AndroidResourceProcessor(stdLogger); + + Preconditions.checkNotNull(options.primaryData); + Preconditions.checkNotNull(options.primaryManifest); + Preconditions.checkNotNull(options.classJarOutput); + + try (ScopedTemporaryDirectory scopedTmp = + new ScopedTemporaryDirectory("android_resource_merge_tmp")) { + 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))); + + VariantConfiguration.Type packageType = Type.LIBRARY; + String packageName = options.packageForR; + AndroidResourceClassWriter resourceClassWriter = + new AndroidResourceClassWriter( + new AndroidFrameworkAttrIdJar(aaptConfigOptions.androidJar), + generatedSources, + packageName); + resourceClassWriter.setIncludeClassFile(true); + resourceClassWriter.setIncludeJavaFile(false); + + final MergedAndroidData mergedData = + resourceProcessor.mergeData( + options.primaryData, + options.primaryManifest, + options.directData, + options.transitiveData, + mergedResources, + mergedAssets, + new StubPngCruncher(), + packageType, + resourceClassWriter); + + 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 = + resourceProcessor.processManifest( + packageType, + options.packageForR, + null, /* applicationId */ + -1, /* versionCode */ + null, /* versionName */ + mergedData, + processedManifest); + resourceProcessor.copyManifestToOutput(processedData, options.manifestOutput); + } + + resourceProcessor.createClassJar(generatedSources, options.classJarOutput); + + logger.fine( + String.format("Create classJar finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS))); + + if (options.resourcesOutput != null) { + // 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). + resourceProcessor.createResourcesZip( + mergedData.getResourceDir(), + mergedData.getAssetDir(), + options.resourcesOutput, + true /* compress */); + logger.fine( + String.format( + "Create resources.zip finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS))); + } + } catch (MergingException e) { + logger.log(Level.SEVERE, "Error during merging resources", e); + throw e; + } catch (Exception e) { + logger.log(Level.SEVERE, "Unexpected", e); + throw e; + } finally { + resourceProcessor.shutdown(); + } + 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(File from, File to) + throws InterruptedException, LoggedErrorException, IOException { + Files.touch(to); + } + } +} diff --git a/src/tools/android/java/com/google/devtools/build/android/AndroidResourceParsingAction.java b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceParsingAction.java index e48440db48..65ff00aadf 100644 --- a/src/tools/android/java/com/google/devtools/build/android/AndroidResourceParsingAction.java +++ b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceParsingAction.java @@ -45,7 +45,7 @@ public class AndroidResourceParsingAction { converter = UnvalidatedAndroidDirectoriesConverter.class, category = "input", help = "The resource and asset directories to parse and summarize in a symbols file." - + " The expected format is resources[#resources]:assets[#assets]") + + " The expected format is " + UnvalidatedAndroidDirectories.EXPECTED_FORMAT) public UnvalidatedAndroidDirectories primaryData; @Option(name = "output", diff --git a/src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessingAction.java b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessingAction.java index 7983256ec6..cc78940086 100644 --- a/src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessingAction.java +++ b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessingAction.java @@ -56,11 +56,11 @@ import java.util.logging.Logger; * --adb path/to/sdk/adb\ * --zipAlign path/to/sdk/zipAlign\ * --androidJar path/to/sdk/androidJar\ - * --manifest path/to/manifest\ - * --primaryData path/to/resources:path/to/assets:path/to/manifest:path/to/R.txt - * --data p/t/res1:p/t/assets1:p/t/1/AndroidManifest.xml:p/t/1/R.txt,\ - * p/t/res2:p/t/assets2:p/t/2/AndroidManifest.xml:p/t/2/R.txt - * --packagePath path/to/write/archive.ap_ + * --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 * </pre> */ @@ -80,7 +80,7 @@ public class AndroidResourceProcessingAction { category = "input", 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 resources[|resources]:assets[|assets]:manifest") + + " is " + UnvalidatedAndroidData.EXPECTED_FORMAT) public UnvalidatedAndroidData primaryData; @Option(name = "data", @@ -89,8 +89,8 @@ public class AndroidResourceProcessingAction { category = "input", help = "Transitive Data dependencies. These values will be used if not defined in the " + "primary resources. The expected format is " - + "resources[#resources]:assets[#assets]:manifest:r.txt:symbols.bin" - + "[,resources[#resources]:assets[#assets]:manifest:r.txt:symbols.bin]") + + DependencyAndroidData.EXPECTED_FORMAT + + "[,...]") public List<DependencyAndroidData> transitiveData; @Option(name = "directData", @@ -99,8 +99,8 @@ public class AndroidResourceProcessingAction { category = "input", help = "Direct Data dependencies. These values will be used if not defined in the " + "primary resources. The expected format is " - + "resources[#resources]:assets[#assets]:manifest:r.txt:symbols.bin" - + "[,resources[#resources]:assets[#assets]:manifest:r.txt:symbols.bin]") + + DependencyAndroidData.EXPECTED_FORMAT + + "[,...]") public List<DependencyAndroidData> directData; @Option(name = "rOutput", @@ -333,7 +333,8 @@ public class AndroidResourceProcessingAction { resourceProcessor.createResourcesZip( processedData.getResourceDir(), processedData.getAssetDir(), - options.resourcesOutput); + options.resourcesOutput, + false /* compress */); } logger.fine( String.format("Packaging finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS))); diff --git a/src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessor.java b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessor.java index d5194a098d..6e2f340623 100644 --- a/src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessor.java +++ b/src/tools/android/java/com/google/devtools/build/android/AndroidResourceProcessor.java @@ -384,17 +384,22 @@ public class AndroidResourceProcessor { * @param resourcesRoot The root containing android resources to be written. * @param assetsRoot The root containing android assets to be written. * @param output The path to write the zip file + * @param compress Whether or not to compress the content * @throws IOException */ - public void createResourcesZip(Path resourcesRoot, Path assetsRoot, Path output) + public void createResourcesZip(Path resourcesRoot, Path assetsRoot, Path output, boolean compress) throws IOException { try (ZipOutputStream zout = new ZipOutputStream( new BufferedOutputStream(Files.newOutputStream(output)))) { if (Files.exists(resourcesRoot)) { - Files.walkFileTree(resourcesRoot, new ZipBuilderVisitor(zout, resourcesRoot, "res")); + ZipBuilderVisitor visitor = new ZipBuilderVisitor(zout, resourcesRoot, "res"); + visitor.setCompress(compress); + Files.walkFileTree(resourcesRoot, visitor); } if (Files.exists(assetsRoot)) { - Files.walkFileTree(assetsRoot, new ZipBuilderVisitor(zout, assetsRoot, "assets")); + ZipBuilderVisitor visitor = new ZipBuilderVisitor(zout, assetsRoot, "assets"); + visitor.setCompress(compress); + Files.walkFileTree(assetsRoot, visitor); } } } @@ -1005,27 +1010,80 @@ public class AndroidResourceProcessor { return output; } - + /** - * Merges all secondary resources with the primary resources. + * Merges all secondary resources with the primary resources, given that the primary resources + * have not yet been parsed and serialized. */ public MergedAndroidData mergeData( final UnvalidatedAndroidData primary, - final List<DependencyAndroidData> direct, - final List<DependencyAndroidData> transitive, + final List<? extends SerializedAndroidData> direct, + final List<? extends SerializedAndroidData> transitive, final Path resourcesOut, final Path assetsOut, @Nullable final PngCruncher cruncher, final VariantConfiguration.Type type, @Nullable final Path symbolsOut) throws MergingException { + try { + final ParsedAndroidData parsedPrimary = ParsedAndroidData.from(primary); + return mergeData(parsedPrimary, primary.getManifest(), direct, transitive, + resourcesOut, assetsOut, cruncher, type, symbolsOut, null /* rclassWriter */); + } catch (IOException e) { + throw new MergingException(e); + } + } + + /** + * Merges all secondary resources with the primary resources, given that the primary resources + * have been separately parsed and serialized. + */ + public MergedAndroidData mergeData( + final SerializedAndroidData primary, + final Path primaryManifest, + final List<? extends SerializedAndroidData> direct, + final List<? extends SerializedAndroidData> transitive, + final Path resourcesOut, + final Path assetsOut, + @Nullable final PngCruncher cruncher, + final VariantConfiguration.Type type, + @Nullable AndroidResourceClassWriter rclassWriter) + throws MergingException { + final ParsedAndroidData.Builder primaryBuilder = ParsedAndroidData.Builder.newBuilder(); + final AndroidDataSerializer serializer = AndroidDataSerializer.create(); + primary.deserialize(serializer, primaryBuilder.consumers()); + ParsedAndroidData primaryData = primaryBuilder.build(); + return mergeData(primaryData, primaryManifest, direct, transitive, + resourcesOut, assetsOut, cruncher, type, null /* symbolsOut */, rclassWriter); + } + + /** + * Merges all secondary resources with the primary resources. + */ + private MergedAndroidData mergeData( + final ParsedAndroidData primary, + final Path primaryManifest, + final List<? extends SerializedAndroidData> direct, + final List<? extends SerializedAndroidData> transitive, + final Path resourcesOut, + final Path assetsOut, + @Nullable final PngCruncher cruncher, + final VariantConfiguration.Type type, + @Nullable final Path symbolsOut, + @Nullable AndroidResourceClassWriter rclassWriter) + throws MergingException { Stopwatch timer = Stopwatch.createStarted(); final ListeningExecutorService executorService = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(15)); try (Closeable closeable = ExecutorServiceCloser.createWith(executorService)) { AndroidDataMerger merger = AndroidDataMerger.createWithPathDeduplictor(executorService); UnwrittenMergedAndroidData merged = - merger.merge(transitive, direct, primary, type != VariantConfiguration.Type.LIBRARY); + merger.loadAndMerge( + transitive, + direct, + primary, + primaryManifest, + type != VariantConfiguration.Type.LIBRARY); logger.fine(String.format("merge finished in %sms", timer.elapsed(TimeUnit.MILLISECONDS))); timer.reset().start(); if (symbolsOut != null) { @@ -1037,6 +1095,12 @@ public class AndroidResourceProcessor { "serialize merge finished in %sms", timer.elapsed(TimeUnit.MILLISECONDS))); timer.reset().start(); } + if (rclassWriter != null) { + merged.writeResourceClass(rclassWriter); + logger.fine( + String.format("write classes finished in %sms", timer.elapsed(TimeUnit.MILLISECONDS))); + timer.reset().start(); + } AndroidDataWriter writer = AndroidDataWriter.createWith( resourcesOut.getParent(), resourcesOut, assetsOut, cruncher, executorService); diff --git a/src/tools/android/java/com/google/devtools/build/android/BUILD b/src/tools/android/java/com/google/devtools/build/android/BUILD index f4b5689623..6dc6b4b2ce 100644 --- a/src/tools/android/java/com/google/devtools/build/android/BUILD +++ b/src/tools/android/java/com/google/devtools/build/android/BUILD @@ -26,6 +26,14 @@ java_binary( ) java_binary( + name = "AndroidResourceMergingAction", + main_class = "com.google.devtools.build.android.AndroidResourceMergingAction", + runtime_deps = [ + ":android_builder_lib", + ], +) + +java_binary( name = "AndroidResourceParsingAction", main_class = "com.google.devtools.build.android.AndroidResourceParsingAction", runtime_deps = [ diff --git a/src/tools/android/java/com/google/devtools/build/android/BUILD.tools b/src/tools/android/java/com/google/devtools/build/android/BUILD.tools index 612936fd71..3a08124f58 100644 --- a/src/tools/android/java/com/google/devtools/build/android/BUILD.tools +++ b/src/tools/android/java/com/google/devtools/build/android/BUILD.tools @@ -14,6 +14,14 @@ java_binary( ) java_binary( + name = "AndroidResourceMergingAction", + main_class = "com.google.devtools.build.android.AndroidResourceMergingAction", + runtime_deps = [ + ":classes", + ], +) + +java_binary( name = "AndroidResourceParsingAction", main_class = "com.google.devtools.build.android.AndroidResourceParsingAction", runtime_deps = [ diff --git a/src/tools/android/java/com/google/devtools/build/android/Converters.java b/src/tools/android/java/com/google/devtools/build/android/Converters.java index 7afc60b229..455fbccdba 100644 --- a/src/tools/android/java/com/google/devtools/build/android/Converters.java +++ b/src/tools/android/java/com/google/devtools/build/android/Converters.java @@ -67,7 +67,7 @@ public final class Converters { @Override public String getTypeDescription() { - return "unvalidated android data in the format " + UnvalidatedAndroidData.expectedFormat(); + return "unvalidated android data in the format " + UnvalidatedAndroidData.EXPECTED_FORMAT; } } @@ -90,7 +90,7 @@ public final class Converters { @Override public String getTypeDescription() { return "unvalidated android directories in the format " - + UnvalidatedAndroidDirectories.expectedFormat(); + + UnvalidatedAndroidDirectories.EXPECTED_FORMAT; } } @@ -104,7 +104,7 @@ public final class Converters { @Override public List<DependencyAndroidData> convert(String input) throws OptionsParsingException { if (input.isEmpty()) { - return ImmutableList.<DependencyAndroidData>of(); + return ImmutableList.of(); } try { ImmutableList.Builder<DependencyAndroidData> builder = ImmutableList.builder(); @@ -121,8 +121,58 @@ public final class Converters { @Override public String getTypeDescription() { return "a list of dependency android data in the format " - + "resources[#resources]:assets[#assets]:manifest:r.txt" - + "[,resources[#resources]:assets[#assets]:manifest:r.txt]"; + + DependencyAndroidData.EXPECTED_FORMAT + "[,...]"; + } + } + + /** + * Converter for a {@link SerializedAndroidData}. + */ + public static class SerializedAndroidDataConverter implements Converter<SerializedAndroidData> { + + @Override + public SerializedAndroidData convert(String input) throws OptionsParsingException { + try { + return SerializedAndroidData.valueOf(input); + } catch (IllegalArgumentException e) { + throw new OptionsParsingException( + String.format("invalid SerializedAndroidData: %s", e.getMessage()), e); + } + } + + @Override + public String getTypeDescription() { + return "preparsed android data in the format " + SerializedAndroidData.EXPECTED_FORMAT; + } + } + + /** + * Converter for a list of {@link SerializedAndroidData}. + */ + public static class SerializedAndroidDataListConverter + implements Converter<List<SerializedAndroidData>> { + + @Override + public List<SerializedAndroidData> convert(String input) throws OptionsParsingException { + if (input.isEmpty()) { + return ImmutableList.of(); + } + try { + ImmutableList.Builder<SerializedAndroidData> builder = ImmutableList.builder(); + for (String entry : input.split("&")) { + builder.add(SerializedAndroidData.valueOf(entry)); + } + return builder.build(); + } catch (IllegalArgumentException e) { + throw new OptionsParsingException( + String.format("invalid SerializedAndroidData: %s", e.getMessage()), e); + } + } + + @Override + public String getTypeDescription() { + return "a list of preparsed android data in the format " + + SerializedAndroidData.EXPECTED_FORMAT + "[&...]"; } } @@ -281,6 +331,15 @@ public final class Converters { } } + // Commas that are not escaped by a backslash. + private static final String UNESCAPED_COMMA_REGEX = "(?<!\\\\)\\,"; + // Colons that are not escaped by a backslash. + private static final String UNESCAPED_COLON_REGEX = "(?<!\\\\)\\:"; + + private static String unescapeInput(String input) { + return input.replace("\\:", ":").replace("\\,", ","); + } + /** * A converter for dictionary arguments of the format key:value[,key:value]*. The keys and values * may contain colons and commas as long as they are escaped with a backslash. @@ -301,8 +360,8 @@ public final class Converters { } Map<K, V> map = new LinkedHashMap<>(); // Only split on comma and colon that are not escaped with a backslash - for (String entry : input.split("(?<!\\\\)\\,")) { - String[] entryFields = entry.split("(?<!\\\\)\\:", -1); + for (String entry : input.split(UNESCAPED_COMMA_REGEX)) { + String[] entryFields = entry.split(UNESCAPED_COLON_REGEX, -1); if (entryFields.length < 2) { throw new OptionsParsingException(String.format( "Dictionary entry [%s] does not contain both a key and a value.", @@ -313,7 +372,7 @@ public final class Converters { entry)); } // Unescape any comma or colon that is not a key or value separator. - String keyString = entryFields[0].replace("\\:", ":").replace("\\,", ","); + String keyString = unescapeInput(entryFields[0]); K key = keyConverter.convert(keyString); if (map.containsKey(key)) { throw new OptionsParsingException(String.format( @@ -321,7 +380,7 @@ public final class Converters { keyString)); } // Unescape any comma or colon that is not a key or value separator. - String valueString = entryFields[1].replace("\\:", ":").replace("\\,", ","); + String valueString = unescapeInput(entryFields[1]); V value = valueConverter.convert(valueString); map.put(key, value); } diff --git a/src/tools/android/java/com/google/devtools/build/android/DependencyAndroidData.java b/src/tools/android/java/com/google/devtools/build/android/DependencyAndroidData.java index bd77054d36..4f9dc1458d 100644 --- a/src/tools/android/java/com/google/devtools/build/android/DependencyAndroidData.java +++ b/src/tools/android/java/com/google/devtools/build/android/DependencyAndroidData.java @@ -15,13 +15,10 @@ package com.google.devtools.build.android; import com.android.builder.dependency.SymbolFileProvider; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import java.io.File; -import java.io.IOException; import java.nio.file.FileSystem; import java.nio.file.FileSystems; -import java.nio.file.Files; import java.nio.file.Path; import java.util.Objects; import java.util.regex.Pattern; @@ -37,8 +34,11 @@ import java.util.regex.Pattern; * invocation) AndroidData can have multiple roots for resources and assets. * </p> */ -class DependencyAndroidData { - static final Pattern VALID_REGEX = Pattern.compile(".*:.*:.+:.+(:.*)?"); +class DependencyAndroidData extends SerializedAndroidData { + private static final Pattern VALID_REGEX = Pattern.compile(".*:.*:.+:.+(:.*)?"); + + public static final String EXPECTED_FORMAT = + "resources[#resources]:assets[#assets]:manifest:r.txt:symbols.bin"; public static DependencyAndroidData valueOf(String text) { return valueOf(text, FileSystems.getDefault()); @@ -48,13 +48,11 @@ class DependencyAndroidData { static DependencyAndroidData valueOf(String text, FileSystem fileSystem) { if (!VALID_REGEX.matcher(text).find()) { throw new IllegalArgumentException( - text - + " is not in the format 'resources[#resources]:assets[#assets]:manifest:" - + "r.txt:symbols.txt'"); + text + " is not in the format '" + EXPECTED_FORMAT + "'"); } - String[] parts = text.split("\\:"); - // TODO(bazel-team): Handle the local-r.txt file. - // The local R is optional -- if it is missing, we'll use the full R.txt + String[] parts = text.split(":"); + // TODO(bazel-team): Handle the symbols.bin file. + // The local symbols.bin is optional -- if it is missing, we'll use the full R.txt return new DependencyAndroidData( splitPaths(parts[0], fileSystem), parts[1].length() == 0 ? ImmutableList.<Path>of() : splitPaths(parts[1], fileSystem), @@ -63,42 +61,19 @@ class DependencyAndroidData { parts.length == 5 ? fileSystem.getPath(parts[4]) : null); } - private static ImmutableList<Path> splitPaths(String pathsString, FileSystem fileSystem) { - if (pathsString.trim().isEmpty()) { - return ImmutableList.<Path>of(); - } - ImmutableList.Builder<Path> paths = new ImmutableList.Builder<>(); - for (String pathString : pathsString.split("#")) { - Preconditions.checkArgument(!pathString.trim().isEmpty()); - paths.add(exists(fileSystem.getPath(pathString))); - } - return paths.build(); - } - - private static Path exists(Path path) { - if (!Files.exists(path)) { - throw new IllegalArgumentException(path + " does not exist"); - } - return path; - } - - private final Path rTxt; private final Path manifest; - private final ImmutableList<Path> assetDirs; - private final ImmutableList<Path> resourceDirs; - private final Path symbolsTxt; + private final Path rTxt; public DependencyAndroidData( ImmutableList<Path> resourceDirs, ImmutableList<Path> assetDirs, Path manifest, Path rTxt, - Path symbolsTxt) { - this.resourceDirs = resourceDirs; - this.assetDirs = assetDirs; + Path symbols) { + // Use the manifest as a label for now. + super(resourceDirs, assetDirs, manifest.toString(), symbols); this.manifest = manifest; this.rTxt = rTxt; - this.symbolsTxt = symbolsTxt; } public SymbolFileProvider asSymbolFileProvider() { @@ -115,19 +90,15 @@ class DependencyAndroidData { }; } - public Path getManifest() { - return manifest; - } - @Override public String toString() { return String.format( - "AndroidData(%s, %s, %s, %s, %s)", resourceDirs, assetDirs, manifest, rTxt, symbolsTxt); + "AndroidData(%s, %s, %s, %s, %s)", resourceDirs, assetDirs, manifest, rTxt, symbols); } @Override public int hashCode() { - return Objects.hash(resourceDirs, assetDirs, manifest, rTxt, symbolsTxt); + return Objects.hash(resourceDirs, assetDirs, manifest, rTxt, symbols); } @Override @@ -145,27 +116,8 @@ class DependencyAndroidData { return Objects.equals(other.resourceDirs, resourceDirs) && Objects.equals(other.assetDirs, assetDirs) && Objects.equals(other.rTxt, rTxt) - && Objects.equals(other.symbolsTxt, symbolsTxt) + && Objects.equals(other.symbols, symbols) && Objects.equals(other.manifest, manifest); } - public void walk(final AndroidDataPathWalker pathWalker) throws IOException { - for (Path path : resourceDirs) { - pathWalker.walkResources(path); - } - for (Path path : assetDirs) { - pathWalker.walkAssets(path); - } - } - - public void deserialize( - AndroidDataSerializer serializer, - KeyValueConsumers consumers) - throws DeserializationException { - // Missing symbolsTxt means the resources where provided via android_resources rules. - if (symbolsTxt == null) { - throw new DeserializationException(true); - } - serializer.read(symbolsTxt, consumers); - } } diff --git a/src/tools/android/java/com/google/devtools/build/android/ResourceShrinkerAction.java b/src/tools/android/java/com/google/devtools/build/android/ResourceShrinkerAction.java index 4d0498533f..a4b5f6ba2d 100644 --- a/src/tools/android/java/com/google/devtools/build/android/ResourceShrinkerAction.java +++ b/src/tools/android/java/com/google/devtools/build/android/ResourceShrinkerAction.java @@ -238,7 +238,7 @@ public class ResourceShrinkerAction { null /* dataBindingInfoOut */); if (options.shrunkResources != null) { resourceProcessor.createResourcesZip(shrunkResources, resourceFiles.resolve("assets"), - options.shrunkResources); + options.shrunkResources, false /* compress */); } logger.fine(String.format("Packing resources finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS))); diff --git a/src/tools/android/java/com/google/devtools/build/android/SerializedAndroidData.java b/src/tools/android/java/com/google/devtools/build/android/SerializedAndroidData.java new file mode 100644 index 0000000000..08f32b9594 --- /dev/null +++ b/src/tools/android/java/com/google/devtools/build/android/SerializedAndroidData.java @@ -0,0 +1,135 @@ +// 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.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Objects; +import java.util.regex.Pattern; + +/** + * Android resource and assets that have been parsed ahead of time, and summarized by an {@link + * AndroidDataSerializer}. This class drives reloading the data. + */ +public class SerializedAndroidData { + + private static final Pattern VALID_REGEX = Pattern.compile(".*;.*;.+;.*"); + + public static final String EXPECTED_FORMAT = + "resources[#resources];assets[#assets];label;symbols.bin"; + + public static SerializedAndroidData valueOf(String text) { + return valueOf(text, FileSystems.getDefault()); + } + + static SerializedAndroidData valueOf(String text, FileSystem fileSystem) { + if (!VALID_REGEX.matcher(text).find()) { + throw new IllegalArgumentException(text + " is not in the format '" + EXPECTED_FORMAT + "'"); + } + String[] parts = text.split(";"); + return new SerializedAndroidData( + splitPaths(parts[0], fileSystem), + splitPaths(parts[1], fileSystem), + parts[2], + parts.length > 3 ? fileSystem.getPath(parts[3]) : null); + } + + protected static ImmutableList<Path> splitPaths(String pathsString, FileSystem fileSystem) { + if (pathsString.trim().isEmpty()) { + return ImmutableList.of(); + } + ImmutableList.Builder<Path> paths = new ImmutableList.Builder<>(); + for (String pathString : pathsString.split("#")) { + Preconditions.checkArgument(!pathString.trim().isEmpty()); + paths.add(exists(fileSystem.getPath(pathString))); + } + return paths.build(); + } + + protected static Path exists(Path path) { + if (!Files.exists(path)) { + throw new IllegalArgumentException(path + " does not exist"); + } + return path; + } + + protected final ImmutableList<Path> assetDirs; + protected final ImmutableList<Path> resourceDirs; + protected final String label; + protected final Path symbols; + + public SerializedAndroidData( + ImmutableList<Path> resourceDirs, ImmutableList<Path> assetDirs, String label, Path symbols) { + this.resourceDirs = resourceDirs; + this.assetDirs = assetDirs; + this.label = label; + this.symbols = symbols; + } + + public void walk(final AndroidDataPathWalker pathWalker) throws IOException { + for (Path path : resourceDirs) { + pathWalker.walkResources(path); + } + for (Path path : assetDirs) { + pathWalker.walkAssets(path); + } + } + + public void deserialize(AndroidDataSerializer serializer, KeyValueConsumers consumers) + throws DeserializationException { + // Missing symbols means the resources where provided via android_resources rules. + if (symbols == null) { + throw new DeserializationException(true); + } + serializer.read(symbols, consumers); + } + + public String getLabel() { + return label; + } + + @Override + public String toString() { + return String.format( + "SerializedAndroidData(%s, %s, %s, %s)", resourceDirs, assetDirs, label, symbols); + } + + @Override + public int hashCode() { + return Objects.hash(resourceDirs, assetDirs, label, symbols); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof SerializedAndroidData)) { + return false; + } + SerializedAndroidData other = (SerializedAndroidData) obj; + return Objects.equals(other.resourceDirs, resourceDirs) + && Objects.equals(other.assetDirs, assetDirs) + && Objects.equals(other.symbols, symbols) + && Objects.equals(other.label, label); + } +} diff --git a/src/tools/android/java/com/google/devtools/build/android/UnvalidatedAndroidData.java b/src/tools/android/java/com/google/devtools/build/android/UnvalidatedAndroidData.java index beeeba7e37..20717d6005 100644 --- a/src/tools/android/java/com/google/devtools/build/android/UnvalidatedAndroidData.java +++ b/src/tools/android/java/com/google/devtools/build/android/UnvalidatedAndroidData.java @@ -31,9 +31,7 @@ import java.util.regex.Pattern; class UnvalidatedAndroidData extends UnvalidatedAndroidDirectories { private static final Pattern VALID_REGEX = Pattern.compile(".*:.*:.+"); - static String expectedFormat() { - return "resources[#resources]:assets[#assets]:manifest"; - } + public static final String EXPECTED_FORMAT = "resources[#resources]:assets[#assets]:manifest"; public static UnvalidatedAndroidData valueOf(String text) { return valueOf(text, FileSystems.getDefault()); @@ -43,7 +41,7 @@ class UnvalidatedAndroidData extends UnvalidatedAndroidDirectories { static UnvalidatedAndroidData valueOf(String text, FileSystem fileSystem) { if (!VALID_REGEX.matcher(text).find()) { throw new IllegalArgumentException( - text + " is not in the format '" + expectedFormat() + "'"); + text + " is not in the format '" + EXPECTED_FORMAT + "'"); } String[] parts = text.split(":"); return new UnvalidatedAndroidData( diff --git a/src/tools/android/java/com/google/devtools/build/android/UnvalidatedAndroidDirectories.java b/src/tools/android/java/com/google/devtools/build/android/UnvalidatedAndroidDirectories.java index f87da7fbf0..5723f6ec61 100644 --- a/src/tools/android/java/com/google/devtools/build/android/UnvalidatedAndroidDirectories.java +++ b/src/tools/android/java/com/google/devtools/build/android/UnvalidatedAndroidDirectories.java @@ -30,9 +30,7 @@ public class UnvalidatedAndroidDirectories { private static final Pattern VALID_REGEX = Pattern.compile(".*:.*"); - static String expectedFormat() { - return "resources[#resources]:assets[#assets]"; - } + public static final String EXPECTED_FORMAT = "resources[#resources]:assets[#assets]"; public static UnvalidatedAndroidDirectories valueOf(String text) { return valueOf(text, FileSystems.getDefault()); @@ -42,7 +40,7 @@ public class UnvalidatedAndroidDirectories { static UnvalidatedAndroidDirectories valueOf(String text, FileSystem fileSystem) { if (!VALID_REGEX.matcher(text).find()) { throw new IllegalArgumentException( - text + " is not in the format '" + expectedFormat() + "'"); + text + " is not in the format '" + EXPECTED_FORMAT + "'"); } String[] parts = text.split(":"); return new UnvalidatedAndroidDirectories( diff --git a/tools/android/BUILD b/tools/android/BUILD index c32b795552..9eb2023587 100644 --- a/tools/android/BUILD +++ b/tools/android/BUILD @@ -36,6 +36,11 @@ alias( ) alias( + name = "resource_merger", + actual = "//src/tools/android/java/com/google/devtools/build/android:AndroidResourceMergingAction", +) + +alias( name = "resource_parser", actual = "//src/tools/android/java/com/google/devtools/build/android:AndroidResourceParsingAction", ) |