blob: 34b424b521de799744ae61c17e3b517a366e957a [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.ide.common.res2.MergingException;
import com.google.common.base.Joiner;
import com.google.common.base.Stopwatch;
import com.google.common.collect.Iterables;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.devtools.build.android.ParsedAndroidData.Builder;
import com.google.devtools.build.android.ParsedAndroidData.ParsedAndroidDataBuildingPathWalker;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
/** Handles the Merging of ParsedAndroidData. */
public class AndroidDataMerger {
private static final Logger logger = Logger.getLogger(AndroidDataMerger.class.getCanonicalName());
private static final class ParseDependencyDataTask implements Callable<Boolean> {
private final AndroidDataSerializer serializer;
private final SerializedAndroidData dependency;
private final Builder targetBuilder;
private ParseDependencyDataTask(
AndroidDataSerializer serializer, SerializedAndroidData dependency, Builder targetBuilder) {
this.serializer = serializer;
this.dependency = dependency;
this.targetBuilder = targetBuilder;
}
@Override
public Boolean call() throws Exception {
final Builder parsedDataBuilder = ParsedAndroidData.Builder.newBuilder();
try {
dependency.deserialize(serializer, parsedDataBuilder.consumers());
} catch (DeserializationException e) {
if (!e.isLegacy()) {
throw MergingException.wrapException(e).build();
}
logger.fine(
String.format(
"\u001B[31mDEPRECATION:\u001B[0m Legacy resources used for %s",
dependency.getLabel()));
// Legacy android resources -- treat them as direct dependencies.
dependency.walk(ParsedAndroidDataBuildingPathWalker.create(parsedDataBuilder));
}
// The builder isn't threadsafe, so synchronize the copyTo call.
synchronized (targetBuilder) {
// All the resources are sorted before writing, so they can be aggregated in
// whatever order here.
parsedDataBuilder.copyTo(targetBuilder);
}
// Had to return something?
return Boolean.TRUE;
}
}
/** Interface for comparing paths. */
interface SourceChecker {
boolean checkEquality(DataSource one, DataSource two) throws IOException;
}
/** Compares two paths by the contents of the files. */
static class ContentComparingChecker implements SourceChecker {
static SourceChecker create() {
return new ContentComparingChecker();
}
@Override
public boolean checkEquality(DataSource one, DataSource two) throws IOException {
// TODO(corysmith): Is there a filesystem hash we can use?
if (one.getFileSize() != two.getFileSize()) {
return false;
}
try (final InputStream oneStream = one.newBufferedInputStream();
final InputStream twoStream = two.newBufferedInputStream()) {
int bytesRead = 0;
while (true) {
int oneByte = oneStream.read();
int twoByte = twoStream.read();
bytesRead++;
if (oneByte == -1 || twoByte == -1) {
if (oneByte == twoByte) {
return true;
} else {
// getFileSize did not return correct size.
logger.severe(
String.format(
"Filesystem size of %s (%s) or %s (%s) is inconsistant with bytes read %s.",
one, one.getFileSize(), two, two.getFileSize(), bytesRead));
return false;
}
}
if (oneByte != twoByte) {
return false;
}
}
}
}
}
static class NoopSourceChecker implements SourceChecker {
static SourceChecker create() {
return new NoopSourceChecker();
}
@Override
public boolean checkEquality(DataSource one, DataSource two) {
return false;
}
}
private final SourceChecker deDuplicator;
private final ListeningExecutorService executorService;
/** Creates a merger with no path deduplication and a default {@link ExecutorService}. */
public static AndroidDataMerger createWithDefaults() {
return createWithDefaultThreadPool(NoopSourceChecker.create());
}
/** Creates a merger with a custom deduplicator and a default {@link ExecutorService}. */
public static AndroidDataMerger createWithDefaultThreadPool(SourceChecker deDuplicator) {
return new AndroidDataMerger(deDuplicator, MoreExecutors.newDirectExecutorService());
}
/** Creates a merger with a custom deduplicator and an {@link ExecutorService}. */
public static AndroidDataMerger create(
SourceChecker deDuplicator, ListeningExecutorService executorService) {
return new AndroidDataMerger(deDuplicator, executorService);
}
/** Creates a merger with a file contents hashing deduplicator. */
public static AndroidDataMerger createWithPathDeduplictor(
ListeningExecutorService executorService) {
return create(ContentComparingChecker.create(), executorService);
}
private AndroidDataMerger(SourceChecker deDuplicator, ListeningExecutorService executorService) {
this.deDuplicator = deDuplicator;
this.executorService = executorService;
}
/**
* Loads a list of dependency {@link SerializedAndroidData} and merge with the primary {@link
* ParsedAndroidData}.
*
* @see AndroidDataMerger#merge(ParsedAndroidData, ParsedAndroidData, UnvalidatedAndroidData,
* boolean) for details.
*/
UnwrittenMergedAndroidData loadAndMerge(
List<? extends SerializedAndroidData> transitive,
List<? extends SerializedAndroidData> direct,
ParsedAndroidData primary,
Path primaryManifest,
boolean allowPrimaryOverrideAll)
throws MergingException {
Stopwatch timer = Stopwatch.createStarted();
try {
final ParsedAndroidData.Builder directBuilder = ParsedAndroidData.Builder.newBuilder();
final ParsedAndroidData.Builder transitiveBuilder = ParsedAndroidData.Builder.newBuilder();
final AndroidDataSerializer serializer = AndroidDataSerializer.create();
final List<ListenableFuture<Boolean>> tasks = new ArrayList<>();
for (final SerializedAndroidData dependency : direct) {
tasks.add(
executorService.submit(
new ParseDependencyDataTask(serializer, dependency, directBuilder)));
}
for (final SerializedAndroidData dependency : transitive) {
tasks.add(
executorService.submit(
new ParseDependencyDataTask(serializer, dependency, transitiveBuilder)));
}
// Wait for all the parsing to complete.
FailedFutureAggregator<MergingException> aggregator =
FailedFutureAggregator.createForMergingExceptionWithMessage(
"Failure(s) during dependency parsing");
aggregator.aggregateAndMaybeThrow(tasks);
logger.fine(
String.format("Merged dependencies read in %sms", timer.elapsed(TimeUnit.MILLISECONDS)));
timer.reset().start();
return doMerge(
transitiveBuilder.build(),
directBuilder.build(),
primary,
primaryManifest,
allowPrimaryOverrideAll);
} finally {
logger.fine(String.format("Resources merged in %sms", timer.elapsed(TimeUnit.MILLISECONDS)));
}
}
/**
* Merges DataResources into an UnwrittenMergedAndroidData.
*
* <p>This method has two basic states, library and binary. These are distinguished by
* allowPrimaryOverrideAll, which allows the primary data to overwrite any value in the closure, a
* trait associated with binaries, as a binary is a leaf node. The other semantics are slightly
* more complicated: a given resource can be overwritten only if it resides in the direct
* dependencies of primary data. This forces an explicit simple priority for each resource,
* instead of the more subtle semantics of multiple layers of libraries with potential overwrites.
*
* <p>The UnwrittenMergedAndroidData contains only one of each DataKey in both the direct and
* transitive closure.
*
* <p>The merge semantics for overwriting resources (non id and styleable) are as follows:
*
* <pre>
* Key:
* A(): package A
* A(foo): package A with resource symbol foo
* A() -> B(): a dependency relationship of B.deps = [:A]
* A(),B() -> C(): a dependency relationship of C.deps = [:A,:B]
*
* For android library (allowPrimaryOverrideAll = False)
*
* A() -> B(foo) -> C(foo) == Valid
* A() -> B() -> C(foo) == Valid
* A() -> B() -> C(foo),D(foo) == Conflict
* A(foo) -> B(foo) -> C() == Conflict
* A(foo) -> B() -> C(foo) == Conflict
* A(foo),B(foo) -> C() -> D() == Conflict
* A() -> B(foo),C(foo) -> D() == Conflict
* A(foo),B(foo) -> C() -> D(foo) == Conflict
* A() -> B(foo),C(foo) -> D(foo) == Conflict
*
* For android binary (allowPrimaryOverrideAll = True)
*
* A() -> B(foo) -> C(foo) == Valid
* A() -> B() -> C(foo) == Valid
* A() -> B() -> C(foo),D(foo) == Conflict
* A(foo) -> B(foo) -> C() == Conflict
* A(foo) -> B() -> C(foo) == Valid
* A(foo),B(foo) -> C() -> D() == Conflict
* A() -> B(foo),C(foo) -> D() == Conflict
* A(foo),B(foo) -> C() -> D(foo) == Valid
* A() -> B(foo),C(foo) -> D(foo) == Valid
* </pre>
*
* <p>Combining resources are much simpler -- since a combining (id and styleable) resource does
* not get replaced when redefined, they are simply combined:
*
* <pre>
* A(foo) -> B(foo) -> C(foo) == Valid
*
* </pre>
*
* @param transitive The transitive dependencies to merge.
* @param direct The direct dependencies to merge.
* @param primaryData The primary data to merge against.
* @param allowPrimaryOverrideAll Boolean that indicates if the primary data will be considered
* the ultimate source of truth, provided it doesn't conflict with itself.
* @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
* primaryData.
*/
UnwrittenMergedAndroidData merge(
ParsedAndroidData transitive,
ParsedAndroidData direct,
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 MergingException.wrapException(e).build();
}
}
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<>();
// Find all internal conflicts.
conflicts.addAll(parsedPrimary.conflicts());
for (MergeConflict conflict : Iterables.concat(direct.conflicts(), transitive.conflicts())) {
if (allowPrimaryOverrideAll
&& (parsedPrimary.containsOverwritable(conflict.dataKey())
|| parsedPrimary.containsAsset(conflict.dataKey()))) {
continue;
}
conflicts.add(conflict);
}
// overwriting resources
for (Entry<DataKey, DataResource> entry : parsedPrimary.iterateOverwritableEntries()) {
if (direct.containsOverwritable(entry.getKey())) {
primaryConsumers.overwritingConsumer.consume(
entry.getKey(), entry.getValue().overwrite(direct.getOverwritable(entry.getKey())));
} else {
primaryConsumers.overwritingConsumer.consume(entry.getKey(), entry.getValue());
}
}
for (Map.Entry<DataKey, DataResource> entry : direct.iterateOverwritableEntries()) {
// Direct dependencies are simply overwritten, no conflict.
if (!parsedPrimary.containsOverwritable(entry.getKey())) {
transitiveConsumers.overwritingConsumer.consume(entry.getKey(), entry.getValue());
}
}
for (Map.Entry<DataKey, DataResource> entry : transitive.iterateOverwritableEntries()) {
// If the primary is considered to be intentional (usually at the binary level),
// skip.
if (allowPrimaryOverrideAll && parsedPrimary.containsOverwritable(entry.getKey())) {
continue;
}
// If a transitive value is in the direct map report a conflict, as it is commonly
// unintentional.
if (direct.containsOverwritable(entry.getKey())) {
conflicts.add(direct.foundResourceConflict(entry.getKey(), entry.getValue()));
} else if (parsedPrimary.containsOverwritable(entry.getKey())) {
// If overwriting a transitive value with a primary map, assume it's an unintentional
// override, unless allowPrimaryOverrideAll is set. At which point, this code path
// should not be reached.
conflicts.add(parsedPrimary.foundResourceConflict(entry.getKey(), entry.getValue()));
} else {
// If it's in none of the of sources, add it.
transitiveConsumers.overwritingConsumer.consume(entry.getKey(), entry.getValue());
}
}
// combining resources
for (Entry<DataKey, DataResource> entry : parsedPrimary.iterateCombiningEntries()) {
primaryConsumers.combiningConsumer.consume(entry.getKey(), entry.getValue());
}
for (Map.Entry<DataKey, DataResource> entry : direct.iterateCombiningEntries()) {
if (parsedPrimary.containsCombineable(entry.getKey())) {
// If it is in the primary, add it to the primary to be combined.
primaryConsumers.combiningConsumer.consume(entry.getKey(), entry.getValue());
} else {
// If the combining asset is not in the primary, put it into the transitive.
transitiveConsumers.combiningConsumer.consume(entry.getKey(), entry.getValue());
}
}
for (Map.Entry<DataKey, DataResource> entry : transitive.iterateCombiningEntries()) {
if (parsedPrimary.containsCombineable(entry.getKey())) {
primaryConsumers.combiningConsumer.consume(entry.getKey(), entry.getValue());
} else {
transitiveConsumers.combiningConsumer.consume(entry.getKey(), entry.getValue());
}
}
// assets
for (Entry<DataKey, DataAsset> entry : parsedPrimary.iterateAssetEntries()) {
if (direct.containsAsset(entry.getKey())) {
primaryConsumers.assetConsumer.consume(
entry.getKey(), entry.getValue().overwrite(direct.getAsset(entry.getKey())));
} else {
primaryConsumers.assetConsumer.consume(entry.getKey(), entry.getValue());
}
}
for (Map.Entry<DataKey, DataAsset> entry : direct.iterateAssetEntries()) {
// Direct dependencies are simply overwritten, no conflict.
if (!parsedPrimary.containsAsset(entry.getKey())) {
transitiveConsumers.assetConsumer.consume(entry.getKey(), entry.getValue());
}
}
for (Map.Entry<DataKey, DataAsset> entry : transitive.iterateAssetEntries()) {
// If the primary is considered to be intentional (usually at the binary level),
// skip.
if (allowPrimaryOverrideAll && parsedPrimary.containsAsset(entry.getKey())) {
continue;
}
// If a transitive value is in the direct map report a conflict, as it is commonly
// unintentional.
if (direct.containsAsset(entry.getKey())) {
conflicts.add(direct.foundAssetConflict(entry.getKey(), entry.getValue()));
} else if (parsedPrimary.containsAsset(entry.getKey())) {
// If overwriting a transitive value with a primary map, assume it's an unintentional
// override, unless allowPrimaryOverrideAll is set. At which point, this code path
// should not be reached.
conflicts.add(parsedPrimary.foundAssetConflict(entry.getKey(), entry.getValue()));
} else {
// If it's in none of the of sources, add it.
transitiveConsumers.assetConsumer.consume(entry.getKey(), entry.getValue());
}
}
if (!conflicts.isEmpty()) {
List<String> messages = new ArrayList<>();
for (MergeConflict conflict : conflicts) {
if (conflict.isValidWith(deDuplicator)) {
messages.add(conflict.toConflictMessage());
}
}
if (!messages.isEmpty()) {
// TODO(corysmith): Turn these into errors.
logger.warning(Joiner.on("").join(messages));
}
}
return UnwrittenMergedAndroidData.of(
primaryManifest, primaryBuilder.build(), transitiveBuilder.build());
} catch (IOException e) {
throw MergingException.wrapException(e).build();
}
}
}