// 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.ImmutableCollection; 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.Constants; 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.PackageIdentifier; 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.EvalException; import com.google.devtools.build.lib.syntax.Label; import com.google.devtools.build.lib.syntax.ParserInputSource; import com.google.devtools.build.lib.syntax.SkylarkEnvironment; 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 packageFunctionCache; private final AtomicBoolean showLoadingProgress; private final AtomicInteger numPackagesLoaded; private final Profiler profiler = Profiler.instance(); private static final PathFragment PRELUDE_FILE_FRAGMENT = new PathFragment(Constants.PRELUDE_FILE_DEPOT_RELATIVE_PATH); static final String DEFAULTS_PACKAGE_NAME = "tools/defaults"; public static final String EXTERNAL_PACKAGE_NAME = "external"; static { Preconditions.checkArgument(!PRELUDE_FILE_FRAGMENT.isAbsolute()); } public PackageFunction(Reporter reporter, PackageFactory packageFactory, CachingPackageLocator pkgLocator, AtomicBoolean showLoadingProgress, Cache packageFunctionCache, AtomicInteger numPackagesLoaded) { this.reporter = reporter; this.packageFactory = packageFactory; this.packageLocator = pkgLocator; this.showLoadingProgress = showLoadingProgress; this.packageFunctionCache = packageFunctionCache; 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. * *

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, Boolean> getPackageLookupDepsAndPropagateInconsistentFilesystemExceptions( PackageIdentifier packageIdentifier, Iterable depKeys, Environment env, boolean packageWasInError) throws InternalInconsistentFilesystemException { Preconditions.checkState( Iterables.all(depKeys, SkyFunctions.isSkyFunction(SkyFunctions.PACKAGE_LOOKUP)), depKeys); boolean packageShouldBeInError = packageWasInError; ImmutableMap.Builder builder = ImmutableMap.builder(); for (Map.Entry> entry : env.getValuesOrThrow(depKeys, BuildFileNotFoundException.class, InconsistentFilesystemException.class, FileSymlinkCycleException.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 (FileSymlinkCycleException e) { // Legacy doesn't detect symlink cycles. packageShouldBeInError = true; } } return Pair.of(builder.build(), packageShouldBeInError); } private static boolean markFileDepsAndPropagateInconsistentFilesystemExceptions( PackageIdentifier packageIdentifier, Iterable depKeys, Environment env, boolean packageWasInError) throws InternalInconsistentFilesystemException { Preconditions.checkState( Iterables.all(depKeys, SkyFunctions.isSkyFunction(SkyFunctions.FILE)), depKeys); boolean packageShouldBeInError = packageWasInError; for (Map.Entry> entry : env.getValuesOrThrow(depKeys, IOException.class, FileSymlinkCycleException.class, InconsistentFilesystemException.class).entrySet()) { try { entry.getValue().get(); } catch (IOException e) { maybeThrowFilesystemInconsistency(packageIdentifier, e, packageWasInError); } catch (FileSymlinkCycleException 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 depKeys, Environment env, boolean packageWasInError) throws InternalInconsistentFilesystemException { Preconditions.checkState( Iterables.all(depKeys, SkyFunctions.isSkyFunction(SkyFunctions.GLOB)), depKeys); boolean packageShouldBeInError = packageWasInError; for (Map.Entry> entry : env.getValuesOrThrow(depKeys, IOException.class, BuildFileNotFoundException.class, FileSymlinkCycleException.class, InconsistentFilesystemException.class).entrySet()) { try { entry.getValue().get(); } catch (IOException | BuildFileNotFoundException e) { maybeThrowFilesystemInconsistency(packageIdentifier, e, packageWasInError); } catch (FileSymlinkCycleException 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. * *

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> globPatterns, Map 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 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.getPackageFragment())); } Pair, Boolean> subincludePackageLookupResult = getPackageLookupDepsAndPropagateInconsistentFilesystemExceptions( pkg.getPackageIdentifier(), subincludePackageLookupDepKeys, env, packageWasOriginallyInError); Map subincludePackageLookupDeps = subincludePackageLookupResult.getFirst(); packageShouldBeInError |= subincludePackageLookupResult.getSecond(); List subincludeFileDepKeys = Lists.newArrayList(); for (Entry 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 globDepKeys = Lists.newArrayList(); for (Pair 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, FileSymlinkCycleException.class, InconsistentFilesystemException.class, EvalException.class); } catch (IOException | FileSymlinkCycleException | 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, FileSymlinkCycleException.class, InconsistentFilesystemException.class); } catch (IOException | FileSymlinkCycleException | 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 = null; try { astLookupKey = ASTFileLookupValue.key(PRELUDE_FILE_FRAGMENT); } catch (ASTLookupInputException e) { // There's a static check ensuring that PRELUDE_FILE_FRAGMENT is relative. throw new IllegalStateException(e); } 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 preludeStatements = astLookupValue.getAST() == null ? ImmutableList.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); } SkylarkImportResult importResult = fetchImportsFromBuildFile(buildFilePath, buildFileFragment, packageId, preludeStatements, inputSource, env); if (importResult == null) { return null; } Package.LegacyBuilder legacyPkgBuilder = loadPackage(externalPkg, inputSource, replacementContents, packageId, buildFilePath, defaultVisibility, preludeStatements, importResult); 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> globPatterns = legacyPkgBuilder.getGlobPatterns(); Map 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); } private SkylarkImportResult fetchImportsFromBuildFile(Path buildFilePath, PathFragment buildFileFragment, PackageIdentifier packageIdentifier, List preludeStatements, ParserInputSource inputSource, Environment env) throws PackageFunctionException { StoredEventHandler eventHandler = new StoredEventHandler(); BuildFileAST buildFileAST = BuildFileAST.parseBuildFile( inputSource, preludeStatements, eventHandler, null, true); if (eventHandler.hasErrors()) { // In case of Python preprocessing, errors have already been reported (see checkSyntax). // In other cases, errors will be reported later. // TODO(bazel-team): maybe we could get rid of checkSyntax and always report errors here? return new SkylarkImportResult( ImmutableMap.of(), ImmutableList.