| // Copyright 2014 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.skyframe; |
| |
| import com.google.common.base.Function; |
| import com.google.common.base.Preconditions; |
| import com.google.common.base.Predicate; |
| import com.google.common.base.Predicates; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.Iterables; |
| import com.google.devtools.build.lib.bazel.bzlmod.ModuleExtensionId; |
| import com.google.devtools.build.lib.cmdline.Label; |
| import com.google.devtools.build.lib.cmdline.PackageIdentifier; |
| import com.google.devtools.build.lib.events.Event; |
| import com.google.devtools.build.lib.events.ExtendedEventHandler; |
| import com.google.devtools.build.lib.packages.WorkspaceFileValue; |
| import com.google.devtools.build.lib.repository.RequestRepositoryInformationEvent; |
| import com.google.devtools.build.lib.rules.repository.RepositoryDirectoryValue; |
| import com.google.devtools.build.skyframe.CycleInfo; |
| import com.google.devtools.build.skyframe.CyclesReporter; |
| import com.google.devtools.build.skyframe.SkyKey; |
| |
| /** Reports cycles of recursive import of Starlark files. */ |
| public class BzlLoadCycleReporter implements CyclesReporter.SingleCycleReporter { |
| |
| private static final Predicate<SkyKey> IS_PACKAGE_SKY_KEY = |
| SkyFunctions.isSkyFunction(SkyFunctions.PACKAGE); |
| |
| private static final Predicate<SkyKey> IS_PACKAGE_LOOKUP = |
| SkyFunctions.isSkyFunction(SkyFunctions.PACKAGE_LOOKUP); |
| |
| private static final Predicate<SkyKey> IS_WORKSPACE_FILE = |
| SkyFunctions.isSkyFunction(WorkspaceFileValue.WORKSPACE_FILE); |
| |
| private static final Predicate<SkyKey> IS_REPOSITORY_DIRECTORY = |
| SkyFunctions.isSkyFunction(SkyFunctions.REPOSITORY_DIRECTORY); |
| |
| private static final Predicate<SkyKey> IS_BZL_LOAD = |
| SkyFunctions.isSkyFunction(SkyFunctions.BZL_LOAD); |
| |
| private static final Predicate<SkyKey> IS_BZLMOD_EXTENSION = |
| SkyFunctions.isSkyFunction(SkyFunctions.SINGLE_EXTENSION_EVAL); |
| |
| private static void requestRepoDefinitions( |
| ExtendedEventHandler eventHandler, Iterable<SkyKey> repos) { |
| for (SkyKey repo : repos) { |
| if (repo instanceof RepositoryDirectoryValue.Key) { |
| eventHandler.post( |
| new RequestRepositoryInformationEvent( |
| ((RepositoryDirectoryValue.Key) repo).argument().getName())); |
| } |
| } |
| } |
| |
| @Override |
| public boolean maybeReportCycle( |
| SkyKey topLevelKey, |
| CycleInfo cycleInfo, |
| boolean alreadyReported, |
| ExtendedEventHandler eventHandler) { |
| ImmutableList<SkyKey> pathToCycle = cycleInfo.getPathToCycle(); |
| ImmutableList<SkyKey> cycle = cycleInfo.getCycle(); |
| if (pathToCycle.isEmpty()) { |
| return false; |
| } |
| SkyKey lastPathElement = pathToCycle.get(pathToCycle.size() - 1); |
| if (alreadyReported) { |
| return true; |
| } |
| |
| if (Iterables.all(cycle, IS_BZL_LOAD) |
| // The last element before the cycle has to be a PackageFunction, a .bzl file, the |
| // WORKSPACE, or an extension eval. |
| && (IS_PACKAGE_SKY_KEY.apply(lastPathElement) |
| || IS_BZL_LOAD.apply(lastPathElement) |
| || IS_WORKSPACE_FILE.apply(lastPathElement) |
| || IS_BZLMOD_EXTENSION.apply(lastPathElement))) { |
| |
| Function<SkyKey, String> printer = |
| input -> { |
| if (input.argument() instanceof BzlLoadValue.Key) { |
| return ((BzlLoadValue.Key) input.argument()).getLabel().toString(); |
| } |
| if (input.argument() instanceof PackageIdentifier) { |
| return input.argument() + "/BUILD"; |
| } |
| if (input.argument() instanceof WorkspaceFileValue.WorkspaceFileKey) { |
| return ((WorkspaceFileValue.WorkspaceFileKey) input.argument()) |
| .getPath() |
| .getRootRelativePath() |
| .toString(); |
| } |
| Preconditions.checkArgument(input.argument() instanceof ModuleExtensionId); |
| ModuleExtensionId id = (ModuleExtensionId) input.argument(); |
| return String.format( |
| "extension '%s' defined in %s", |
| id.getExtensionName(), id.getBzlFileLabel().getCanonicalForm()); |
| }; |
| |
| StringBuilder cycleMessage = |
| new StringBuilder().append("cycle detected in extension files: "); |
| |
| // go back the path that lead to the cycle till we found the BUILD file, WORKSPACE |
| // file or extension eval that lead to the circular load, |
| int startIndex = pathToCycle.size() - 1; |
| while (startIndex > 0 |
| && (IS_PACKAGE_SKY_KEY.apply(pathToCycle.get(startIndex - 1)) |
| || IS_BZL_LOAD.apply(pathToCycle.get(startIndex - 1)) |
| || IS_WORKSPACE_FILE.apply(pathToCycle.get(startIndex - 1)) |
| || IS_BZLMOD_EXTENSION.apply(pathToCycle.get(startIndex - 1)))) { |
| |
| startIndex--; |
| } |
| for (int i = startIndex; i < pathToCycle.size(); i++) { |
| cycleMessage.append("\n ").append(printer.apply(pathToCycle.get(i))); |
| } |
| AbstractLabelCycleReporter.printCycle(cycleInfo.getCycle(), cycleMessage, printer); |
| // TODO(bazel-team): it would be nice to pass the Location of the load Statement in the |
| // BUILD file. |
| eventHandler.handle(Event.error(null, cycleMessage.toString())); |
| return true; |
| } else if (Iterables.all(cycle, Predicates.or(IS_PACKAGE_LOOKUP, IS_REPOSITORY_DIRECTORY))) { |
| StringBuilder cycleMessage = |
| new StringBuilder().append("Circular definition of repositories:"); |
| Iterable<SkyKey> repos = Iterables.filter(cycle, IS_REPOSITORY_DIRECTORY); |
| Function<SkyKey, String> printer = |
| input -> { |
| if (input instanceof RepositoryDirectoryValue.Key) { |
| return ((RepositoryDirectoryValue.Key) input).argument().getNameWithAt(); |
| } else { |
| throw new UnsupportedOperationException(); |
| } |
| }; |
| AbstractLabelCycleReporter.printCycle(ImmutableList.copyOf(repos), cycleMessage, printer); |
| eventHandler.handle(Event.error(null, cycleMessage.toString())); |
| // To help debugging, request that the information be printed about where the respective |
| // repositories were defined. |
| requestRepoDefinitions(eventHandler, repos); |
| return true; |
| } else if (Iterables.any(cycle, IS_REPOSITORY_DIRECTORY) |
| && Iterables.any(cycle, IS_WORKSPACE_FILE)) { |
| Iterable<SkyKey> repos = |
| Iterables.filter(Iterables.concat(pathToCycle, cycle), IS_REPOSITORY_DIRECTORY); |
| |
| StringBuilder message = new StringBuilder(); |
| |
| if (Iterables.any(cycle, IS_BZL_LOAD)) { |
| Label fileLabel = |
| ((BzlLoadValue.Key) Iterables.getLast(Iterables.filter(cycle, IS_BZL_LOAD))).getLabel(); |
| message.append("Failed to load Starlark extension '").append(fileLabel).append("'.\n"); |
| } |
| |
| message |
| .append("Cycle in the workspace file detected. ") |
| .append("This indicates that a repository is used prior to being defined.\n") |
| .append( |
| "The following chain of repository dependencies lead to the missing definition.\n"); |
| for (SkyKey repo : repos) { |
| if (repo instanceof RepositoryDirectoryValue.Key) { |
| message |
| .append(" - ") |
| .append(((RepositoryDirectoryValue.Key) repo).argument().getNameWithAt()) |
| .append("\n"); |
| } |
| } |
| SkyKey missingRepo = Iterables.getLast(repos); |
| if (missingRepo instanceof RepositoryDirectoryValue.Key) { |
| message |
| .append("This could either mean you have to add the '") |
| .append(((RepositoryDirectoryValue.Key) missingRepo).argument().getNameWithAt()) |
| .append("' repository with a statement like `http_archive` in your WORKSPACE file") |
| .append(" (note that transitive dependencies are not added automatically), or move") |
| .append(" an existing definition earlier in your WORKSPACE file."); |
| } |
| eventHandler.handle(Event.error(message.toString())); |
| // To help debugging, request that the information be printed about where the respective |
| // repositories were defined. |
| requestRepoDefinitions(eventHandler, repos); |
| return true; |
| } |
| return false; |
| } |
| } |