blob: c67da3cdd8eecec21e7854ae880d9474174f839d [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.idea.blaze.android.sync.importer;
import com.google.common.collect.*;
import com.google.idea.blaze.android.sync.importer.aggregators.TransitiveResourceMap;
import com.google.idea.blaze.android.sync.model.AndroidResourceModule;
import com.google.idea.blaze.android.sync.model.BlazeAndroidImportResult;
import com.google.idea.blaze.base.experiments.BoolExperiment;
import com.google.idea.blaze.base.ideinfo.AndroidRuleIdeInfo;
import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
import com.google.idea.blaze.base.ideinfo.LibraryArtifact;
import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
import com.google.idea.blaze.base.model.primitives.Kind;
import com.google.idea.blaze.base.model.primitives.Label;
import com.google.idea.blaze.base.model.primitives.LanguageClass;
import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
import com.google.idea.blaze.base.projectview.ProjectViewSet;
import com.google.idea.blaze.base.scope.BlazeContext;
import com.google.idea.blaze.base.scope.output.IssueOutput;
import com.google.idea.blaze.base.scope.output.PerformanceWarning;
import com.google.idea.blaze.base.sync.projectview.ProjectViewRuleImportFilter;
import com.google.idea.blaze.java.sync.model.BlazeLibrary;
import com.google.idea.blaze.java.sync.model.LibraryKey;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Builds a BlazeWorkspace.
*/
public final class BlazeAndroidWorkspaceImporter {
private static final Logger LOG = Logger.getInstance(BlazeAndroidWorkspaceImporter.class);
private static final BoolExperiment DISCARD_ANDROID_BINARY_RESOURCE_JAR = new BoolExperiment("discard.android.binary.resource.jar", true);
private final Project project;
private final BlazeContext context;
private final WorkspaceRoot workspaceRoot;
private final ImmutableMap<Label, RuleIdeInfo> ruleMap;
private final ProjectViewRuleImportFilter importFilter;
private final boolean discardAndroidBinaryResourceJar;
public BlazeAndroidWorkspaceImporter(
Project project,
BlazeContext context,
WorkspaceRoot workspaceRoot,
ProjectViewSet projectViewSet,
ImmutableMap<Label, RuleIdeInfo> ruleMap) {
this.project = project;
this.context = context;
this.workspaceRoot = workspaceRoot;
this.ruleMap = ruleMap;
this.importFilter = new ProjectViewRuleImportFilter(project, workspaceRoot, projectViewSet);
this.discardAndroidBinaryResourceJar = DISCARD_ANDROID_BINARY_RESOURCE_JAR.getValue();
}
public BlazeAndroidImportResult importWorkspace() {
List<RuleIdeInfo> rules = ruleMap.values()
.stream()
.filter(rule -> rule.kind.getLanguageClass() == LanguageClass.ANDROID)
.filter(importFilter::isSourceRule)
.filter(rule -> !importFilter.excludeTarget(rule))
.collect(Collectors.toList());
TransitiveResourceMap transitiveResourceMap = new TransitiveResourceMap(ruleMap);
WorkspaceBuilder workspaceBuilder = new WorkspaceBuilder();
for (RuleIdeInfo rule : rules) {
addRule(
workspaceBuilder,
transitiveResourceMap,
rule
);
}
ImmutableList<AndroidResourceModule> androidResourceModules = buildAndroidResourceModules(workspaceBuilder);
BlazeLibrary resourceLibrary = createResourceLibrary(androidResourceModules);
if (resourceLibrary != null) {
workspaceBuilder.libraries.add(resourceLibrary);
}
return new BlazeAndroidImportResult(
androidResourceModules,
workspaceBuilder.libraries.build()
);
}
private void addRule(
WorkspaceBuilder workspaceBuilder,
TransitiveResourceMap transitiveResourceMap,
RuleIdeInfo rule) {
AndroidRuleIdeInfo androidRuleIdeInfo = rule.androidRuleIdeInfo;
if (androidRuleIdeInfo != null) {
// Generate an android resource module if this rule defines resources
// We don't want to generate one if this depends on a legacy resource rule through :resources
// In this case, the resource information is redundantly forwarded to this class for
// backwards compatibility, but the android_resource rule itself is already generating
// the android resource module
if (androidRuleIdeInfo.generateResourceClass && androidRuleIdeInfo.legacyResources == null) {
List<ArtifactLocation> nonGeneratedResources = Lists.newArrayList();
for (ArtifactLocation artifactLocation : androidRuleIdeInfo.resources) {
if (!artifactLocation.isGenerated()) {
nonGeneratedResources.add(artifactLocation);
}
}
// Only create a resource module if there are any non-generated resources
// Empty R classes or ones with only generated sources are added as jars
if (!nonGeneratedResources.isEmpty()) {
AndroidResourceModule.Builder builder = new AndroidResourceModule.Builder(rule.label);
workspaceBuilder.androidResourceModules.add(builder);
builder.addAllResources(nonGeneratedResources);
TransitiveResourceMap.TransitiveResourceInfo transitiveResourceInfo = transitiveResourceMap.get(rule.label);
for (ArtifactLocation artifactLocation : transitiveResourceInfo.transitiveResources) {
if (!artifactLocation.isGenerated()) {
builder.addTransitiveResource(artifactLocation);
}
}
for (Label resourceDependency : transitiveResourceInfo.transitiveResourceRules) {
if (!resourceDependency.equals(rule.label)) {
builder.addTransitiveResourceDependency(resourceDependency);
}
}
} else {
// Add blaze's output unless it's a top level rule. In these cases the resource jar contains the entire
// transitive closure of R classes. It's unlikely this is wanted to resolve in the IDE.
boolean discardResourceJar = discardAndroidBinaryResourceJar && rule.kindIsOneOf(Kind.ANDROID_BINARY, Kind.ANDROID_TEST);
if (!discardResourceJar) {
LibraryArtifact resourceJar = androidRuleIdeInfo.resourceJar;
if (resourceJar != null) {
BlazeLibrary library = new BlazeLibrary(LibraryKey.fromJarFile(resourceJar.jar.getFile()), resourceJar);
workspaceBuilder.libraries.add(library);
}
}
}
}
LibraryArtifact idlJar = androidRuleIdeInfo.idlJar;
if (idlJar != null) {
BlazeLibrary library = new BlazeLibrary(LibraryKey.fromJarFile(idlJar.jar.getFile()), idlJar);
workspaceBuilder.libraries.add(library);
}
}
}
@Nullable
private BlazeLibrary createResourceLibrary(Collection<AndroidResourceModule> androidResourceModules) {
Set<File> result = Sets.newHashSet();
for (AndroidResourceModule androidResourceModule : androidResourceModules) {
result.addAll(androidResourceModule.transitiveResources);
}
for (AndroidResourceModule androidResourceModule : androidResourceModules) {
result.removeAll(androidResourceModule.resources);
}
if (!result.isEmpty()) {
return new BlazeLibrary(LibraryKey.forResourceLibrary(),
ImmutableList.copyOf(result.stream().sorted().collect(Collectors.toList())));
}
return null;
}
@NotNull
private ImmutableList<AndroidResourceModule> buildAndroidResourceModules(WorkspaceBuilder workspaceBuilder) {
// Filter empty resource modules
Stream<AndroidResourceModule> androidResourceModuleStream = workspaceBuilder.androidResourceModules
.stream()
.map(AndroidResourceModule.Builder::build)
.filter(androidResourceModule -> !androidResourceModule.isEmpty())
.filter(androidResourceModule -> !androidResourceModule.resources.isEmpty());
List<AndroidResourceModule> androidResourceModules = androidResourceModuleStream.collect(Collectors.toList());
// Detect, filter, and warn about multiple R classes
Multimap<String, AndroidResourceModule> javaPackageToResourceModule = ArrayListMultimap.create();
for (AndroidResourceModule androidResourceModule : androidResourceModules) {
RuleIdeInfo rule = ruleMap.get(androidResourceModule.label);
AndroidRuleIdeInfo androidRuleIdeInfo = rule.androidRuleIdeInfo;
assert androidRuleIdeInfo != null;
javaPackageToResourceModule.put(androidRuleIdeInfo.resourceJavaPackage, androidResourceModule);
}
List<AndroidResourceModule> result = Lists.newArrayList();
for (String resourceJavaPackage : javaPackageToResourceModule.keySet()) {
Collection<AndroidResourceModule> androidResourceModulesWithJavaPackage = javaPackageToResourceModule.get(resourceJavaPackage);
if (androidResourceModulesWithJavaPackage.size() == 1) {
result.addAll(androidResourceModulesWithJavaPackage);
}
else {
StringBuilder messageBuilder = new StringBuilder();
messageBuilder.append("Multiple R classes generated with the same java package ").append(resourceJavaPackage).append(".R: ");
messageBuilder.append('\n');
for (AndroidResourceModule androidResourceModule : androidResourceModulesWithJavaPackage) {
messageBuilder.append(" ").append(androidResourceModule.label).append('\n');
}
String message = messageBuilder.toString();
context.output(new PerformanceWarning(message));
IssueOutput
.warn(message)
.submit(context);
result.add(selectBestAndroidResourceModule(androidResourceModulesWithJavaPackage));
}
}
Collections.sort(result, (lhs, rhs) -> Label.COMPARATOR.compare(lhs.label, rhs.label));
return ImmutableList.copyOf(result);
}
private AndroidResourceModule selectBestAndroidResourceModule(Collection<AndroidResourceModule> androidResourceModulesWithJavaPackage) {
return androidResourceModulesWithJavaPackage
.stream()
.max((lhs, rhs) -> ComparisonChain.start()
.compare(lhs.resources.size(), rhs.resources.size()) // Most resources wins
.compare(lhs.transitiveResources.size(), rhs.transitiveResources.size()) // Most transitive resources wins
.compare(rhs.label.toString().length(), lhs.label.toString().length()) // Shortest label wins - note lhs, rhs are flipped
.result())
.get();
}
static class WorkspaceBuilder {
List<AndroidResourceModule.Builder> androidResourceModules = Lists.newArrayList();
ImmutableList.Builder<BlazeLibrary> libraries = ImmutableList.builder();
}
}