blob: 254b5c89023ec46f038b76c5ec62d3708c5f3697 [file] [log] [blame]
// Copyright 2022 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.bazel.bzlmod.modcommand;
import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Sets;
import com.google.devtools.build.lib.bazel.bzlmod.BazelModuleInspectorValue.AugmentedModule;
import com.google.devtools.build.lib.bazel.bzlmod.ModuleExtensionId;
import com.google.devtools.build.lib.bazel.bzlmod.ModuleKey;
import com.google.devtools.build.lib.bazel.bzlmod.Version;
import com.google.devtools.build.lib.bazel.bzlmod.modcommand.ModExecutor.ResultNode;
import com.google.devtools.build.lib.bazel.bzlmod.modcommand.ModExecutor.ResultNode.IsIndirect;
import com.google.devtools.build.lib.bazel.bzlmod.modcommand.ModExecutor.ResultNode.NodeMetadata;
import com.google.devtools.build.lib.bazel.bzlmod.modcommand.ModOptions.ExtensionShow;
import com.google.devtools.build.lib.bazel.bzlmod.modcommand.OutputFormatters.OutputFormatter;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashSet;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
/**
* Outputs graph-based results of {@link ModExecutor} in the Graphviz <i>dot</i> format which can be
* further pipelined to create an image graph visualization.
*/
public class GraphvizOutputFormatter extends OutputFormatter {
private StringBuilder str;
@Override
public void output() {
str = new StringBuilder();
str.append("digraph mygraph {\n")
.append(" ")
.append("node [ shape=box ]\n")
.append(" ")
.append("edge [ fontsize=8 ]\n");
Set<ModuleKey> seen = new HashSet<>();
Set<ModuleExtensionId> seenExtensions = new HashSet<>();
Deque<ModuleKey> toVisit = new ArrayDeque<>();
seen.add(ModuleKey.ROOT);
toVisit.add(ModuleKey.ROOT);
while (!toVisit.isEmpty()) {
ModuleKey key = toVisit.pop();
AugmentedModule module = Objects.requireNonNull(depGraph.get(key));
ResultNode node = Objects.requireNonNull(result.get(key));
String sourceId = toId(key);
if (key.equals(ModuleKey.ROOT)) {
String rootLabel = String.format("<root> (%s@%s)", module.getName(), module.getVersion());
str.append(String.format(" \"<root>\" [ label=\"%s\" ]\n", rootLabel));
} else if (node.isTarget() || !module.isUsed()) {
String shapeString = node.isTarget() ? "diamond" : "box";
String styleString = module.isUsed() ? "solid" : "dotted";
str.append(
String.format(" %s [ shape=%s style=%s ]\n", toId(key), shapeString, styleString));
}
if (options.extensionInfo != ExtensionShow.HIDDEN) {
ImmutableSortedSet<ModuleExtensionId> extensionsUsed =
extensionRepoImports.keySet().stream()
.filter(e -> extensionRepoImports.get(e).inverse().containsKey(key))
.collect(toImmutableSortedSet(ModuleExtensionId.LEXICOGRAPHIC_COMPARATOR));
for (ModuleExtensionId extensionId : extensionsUsed) {
if (options.extensionInfo == ExtensionShow.USAGES) {
str.append(String.format(" %s -> \"%s\"\n", toId(key), toId(extensionId)));
continue;
}
if (seenExtensions.add(extensionId)) {
printExtension(extensionId);
}
ImmutableSortedSet<String> repoImports =
ImmutableSortedSet.copyOf(extensionRepoImports.get(extensionId).inverse().get(key));
for (String repo : repoImports) {
str.append(String.format(" %s -> %s\n", toId(key), toId(extensionId, repo)));
}
}
}
for (Entry<ModuleKey, NodeMetadata> e : node.getChildrenSortedByKey()) {
ModuleKey childKey = e.getKey();
IsIndirect childIndirect = e.getValue().isIndirect();
String childId = toId(childKey);
if (childIndirect == IsIndirect.FALSE) {
String reasonLabel = getReasonLabel(childKey, key);
str.append(String.format(" %s -> %s [ %s ]\n", sourceId, childId, reasonLabel));
} else {
str.append(String.format(" %s -> %s [ style=dashed ]\n", sourceId, childId));
}
if (seen.add(childKey)) {
toVisit.add(childKey);
}
}
}
str.append("}");
printer.println(str);
printer.flush();
}
private String toId(ModuleKey key) {
if (key.equals(ModuleKey.ROOT)) {
return "\"<root>\"";
}
return String.format(
"\"%s@%s\"",
key.getName(), key.getVersion().equals(Version.EMPTY) ? "_" : key.getVersion());
}
private String toId(ModuleExtensionId id) {
return id.asTargetString();
}
private String toId(ModuleExtensionId id, String repo) {
return String.format("\"%s%%%s\"", toId(id), repo);
}
private void printExtension(ModuleExtensionId id) {
str.append(String.format(" subgraph \"cluster_%s\" {\n", toId(id)));
str.append(String.format(" label=\"%s\"\n", toId(id)));
if (options.extensionInfo == ExtensionShow.USAGES) {
return;
}
ImmutableSortedSet<String> usedRepos =
ImmutableSortedSet.copyOf(extensionRepoImports.get(id).keySet());
for (String repo : usedRepos) {
str.append(String.format(" %s [ label=\"%s\" ]\n", toId(id, repo), repo));
}
if (options.extensionInfo == ExtensionShow.REPOS) {
return;
}
ImmutableSortedSet<String> unusedRepos =
ImmutableSortedSet.copyOf(Sets.difference(extensionRepos.get(id), usedRepos));
for (String repo : unusedRepos) {
str.append(String.format(" %s [ label=\"%s\" style=dotted ]\n", toId(id, repo), repo));
}
str.append(" }\n");
}
private String getReasonLabel(ModuleKey key, ModuleKey parent) {
if (!options.verbose) {
return "";
}
Explanation explanation = getExtraResolutionExplanation(key, parent);
if (explanation == null) {
return "";
}
String label = explanation.getResolutionReason().getLabel();
if (!label.isEmpty()) {
return String.format("label=%s", label);
}
return "";
}
}