blob: 12eea02a7dc3b944ade18c3b790ee8ef7c72c2ec [file] [log] [blame]
// Copyright 2014 Google Inc. 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.Preconditions;
import com.google.common.cache.Cache;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.devtools.build.lib.cmdline.PackageIdentifier;
import com.google.devtools.build.lib.cmdline.PackageIdentifier.RepositoryName;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.EventHandler;
import com.google.devtools.build.lib.events.Location;
import com.google.devtools.build.lib.events.Reporter;
import com.google.devtools.build.lib.events.StoredEventHandler;
import com.google.devtools.build.lib.packages.BuildFileContainsErrorsException;
import com.google.devtools.build.lib.packages.BuildFileNotFoundException;
import com.google.devtools.build.lib.packages.CachingPackageLocator;
import com.google.devtools.build.lib.packages.ExternalPackage;
import com.google.devtools.build.lib.packages.InvalidPackageNameException;
import com.google.devtools.build.lib.packages.NoSuchPackageException;
import com.google.devtools.build.lib.packages.Package;
import com.google.devtools.build.lib.packages.PackageFactory;
import com.google.devtools.build.lib.packages.PackageFactory.Globber;
import com.google.devtools.build.lib.packages.Preprocessor;
import com.google.devtools.build.lib.packages.RuleVisibility;
import com.google.devtools.build.lib.packages.Target;
import com.google.devtools.build.lib.profiler.Profiler;
import com.google.devtools.build.lib.profiler.ProfilerTask;
import com.google.devtools.build.lib.skyframe.ASTFileLookupValue.ASTLookupInputException;
import com.google.devtools.build.lib.skyframe.GlobValue.InvalidGlobPatternException;
import com.google.devtools.build.lib.skyframe.SkylarkImportLookupFunction.SkylarkImportFailedException;
import com.google.devtools.build.lib.syntax.BuildFileAST;
import com.google.devtools.build.lib.syntax.Environment.Extension;
import com.google.devtools.build.lib.syntax.EvalException;
import com.google.devtools.build.lib.syntax.Label;
import com.google.devtools.build.lib.syntax.ParserInputSource;
import com.google.devtools.build.lib.syntax.Statement;
import com.google.devtools.build.lib.util.Pair;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.lib.vfs.RootedPath;
import com.google.devtools.build.skyframe.SkyFunction;
import com.google.devtools.build.skyframe.SkyFunctionException;
import com.google.devtools.build.skyframe.SkyFunctionException.Transience;
import com.google.devtools.build.skyframe.SkyKey;
import com.google.devtools.build.skyframe.SkyValue;
import com.google.devtools.build.skyframe.ValueOrException3;
import com.google.devtools.build.skyframe.ValueOrException4;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Nullable;
/**
* A SkyFunction for {@link PackageValue}s.
*/
public class PackageFunction implements SkyFunction {
private final EventHandler reporter;
private final PackageFactory packageFactory;
private final CachingPackageLocator packageLocator;
private final Cache<PackageIdentifier, Package.LegacyBuilder> packageFunctionCache;
private final Cache<PackageIdentifier, Preprocessor.Result> preprocessCache;
private final AtomicBoolean showLoadingProgress;
private final AtomicInteger numPackagesLoaded;
private final Profiler profiler = Profiler.instance();
private final PathFragment preludePath;
static final String DEFAULTS_PACKAGE_NAME = "tools/defaults";
public static final String EXTERNAL_PACKAGE_NAME = "external";
public PackageFunction(Reporter reporter, PackageFactory packageFactory,
CachingPackageLocator pkgLocator, AtomicBoolean showLoadingProgress,
Cache<PackageIdentifier, Package.LegacyBuilder> packageFunctionCache,
Cache<PackageIdentifier, Preprocessor.Result> preprocessCache,
AtomicInteger numPackagesLoaded) {
this.reporter = reporter;
// Can be null in tests.
this.preludePath = packageFactory == null
? null
: packageFactory.getRuleClassProvider().getPreludePath();
this.packageFactory = packageFactory;
this.packageLocator = pkgLocator;
this.showLoadingProgress = showLoadingProgress;
this.packageFunctionCache = packageFunctionCache;
this.preprocessCache = preprocessCache;
this.numPackagesLoaded = numPackagesLoaded;
}
private static void maybeThrowFilesystemInconsistency(PackageIdentifier packageIdentifier,
Exception skyframeException, boolean packageWasInError)
throws InternalInconsistentFilesystemException {
if (!packageWasInError) {
throw new InternalInconsistentFilesystemException(packageIdentifier, "Encountered error '"
+ skyframeException.getMessage() + "' but didn't encounter it when doing the same thing "
+ "earlier in the build");
}
}
/**
* Marks the given dependencies, and returns those already present. Ignores any exception
* thrown while building the dependency, except for filesystem inconsistencies.
*
* <p>We need to mark dependencies implicitly used by the legacy package loading code, but we
* don't care about any skyframe errors since the package knows whether it's in error or not.
*/
private static Pair<? extends Map<PathFragment, PackageLookupValue>, Boolean>
getPackageLookupDepsAndPropagateInconsistentFilesystemExceptions(
PackageIdentifier packageIdentifier,
Iterable<SkyKey> depKeys, Environment env, boolean packageWasInError)
throws InternalInconsistentFilesystemException {
Preconditions.checkState(
Iterables.all(depKeys, SkyFunctions.isSkyFunction(SkyFunctions.PACKAGE_LOOKUP)), depKeys);
boolean packageShouldBeInError = packageWasInError;
ImmutableMap.Builder<PathFragment, PackageLookupValue> builder = ImmutableMap.builder();
for (Map.Entry<SkyKey, ValueOrException3<BuildFileNotFoundException,
InconsistentFilesystemException, FileSymlinkException>> entry :
env.getValuesOrThrow(depKeys, BuildFileNotFoundException.class,
InconsistentFilesystemException.class,
FileSymlinkException.class).entrySet()) {
PathFragment pkgName = ((PackageIdentifier) entry.getKey().argument()).getPackageFragment();
try {
PackageLookupValue value = (PackageLookupValue) entry.getValue().get();
if (value != null) {
builder.put(pkgName, value);
}
} catch (BuildFileNotFoundException e) {
maybeThrowFilesystemInconsistency(packageIdentifier, e, packageWasInError);
} catch (InconsistentFilesystemException e) {
throw new InternalInconsistentFilesystemException(packageIdentifier, e);
} catch (FileSymlinkException e) {
// Legacy doesn't detect symlink cycles.
packageShouldBeInError = true;
}
}
return Pair.of(builder.build(), packageShouldBeInError);
}
private static boolean markFileDepsAndPropagateInconsistentFilesystemExceptions(
PackageIdentifier packageIdentifier, Iterable<SkyKey> depKeys, Environment env,
boolean packageWasInError) throws InternalInconsistentFilesystemException {
Preconditions.checkState(
Iterables.all(depKeys, SkyFunctions.isSkyFunction(SkyFunctions.FILE)), depKeys);
boolean packageShouldBeInError = packageWasInError;
for (Map.Entry<SkyKey, ValueOrException3<IOException, FileSymlinkException,
InconsistentFilesystemException>> entry : env.getValuesOrThrow(depKeys, IOException.class,
FileSymlinkException.class, InconsistentFilesystemException.class).entrySet()) {
try {
entry.getValue().get();
} catch (IOException e) {
maybeThrowFilesystemInconsistency(packageIdentifier, e, packageWasInError);
} catch (FileSymlinkException e) {
// Legacy doesn't detect symlink cycles.
packageShouldBeInError = true;
} catch (InconsistentFilesystemException e) {
throw new InternalInconsistentFilesystemException(packageIdentifier, e);
}
}
return packageShouldBeInError;
}
private static boolean markGlobDepsAndPropagateInconsistentFilesystemExceptions(
PackageIdentifier packageIdentifier, Iterable<SkyKey> depKeys, Environment env,
boolean packageWasInError) throws InternalInconsistentFilesystemException {
Preconditions.checkState(
Iterables.all(depKeys, SkyFunctions.isSkyFunction(SkyFunctions.GLOB)), depKeys);
boolean packageShouldBeInError = packageWasInError;
for (Map.Entry<SkyKey, ValueOrException4<IOException, BuildFileNotFoundException,
FileSymlinkException, InconsistentFilesystemException>> entry :
env.getValuesOrThrow(depKeys, IOException.class, BuildFileNotFoundException.class,
FileSymlinkException.class, InconsistentFilesystemException.class).entrySet()) {
try {
entry.getValue().get();
} catch (IOException | BuildFileNotFoundException e) {
maybeThrowFilesystemInconsistency(packageIdentifier, e, packageWasInError);
} catch (FileSymlinkException e) {
// Legacy doesn't detect symlink cycles.
packageShouldBeInError = true;
} catch (InconsistentFilesystemException e) {
throw new InternalInconsistentFilesystemException(packageIdentifier, e);
}
}
return packageShouldBeInError;
}
/**
* Marks dependencies implicitly used by legacy package loading code, after the fact. Note that
* the given package might already be in error.
*
* <p>Any skyframe exceptions encountered here are ignored, as similar errors should have
* already been encountered by legacy package loading (if not, then the filesystem is
* inconsistent).
*/
private static boolean markDependenciesAndPropagateInconsistentFilesystemExceptions(
Package pkg, Environment env, Collection<Pair<String, Boolean>> globPatterns,
Map<Label, Path> subincludes) throws InternalInconsistentFilesystemException {
boolean packageWasOriginallyInError = pkg.containsErrors();
boolean packageShouldBeInError = packageWasOriginallyInError;
// TODO(bazel-team): This means that many packages will have to be preprocessed twice. Ouch!
// We need a better continuation mechanism to avoid repeating work. [skyframe-loading]
// TODO(bazel-team): It would be preferable to perform I/O from the package preprocessor via
// Skyframe rather than add (potentially incomplete) dependencies after the fact.
// [skyframe-loading]
Set<SkyKey> subincludePackageLookupDepKeys = Sets.newHashSet();
for (Label label : pkg.getSubincludeLabels()) {
// Declare a dependency on the package lookup for the package giving access to the label.
subincludePackageLookupDepKeys.add(PackageLookupValue.key(label.getPackageIdentifier()));
}
Pair<? extends Map<PathFragment, PackageLookupValue>, Boolean> subincludePackageLookupResult =
getPackageLookupDepsAndPropagateInconsistentFilesystemExceptions(
pkg.getPackageIdentifier(), subincludePackageLookupDepKeys, env,
packageWasOriginallyInError);
Map<PathFragment, PackageLookupValue> subincludePackageLookupDeps =
subincludePackageLookupResult.getFirst();
packageShouldBeInError |= subincludePackageLookupResult.getSecond();
List<SkyKey> subincludeFileDepKeys = Lists.newArrayList();
for (Entry<Label, Path> subincludeEntry : subincludes.entrySet()) {
// Ideally, we would have a direct dependency on the target with the given label, but then
// subincluding a file from the same package will cause a dependency cycle, since targets
// depend on their containing packages.
Label label = subincludeEntry.getKey();
PackageLookupValue subincludePackageLookupValue =
subincludePackageLookupDeps.get(label.getPackageFragment());
if (subincludePackageLookupValue != null) {
// Declare a dependency on the actual file that was subincluded.
Path subincludeFilePath = subincludeEntry.getValue();
if (subincludeFilePath != null) {
if (!subincludePackageLookupValue.packageExists()) {
// Legacy blaze puts a non-null path when only when the package does indeed exist.
throw new InternalInconsistentFilesystemException(pkg.getPackageIdentifier(),
String.format("Unexpected package in %s. Was it modified during the build?",
subincludeFilePath));
}
// Sanity check for consistency of Skyframe and legacy blaze.
Path subincludeFilePathSkyframe =
subincludePackageLookupValue.getRoot().getRelative(label.toPathFragment());
if (!subincludeFilePathSkyframe.equals(subincludeFilePath)) {
throw new InternalInconsistentFilesystemException(pkg.getPackageIdentifier(),
String.format("Inconsistent package location for %s: '%s' vs '%s'. "
+ "Was the source tree modified during the build?",
label.getPackageFragment(), subincludeFilePathSkyframe, subincludeFilePath));
}
// The actual file may be under a different package root than the package being
// constructed.
SkyKey subincludeSkyKey =
FileValue.key(RootedPath.toRootedPath(subincludePackageLookupValue.getRoot(),
subincludeFilePath));
subincludeFileDepKeys.add(subincludeSkyKey);
}
}
}
packageShouldBeInError |= markFileDepsAndPropagateInconsistentFilesystemExceptions(
pkg.getPackageIdentifier(), subincludeFileDepKeys, env, packageWasOriginallyInError);
// TODO(bazel-team): In the long term, we want to actually resolve the glob patterns within
// Skyframe. For now, just logging the glob requests provides correct incrementality and
// adequate performance.
PackageIdentifier packageId = pkg.getPackageIdentifier();
List<SkyKey> globDepKeys = Lists.newArrayList();
for (Pair<String, Boolean> globPattern : globPatterns) {
String pattern = globPattern.getFirst();
boolean excludeDirs = globPattern.getSecond();
SkyKey globSkyKey;
try {
globSkyKey = GlobValue.key(packageId, pattern, excludeDirs, PathFragment.EMPTY_FRAGMENT);
} catch (InvalidGlobPatternException e) {
// Globs that make it to pkg.getGlobPatterns() should already be filtered for errors.
throw new IllegalStateException(e);
}
globDepKeys.add(globSkyKey);
}
packageShouldBeInError |= markGlobDepsAndPropagateInconsistentFilesystemExceptions(
pkg.getPackageIdentifier(), globDepKeys, env, packageWasOriginallyInError);
return packageShouldBeInError;
}
/**
* Adds a dependency on the WORKSPACE file, representing it as a special type of package.
* @throws PackageFunctionException if there is an error computing the workspace file or adding
* its rules to the //external package.
*/
private SkyValue getExternalPackage(Environment env, Path packageLookupPath)
throws PackageFunctionException {
RootedPath workspacePath = RootedPath.toRootedPath(
packageLookupPath, new PathFragment("WORKSPACE"));
SkyKey workspaceKey = PackageValue.workspaceKey(workspacePath);
PackageValue workspace = null;
try {
workspace = (PackageValue) env.getValueOrThrow(workspaceKey, IOException.class,
FileSymlinkException.class, InconsistentFilesystemException.class,
EvalException.class);
} catch (IOException | FileSymlinkException | InconsistentFilesystemException
| EvalException e) {
throw new PackageFunctionException(new BadWorkspaceFileException(e.getMessage()),
Transience.PERSISTENT);
}
if (workspace == null) {
return null;
}
Package pkg = workspace.getPackage();
Event.replayEventsOn(env.getListener(), pkg.getEvents());
if (pkg.containsErrors()) {
throw new PackageFunctionException(new BuildFileContainsErrorsException(
ExternalPackage.PACKAGE_IDENTIFIER, "Package 'external' contains errors"),
pkg.containsTemporaryErrors() ? Transience.TRANSIENT : Transience.PERSISTENT);
}
return new PackageValue(pkg);
}
@Override
public SkyValue compute(SkyKey key, Environment env) throws PackageFunctionException,
InterruptedException {
PackageIdentifier packageId = (PackageIdentifier) key.argument();
PathFragment packageNameFragment = packageId.getPackageFragment();
String packageName = packageNameFragment.getPathString();
SkyKey packageLookupKey = PackageLookupValue.key(packageId);
PackageLookupValue packageLookupValue;
try {
packageLookupValue = (PackageLookupValue)
env.getValueOrThrow(packageLookupKey, BuildFileNotFoundException.class,
InconsistentFilesystemException.class);
} catch (BuildFileNotFoundException e) {
throw new PackageFunctionException(e, Transience.PERSISTENT);
} catch (InconsistentFilesystemException e) {
// This error is not transient from the perspective of the PackageFunction.
throw new PackageFunctionException(
new InternalInconsistentFilesystemException(packageId, e), Transience.PERSISTENT);
}
if (packageLookupValue == null) {
return null;
}
if (!packageLookupValue.packageExists()) {
switch (packageLookupValue.getErrorReason()) {
case NO_BUILD_FILE:
case DELETED_PACKAGE:
case NO_EXTERNAL_PACKAGE:
throw new PackageFunctionException(new BuildFileNotFoundException(packageId,
packageLookupValue.getErrorMsg()), Transience.PERSISTENT);
case INVALID_PACKAGE_NAME:
throw new PackageFunctionException(new InvalidPackageNameException(packageId,
packageLookupValue.getErrorMsg()), Transience.PERSISTENT);
default:
// We should never get here.
throw new IllegalStateException();
}
}
if (packageName.equals(EXTERNAL_PACKAGE_NAME)) {
return getExternalPackage(env, packageLookupValue.getRoot());
}
PackageValue externalPackage = (PackageValue) env.getValue(
PackageValue.key(PackageIdentifier.createInDefaultRepo(EXTERNAL_PACKAGE_NAME)));
if (externalPackage == null) {
return null;
}
Package externalPkg = externalPackage.getPackage();
PathFragment buildFileFragment = packageNameFragment.getChild("BUILD");
RootedPath buildFileRootedPath = RootedPath.toRootedPath(packageLookupValue.getRoot(),
buildFileFragment);
FileValue buildFileValue;
try {
buildFileValue = (FileValue) env.getValueOrThrow(FileValue.key(buildFileRootedPath),
IOException.class, FileSymlinkException.class,
InconsistentFilesystemException.class);
} catch (IOException | FileSymlinkException | InconsistentFilesystemException e) {
throw new IllegalStateException("Package lookup succeeded but encountered error when "
+ "getting FileValue for BUILD file directly.", e);
}
if (buildFileValue == null) {
return null;
}
Preconditions.checkState(buildFileValue.exists(),
"Package lookup succeeded but BUILD file doesn't exist");
Path buildFilePath = buildFileRootedPath.asPath();
String replacementContents = null;
if (packageName.equals(DEFAULTS_PACKAGE_NAME)) {
replacementContents = PrecomputedValue.DEFAULTS_PACKAGE_CONTENTS.get(env);
if (replacementContents == null) {
return null;
}
}
RuleVisibility defaultVisibility = PrecomputedValue.DEFAULT_VISIBILITY.get(env);
if (defaultVisibility == null) {
return null;
}
ASTFileLookupValue astLookupValue = null;
SkyKey astLookupKey = ASTFileLookupValue.key(
PackageIdentifier.createInDefaultRepo(preludePath));
try {
astLookupValue = (ASTFileLookupValue) env.getValueOrThrow(astLookupKey,
ErrorReadingSkylarkExtensionException.class, InconsistentFilesystemException.class);
} catch (ErrorReadingSkylarkExtensionException | InconsistentFilesystemException e) {
throw new PackageFunctionException(new BadPreludeFileException(packageId, e.getMessage()),
Transience.PERSISTENT);
}
if (astLookupValue == null) {
return null;
}
List<Statement> preludeStatements = astLookupValue.getAST() == null
? ImmutableList.<Statement>of() : astLookupValue.getAST().getStatements();
// Load the BUILD file AST and handle Skylark dependencies. This way BUILD files are
// only loaded twice if there are unavailable Skylark or package dependencies or an
// IOException occurs. Note that the BUILD files are still parsed two times.
ParserInputSource inputSource;
try {
if (showLoadingProgress.get() && packageFunctionCache.getIfPresent(packageId) == null) {
// TODO(bazel-team): don't duplicate the loading message if there are unavailable
// Skylark dependencies.
reporter.handle(Event.progress("Loading package: " + packageName));
}
inputSource = ParserInputSource.create(buildFilePath);
} catch (IOException e) {
env.getListener().handle(Event.error(Location.fromFile(buildFilePath), e.getMessage()));
// Note that we did this work, so we should conservatively report this error as transient.
throw new PackageFunctionException(new BuildFileContainsErrorsException(
packageId, e.getMessage()), Transience.TRANSIENT);
}
Package.LegacyBuilder legacyPkgBuilder =
loadPackage(
externalPkg,
inputSource,
replacementContents,
packageId,
buildFilePath,
buildFileFragment,
defaultVisibility,
preludeStatements,
env);
if (legacyPkgBuilder == null) {
return null;
}
legacyPkgBuilder.buildPartial();
try {
handleLabelsCrossingSubpackagesAndPropagateInconsistentFilesystemExceptions(
packageLookupValue.getRoot(), packageId, legacyPkgBuilder, env);
} catch (InternalInconsistentFilesystemException e) {
packageFunctionCache.invalidate(packageId);
throw new PackageFunctionException(e,
e.isTransient() ? Transience.TRANSIENT : Transience.PERSISTENT);
}
if (env.valuesMissing()) {
// The package we just loaded will be in the {@code packageFunctionCache} next when this
// SkyFunction is called again.
return null;
}
Collection<Pair<String, Boolean>> globPatterns = legacyPkgBuilder.getGlobPatterns();
Map<Label, Path> subincludes = legacyPkgBuilder.getSubincludes();
Package pkg = legacyPkgBuilder.finishBuild();
Event.replayEventsOn(env.getListener(), pkg.getEvents());
boolean packageShouldBeConsideredInError;
try {
packageShouldBeConsideredInError =
markDependenciesAndPropagateInconsistentFilesystemExceptions(pkg, env,
globPatterns, subincludes);
} catch (InternalInconsistentFilesystemException e) {
packageFunctionCache.invalidate(packageId);
throw new PackageFunctionException(e,
e.isTransient() ? Transience.TRANSIENT : Transience.PERSISTENT);
}
if (env.valuesMissing()) {
return null;
}
// We know this SkyFunction will not be called again, so we can remove the cache entry.
packageFunctionCache.invalidate(packageId);
if (packageShouldBeConsideredInError) {
throw new PackageFunctionException(new BuildFileContainsErrorsException(pkg,
"Package '" + packageName + "' contains errors"),
pkg.containsTemporaryErrors() ? Transience.TRANSIENT : Transience.PERSISTENT);
}
return new PackageValue(pkg);
}
/**
* Returns true if includes referencing a different repository have already been computed.
*/
private boolean fetchIncludeRepositoryDeps(Environment env, BuildFileAST ast) {
boolean ok = true;
for (Label label : ast.getIncludes()) {
if (!label.getPackageIdentifier().getRepository().isDefault()) {
// If this is the default repository, the include refers to the same repository, whose
// RepositoryValue is already a dependency of this PackageValue.
if (env.getValue(RepositoryValue.key(
label.getPackageIdentifier().getRepository())) == null) {
ok = false;
}
}
}
return ok;
}
// TODO(bazel-team): this should take the AST so we don't parse the file twice.
@Nullable
private SkylarkImportResult discoverSkylarkImports(
Path buildFilePath,
PathFragment buildFileFragment,
PackageIdentifier packageId,
Environment env,
ParserInputSource inputSource,
List<Statement> preludeStatements)
throws PackageFunctionException {
StoredEventHandler eventHandler = new StoredEventHandler();
BuildFileAST buildFileAST =
BuildFileAST.parseBuildFile(
inputSource,
preludeStatements,
eventHandler,
/* package locator */ null,
/* parse python */ false);
SkylarkImportResult importResult;
boolean includeRepositoriesFetched;
if (eventHandler.hasErrors()) {
importResult =
new SkylarkImportResult(
ImmutableMap.<PathFragment, Extension>of(),
ImmutableList.<Label>of());
includeRepositoriesFetched = true;
} else {
importResult =
fetchImportsFromBuildFile(buildFilePath, buildFileFragment, packageId, buildFileAST, env);
includeRepositoriesFetched = fetchIncludeRepositoryDeps(env, buildFileAST);
}
if (!includeRepositoriesFetched) {
return null;
}
return importResult;
}
/**
* Fetch the skylark loads for this BUILD file. If any of them haven't been computed yet,
* returns null.
*/
@Nullable
private SkylarkImportResult fetchImportsFromBuildFile(
Path buildFilePath,
PathFragment buildFileFragment,
PackageIdentifier packageId,
BuildFileAST buildFileAST,
Environment env)
throws PackageFunctionException {
ImmutableMap<Location, PathFragment> imports = buildFileAST.getImports();
Map<PathFragment, Extension> importMap = new HashMap<>();
ImmutableList.Builder<SkylarkFileDependency> fileDependencies = ImmutableList.builder();
try {
for (Map.Entry<Location, PathFragment> entry : imports.entrySet()) {
PathFragment importFile = entry.getValue();
// HACK: The prelude sometimes contains load() statements, which need to be resolved
// relative to the prelude file. However, we don't have a good way to tell "this should come
// from the main repository" in a load() statement, and we don't have a good way to tell if
// a load() statement comes from the prelude, since we just prepend those statements before
// the actual BUILD file. So we use this evil .endsWith() statement to figure it out.
RepositoryName repository =
entry.getKey().getPath().endsWith(preludePath)
? PackageIdentifier.DEFAULT_REPOSITORY_NAME : packageId.getRepository();
SkyKey importsLookupKey = SkylarkImportLookupValue.key(
repository, buildFileFragment, importFile);
SkylarkImportLookupValue importLookupValue = (SkylarkImportLookupValue)
env.getValueOrThrow(importsLookupKey, SkylarkImportFailedException.class,
InconsistentFilesystemException.class, ASTLookupInputException.class,
BuildFileNotFoundException.class);
if (importLookupValue != null) {
importMap.put(importFile, importLookupValue.getEnvironmentExtension());
fileDependencies.add(importLookupValue.getDependency());
}
}
} catch (SkylarkImportFailedException e) {
env.getListener().handle(Event.error(Location.fromFile(buildFilePath), e.getMessage()));
throw new PackageFunctionException(
new BuildFileContainsErrorsException(packageId, e.getMessage()), Transience.PERSISTENT);
} catch (InconsistentFilesystemException e) {
throw new PackageFunctionException(
new InternalInconsistentFilesystemException(packageId, e), Transience.PERSISTENT);
} catch (ASTLookupInputException e) {
// The load syntax is bad in the BUILD file so BuildFileContainsErrorsException is OK.
throw new PackageFunctionException(
new BuildFileContainsErrorsException(packageId, e.getMessage()), Transience.PERSISTENT);
} catch (BuildFileNotFoundException e) {
throw new PackageFunctionException(e, Transience.PERSISTENT);
}
if (env.valuesMissing()) {
// There are unavailable Skylark dependencies.
return null;
}
return new SkylarkImportResult(importMap, transitiveClosureOfLabels(fileDependencies.build()));
}
private ImmutableList<Label> transitiveClosureOfLabels(
ImmutableList<SkylarkFileDependency> immediateDeps) {
Set<Label> transitiveClosure = Sets.newHashSet();
transitiveClosureOfLabels(immediateDeps, transitiveClosure);
return ImmutableList.copyOf(transitiveClosure);
}
private void transitiveClosureOfLabels(
ImmutableList<SkylarkFileDependency> immediateDeps, Set<Label> transitiveClosure) {
for (SkylarkFileDependency dep : immediateDeps) {
if (transitiveClosure.add(dep.getLabel())) {
transitiveClosureOfLabels(dep.getDependencies(), transitiveClosure);
}
}
}
@Nullable
@Override
public String extractTag(SkyKey skyKey) {
return null;
}
private static void handleLabelsCrossingSubpackagesAndPropagateInconsistentFilesystemExceptions(
Path pkgRoot, PackageIdentifier pkgId, Package.LegacyBuilder pkgBuilder, Environment env)
throws InternalInconsistentFilesystemException {
Set<SkyKey> containingPkgLookupKeys = Sets.newHashSet();
Map<Target, SkyKey> targetToKey = new HashMap<>();
for (Target target : pkgBuilder.getTargets()) {
PathFragment dir = target.getLabel().toPathFragment().getParentDirectory();
PackageIdentifier dirId = new PackageIdentifier(pkgId.getRepository(), dir);
if (dir.equals(pkgId.getPackageFragment())) {
continue;
}
SkyKey key = ContainingPackageLookupValue.key(dirId);
targetToKey.put(target, key);
containingPkgLookupKeys.add(key);
}
Map<Label, SkyKey> subincludeToKey = new HashMap<>();
for (Label subincludeLabel : pkgBuilder.getSubincludeLabels()) {
PathFragment dir = subincludeLabel.toPathFragment().getParentDirectory();
PackageIdentifier dirId = new PackageIdentifier(pkgId.getRepository(), dir);
if (dir.equals(pkgId.getPackageFragment())) {
continue;
}
SkyKey key = ContainingPackageLookupValue.key(dirId);
subincludeToKey.put(subincludeLabel, key);
containingPkgLookupKeys.add(ContainingPackageLookupValue.key(dirId));
}
Map<SkyKey, ValueOrException3<BuildFileNotFoundException, InconsistentFilesystemException,
FileSymlinkException>> containingPkgLookupValues = env.getValuesOrThrow(
containingPkgLookupKeys, BuildFileNotFoundException.class,
InconsistentFilesystemException.class, FileSymlinkException.class);
if (env.valuesMissing()) {
return;
}
for (Target target : ImmutableSet.copyOf(pkgBuilder.getTargets())) {
SkyKey key = targetToKey.get(target);
if (!containingPkgLookupValues.containsKey(key)) {
continue;
}
ContainingPackageLookupValue containingPackageLookupValue =
getContainingPkgLookupValueAndPropagateInconsistentFilesystemExceptions(
pkgId, containingPkgLookupValues.get(key), env);
if (maybeAddEventAboutLabelCrossingSubpackage(pkgBuilder, pkgRoot, target.getLabel(),
target.getLocation(), containingPackageLookupValue)) {
pkgBuilder.removeTarget(target);
pkgBuilder.setContainsErrors();
}
}
for (Label subincludeLabel : pkgBuilder.getSubincludeLabels()) {
SkyKey key = subincludeToKey.get(subincludeLabel);
if (!containingPkgLookupValues.containsKey(key)) {
continue;
}
ContainingPackageLookupValue containingPackageLookupValue =
getContainingPkgLookupValueAndPropagateInconsistentFilesystemExceptions(
pkgId, containingPkgLookupValues.get(key), env);
if (maybeAddEventAboutLabelCrossingSubpackage(pkgBuilder, pkgRoot, subincludeLabel,
/*location=*/null, containingPackageLookupValue)) {
pkgBuilder.setContainsErrors();
}
}
}
@Nullable
private static ContainingPackageLookupValue
getContainingPkgLookupValueAndPropagateInconsistentFilesystemExceptions(
PackageIdentifier packageIdentifier,
ValueOrException3<BuildFileNotFoundException, InconsistentFilesystemException,
FileSymlinkException> containingPkgLookupValueOrException, Environment env)
throws InternalInconsistentFilesystemException {
try {
return (ContainingPackageLookupValue) containingPkgLookupValueOrException.get();
} catch (BuildFileNotFoundException | FileSymlinkException e) {
env.getListener().handle(Event.error(null, e.getMessage()));
return null;
} catch (InconsistentFilesystemException e) {
throw new InternalInconsistentFilesystemException(packageIdentifier, e);
}
}
private static boolean maybeAddEventAboutLabelCrossingSubpackage(
Package.LegacyBuilder pkgBuilder, Path pkgRoot, Label label, @Nullable Location location,
@Nullable ContainingPackageLookupValue containingPkgLookupValue) {
if (containingPkgLookupValue == null) {
return true;
}
if (!containingPkgLookupValue.hasContainingPackage()) {
// The missing package here is a problem, but it's not an error from the perspective of
// PackageFunction.
return false;
}
PackageIdentifier containingPkg = containingPkgLookupValue.getContainingPackageName();
if (containingPkg.equals(label.getPackageIdentifier())) {
// The label does not cross a subpackage boundary.
return false;
}
if (!containingPkg.getPathFragment().startsWith(
label.getPackageIdentifier().getPathFragment())) {
// This label is referencing an imaginary package, because the containing package should
// extend the label's package: if the label is //a/b:c/d, the containing package could be
// //a/b/c or //a/b, but should never be //a. Usually such errors will be caught earlier, but
// in some exceptional cases (such as a Python-aware BUILD file catching its own io
// exceptions), it reaches here, and we tolerate it.
return false;
}
PathFragment labelNameFragment = new PathFragment(label.getName());
String message = String.format("Label '%s' crosses boundary of subpackage '%s'",
label, containingPkg);
Path containingRoot = containingPkgLookupValue.getContainingPackageRoot();
if (pkgRoot.equals(containingRoot)) {
PathFragment labelNameInContainingPackage = labelNameFragment.subFragment(
containingPkg.getPackageFragment().segmentCount()
- label.getPackageFragment().segmentCount(),
labelNameFragment.segmentCount());
message += " (perhaps you meant to put the colon here: "
+ "'//" + containingPkg + ":" + labelNameInContainingPackage + "'?)";
} else {
message += " (have you deleted " + containingPkg + "/BUILD? "
+ "If so, use the --deleted_packages=" + containingPkg + " option)";
}
pkgBuilder.addEvent(Event.error(location, message));
return true;
}
/**
* Constructs a {@link Package} object for the given package using legacy package loading.
* Note that the returned package may be in error.
*
* <p>May return null if the computation has to be restarted.
*/
@Nullable
private Package.LegacyBuilder loadPackage(
Package externalPkg,
ParserInputSource inputSource,
@Nullable String replacementContents,
PackageIdentifier packageId,
Path buildFilePath,
PathFragment buildFileFragment,
RuleVisibility defaultVisibility,
List<Statement> preludeStatements,
Environment env)
throws InterruptedException, PackageFunctionException {
ParserInputSource replacementSource = replacementContents == null ? null
: ParserInputSource.create(replacementContents, buildFilePath.asFragment());
Package.LegacyBuilder pkgBuilder = packageFunctionCache.getIfPresent(packageId);
if (pkgBuilder == null) {
profiler.startTask(ProfilerTask.CREATE_PACKAGE, packageId.toString());
try {
Globber globber = packageFactory.createLegacyGlobber(buildFilePath.getParentDirectory(),
packageId, packageLocator);
Preprocessor.Result preprocessingResult = preprocessCache.getIfPresent(packageId);
if (preprocessingResult == null) {
preprocessingResult = replacementSource == null
? packageFactory.preprocess(packageId, buildFilePath, inputSource, globber)
: Preprocessor.Result.noPreprocessing(replacementSource);
preprocessCache.put(packageId, preprocessingResult);
}
SkylarkImportResult importResult =
discoverSkylarkImports(
buildFilePath,
buildFileFragment,
packageId,
env,
preprocessingResult.result,
preludeStatements);
if (importResult == null) {
return null;
}
preprocessCache.invalidate(packageId);
pkgBuilder = packageFactory.createPackageFromPreprocessingResult(externalPkg, packageId,
buildFilePath, preprocessingResult, preprocessingResult.events, preludeStatements,
importResult.importMap, importResult.fileDependencies, packageLocator,
defaultVisibility, globber);
numPackagesLoaded.incrementAndGet();
packageFunctionCache.put(packageId, pkgBuilder);
} finally {
profiler.completeTask(ProfilerTask.CREATE_PACKAGE);
}
}
return pkgBuilder;
}
private static class InternalInconsistentFilesystemException extends NoSuchPackageException {
private boolean isTransient;
/**
* Used to represent a filesystem inconsistency discovered outside the
* {@link PackageFunction}.
*/
public InternalInconsistentFilesystemException(PackageIdentifier packageIdentifier,
InconsistentFilesystemException e) {
super(packageIdentifier, e.getMessage(), e);
// This is not a transient error from the perspective of the PackageFunction.
this.isTransient = false;
}
/** Used to represent a filesystem inconsistency discovered by the {@link PackageFunction}. */
public InternalInconsistentFilesystemException(PackageIdentifier packageIdentifier,
String inconsistencyMessage) {
this(packageIdentifier, new InconsistentFilesystemException(inconsistencyMessage));
this.isTransient = true;
}
public boolean isTransient() {
return isTransient;
}
}
private static class BadWorkspaceFileException extends NoSuchPackageException {
private BadWorkspaceFileException(String message) {
super(ExternalPackage.PACKAGE_IDENTIFIER,
"Error encountered while dealing with the WORKSPACE file: " + message);
}
}
private static class BadPreludeFileException extends NoSuchPackageException {
private BadPreludeFileException(PackageIdentifier packageIdentifier, String message) {
super(packageIdentifier, "Error encountered while reading the prelude file: " + message);
}
}
/**
* Used to declare all the exception types that can be wrapped in the exception thrown by
* {@link PackageFunction#compute}.
*/
private static class PackageFunctionException extends SkyFunctionException {
public PackageFunctionException(NoSuchPackageException e, Transience transience) {
super(e, transience);
}
}
/** A simple value class to store the result of the Skylark imports.*/
private static final class SkylarkImportResult {
private final Map<PathFragment, Extension> importMap;
private final ImmutableList<Label> fileDependencies;
private SkylarkImportResult(
Map<PathFragment, Extension> importMap,
ImmutableList<Label> fileDependencies) {
this.importMap = importMap;
this.fileDependencies = fileDependencies;
}
}
}