| // 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 ""; |
| } |
| } |