Update from Google.
--
MOE_MIGRATED_REVID=85702957
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ASTFileLookupFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/ASTFileLookupFunction.java
new file mode 100644
index 0000000..6dcc224
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ASTFileLookupFunction.java
@@ -0,0 +1,177 @@
+// 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.devtools.build.lib.packages.CachingPackageLocator;
+import com.google.devtools.build.lib.packages.RuleClassProvider;
+import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
+import com.google.devtools.build.lib.syntax.BuildFileAST;
+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 java.io.IOException;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.annotation.Nullable;
+
+/**
+ * A SkyFunction for {@link ASTFileLookupValue}s. Tries to locate a file and load it as a
+ * syntax tree and cache the resulting {@link BuildFileAST}. If the file doesn't exist
+ * the function doesn't fail but returns a specific NO_FILE ASTLookupValue.
+ */
+public class ASTFileLookupFunction implements SkyFunction {
+
+ private abstract static class FileLookupResult {
+ /** Returns whether the file lookup was successful. */
+ public abstract boolean lookupSuccessful();
+
+ /** If {@code lookupSuccessful()}, returns the {@link RootedPath} to the file. */
+ public abstract RootedPath rootedPath();
+
+ static FileLookupResult noFile() {
+ return UnsuccessfulFileResult.INSTANCE;
+ }
+
+ static FileLookupResult file(RootedPath rootedPath) {
+ return new SuccessfulFileResult(rootedPath);
+ }
+
+ private static class SuccessfulFileResult extends FileLookupResult {
+ private final RootedPath rootedPath;
+
+ private SuccessfulFileResult(RootedPath rootedPath) {
+ this.rootedPath = rootedPath;
+ }
+
+ @Override
+ public boolean lookupSuccessful() {
+ return true;
+ }
+
+ @Override
+ public RootedPath rootedPath() {
+ return rootedPath;
+ }
+ }
+
+ private static class UnsuccessfulFileResult extends FileLookupResult {
+ private static final UnsuccessfulFileResult INSTANCE = new UnsuccessfulFileResult();
+ private UnsuccessfulFileResult() {
+ }
+
+ @Override
+ public boolean lookupSuccessful() {
+ return false;
+ }
+
+ @Override
+ public RootedPath rootedPath() {
+ throw new IllegalStateException("unsucessful lookup");
+ }
+ }
+ }
+
+ private final AtomicReference<PathPackageLocator> pkgLocator;
+ private final RuleClassProvider ruleClassProvider;
+ private final CachingPackageLocator packageManager;
+
+ public ASTFileLookupFunction(AtomicReference<PathPackageLocator> pkgLocator,
+ CachingPackageLocator packageManager,
+ RuleClassProvider ruleClassProvider) {
+ this.pkgLocator = pkgLocator;
+ this.packageManager = packageManager;
+ this.ruleClassProvider = ruleClassProvider;
+ }
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) throws SkyFunctionException,
+ InterruptedException {
+ PathFragment astFilePathFragment = (PathFragment) skyKey.argument();
+ FileLookupResult lookupResult = getASTFile(env, astFilePathFragment);
+ if (lookupResult == null) {
+ return null;
+ }
+
+ BuildFileAST ast = null;
+ if (!lookupResult.lookupSuccessful()) {
+ // Return the specific NO_FILE ASTLookupValue instance if no file was found.
+ return ASTFileLookupValue.NO_FILE;
+ } else {
+ Path path = lookupResult.rootedPath().asPath();
+ // Skylark files end with bzl.
+ boolean parseAsSkylark = astFilePathFragment.getPathString().endsWith(".bzl");
+ try {
+ ast = parseAsSkylark
+ ? BuildFileAST.parseSkylarkFile(path, env.getListener(),
+ packageManager, ruleClassProvider.getSkylarkValidationEnvironment().clone())
+ : BuildFileAST.parseBuildFile(path, env.getListener(),
+ packageManager, false);
+ } catch (IOException e) {
+ throw new ASTLookupFunctionException(new ErrorReadingSkylarkExtensionException(
+ e.getMessage()), Transience.TRANSIENT);
+ }
+ }
+
+ return new ASTFileLookupValue(ast);
+ }
+
+ private FileLookupResult getASTFile(Environment env, PathFragment astFilePathFragment)
+ throws ASTLookupFunctionException {
+ for (Path packagePathEntry : pkgLocator.get().getPathEntries()) {
+ RootedPath rootedPath = RootedPath.toRootedPath(packagePathEntry, astFilePathFragment);
+ SkyKey fileSkyKey = FileValue.key(rootedPath);
+ FileValue fileValue = null;
+ try {
+ fileValue = (FileValue) env.getValueOrThrow(fileSkyKey, IOException.class,
+ FileSymlinkCycleException.class, InconsistentFilesystemException.class);
+ } catch (IOException | FileSymlinkCycleException e) {
+ throw new ASTLookupFunctionException(new ErrorReadingSkylarkExtensionException(
+ e.getMessage()), Transience.PERSISTENT);
+ } catch (InconsistentFilesystemException e) {
+ throw new ASTLookupFunctionException(e, Transience.PERSISTENT);
+ }
+ if (fileValue == null) {
+ return null;
+ }
+ if (fileValue.isFile()) {
+ return FileLookupResult.file(rootedPath);
+ }
+ }
+ return FileLookupResult.noFile();
+ }
+
+ @Nullable
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+
+ private static final class ASTLookupFunctionException extends SkyFunctionException {
+ private ASTLookupFunctionException(ErrorReadingSkylarkExtensionException e,
+ Transience transience) {
+ super(e, transience);
+ }
+
+ private ASTLookupFunctionException(InconsistentFilesystemException e, Transience transience) {
+ super(e, transience);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ASTFileLookupValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/ASTFileLookupValue.java
new file mode 100644
index 0000000..1061c86
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ASTFileLookupValue.java
@@ -0,0 +1,61 @@
+// 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.devtools.build.lib.syntax.BuildFileAST;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import javax.annotation.Nullable;
+
+/**
+ * A value that represents an AST file lookup result.
+ */
+public class ASTFileLookupValue implements SkyValue {
+
+ static final ASTFileLookupValue NO_FILE = new ASTFileLookupValue(null);
+
+ @Nullable private final BuildFileAST ast;
+
+ public ASTFileLookupValue(@Nullable BuildFileAST ast) {
+ this.ast = ast;
+ }
+
+ /**
+ * Returns the original AST file.
+ */
+ @Nullable public BuildFileAST getAST() {
+ return ast;
+ }
+
+ static void checkInputArgument(PathFragment astFilePathFragment) throws ASTLookupInputException {
+ if (astFilePathFragment.isAbsolute()) {
+ throw new ASTLookupInputException(String.format(
+ "Input file '%s' cannot be an absolute path.", astFilePathFragment));
+ }
+ }
+
+ static SkyKey key(PathFragment astFilePathFragment) throws ASTLookupInputException {
+ checkInputArgument(astFilePathFragment);
+ return new SkyKey(SkyFunctions.AST_FILE_LOOKUP, astFilePathFragment);
+ }
+
+ static final class ASTLookupInputException extends Exception {
+ private ASTLookupInputException(String msg) {
+ super(msg);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/AbstractLabelCycleReporter.java b/src/main/java/com/google/devtools/build/lib/skyframe/AbstractLabelCycleReporter.java
new file mode 100644
index 0000000..797f158
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/AbstractLabelCycleReporter.java
@@ -0,0 +1,130 @@
+// 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.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.packages.NoSuchThingException;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.pkgcache.LoadedPackageProvider;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.skyframe.CycleInfo;
+import com.google.devtools.build.skyframe.CyclesReporter;
+import com.google.devtools.build.skyframe.SkyKey;
+
+/** Reports cycles between skyframe values whose keys contains {@link Label}s. */
+abstract class AbstractLabelCycleReporter implements CyclesReporter.SingleCycleReporter {
+
+ private final LoadedPackageProvider loadedPackageProvider;
+
+ AbstractLabelCycleReporter(LoadedPackageProvider loadedPackageProvider) {
+ this.loadedPackageProvider = loadedPackageProvider;
+ }
+
+ /** Returns the String representation of the {@code SkyKey}. */
+ protected abstract String prettyPrint(SkyKey key);
+
+ /** Returns the associated Label of the SkyKey. */
+ protected abstract Label getLabel(SkyKey key);
+
+ protected abstract boolean canReportCycle(SkyKey topLevelKey, CycleInfo cycleInfo);
+
+ protected String getAdditionalMessageAboutCycle(SkyKey topLevelKey, CycleInfo cycleInfo) {
+ return "";
+ }
+
+ @Override
+ public boolean maybeReportCycle(SkyKey topLevelKey, CycleInfo cycleInfo,
+ boolean alreadyReported, EventHandler eventHandler) {
+ Preconditions.checkNotNull(eventHandler);
+ if (!canReportCycle(topLevelKey, cycleInfo)) {
+ return false;
+ }
+
+ if (alreadyReported) {
+ Label label = getLabel(topLevelKey);
+ Target target = getTargetForLabel(label);
+ eventHandler.handle(Event.error(target.getLocation(),
+ "in " + target.getTargetKind() + " " + label +
+ ": cycle in dependency graph: target depends on an already-reported cycle"));
+ } else {
+ StringBuilder cycleMessage = new StringBuilder("cycle in dependency graph:");
+ ImmutableList<SkyKey> pathToCycle = cycleInfo.getPathToCycle();
+ ImmutableList<SkyKey> cycle = cycleInfo.getCycle();
+ for (SkyKey value : pathToCycle) {
+ cycleMessage.append("\n ");
+ cycleMessage.append(prettyPrint(value));
+ }
+
+ SkyKey cycleValue = printCycle(cycle, cycleMessage, new Function<SkyKey, String>() {
+ @Override
+ public String apply(SkyKey input) {
+ return prettyPrint(input);
+ }
+ });
+
+ cycleMessage.append(getAdditionalMessageAboutCycle(topLevelKey, cycleInfo));
+
+ Label label = getLabel(cycleValue);
+ Target target = getTargetForLabel(label);
+ eventHandler.handle(
+ Event.error(target.getLocation(), "in " + target.getTargetKind() + " " + label
+ + ": " + cycleMessage.toString()));
+ }
+
+ return true;
+ }
+
+ /**
+ * Prints the SkyKey-s in cycle into cycleMessage using the print function.
+ */
+ static SkyKey printCycle(ImmutableList<SkyKey> cycle, StringBuilder cycleMessage,
+ Function<SkyKey, String> printFunction) {
+ Iterable<SkyKey> valuesToPrint = cycle.size() > 1
+ ? Iterables.concat(cycle, ImmutableList.of(cycle.get(0))) : cycle;
+ SkyKey cycleValue = null;
+ for (SkyKey value : valuesToPrint) {
+ if (cycleValue == null) {
+ cycleValue = value;
+ }
+ if (value == cycleValue) {
+ cycleMessage.append("\n * ");
+ } else {
+ cycleMessage.append("\n ");
+ }
+ cycleMessage.append(printFunction.apply(value));
+ }
+
+ if (cycle.size() == 1) {
+ cycleMessage.append(" [self-edge]");
+ }
+
+ return cycleValue;
+ }
+
+ protected final Target getTargetForLabel(Label label) {
+ try {
+ return loadedPackageProvider.getLoadedTarget(label);
+ } catch (NoSuchThingException e) {
+ // This method is used for getting the target from a label in a circular dependency.
+ // If we have a cycle that means that we need to have accessed the target (to get its
+ // dependencies). So all the labels in a dependency cycle need to exist.
+ throw new IllegalStateException(e);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ActionArtifactCycleReporter.java b/src/main/java/com/google/devtools/build/lib/skyframe/ActionArtifactCycleReporter.java
new file mode 100644
index 0000000..3105539
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ActionArtifactCycleReporter.java
@@ -0,0 +1,77 @@
+// 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.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.analysis.LabelAndConfiguration;
+import com.google.devtools.build.lib.pkgcache.LoadedPackageProvider;
+import com.google.devtools.build.lib.skyframe.ArtifactValue.OwnedArtifact;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.skyframe.CycleInfo;
+import com.google.devtools.build.skyframe.SkyFunctionName;
+import com.google.devtools.build.skyframe.SkyKey;
+
+/**
+ * Reports cycles between Actions and Artifacts. These indicates cycles within a rule.
+ */
+public class ActionArtifactCycleReporter extends AbstractLabelCycleReporter {
+
+ private static final Predicate<SkyKey> IS_ARTIFACT_OR_ACTION_SKY_KEY = Predicates.or(
+ SkyFunctions.isSkyFunction(SkyFunctions.ARTIFACT),
+ SkyFunctions.isSkyFunction(SkyFunctions.ACTION_EXECUTION),
+ SkyFunctions.isSkyFunction(SkyFunctions.TARGET_COMPLETION));
+
+ ActionArtifactCycleReporter(LoadedPackageProvider loadedPackageProvider) {
+ super(loadedPackageProvider);
+ }
+
+ @Override
+ protected String prettyPrint(SkyKey key) {
+ return prettyPrint(key.functionName(), key.argument());
+ }
+
+ private String prettyPrint(SkyFunctionName skyFunctionName, Object arg) {
+ if (arg instanceof OwnedArtifact) {
+ return "file: " + ((OwnedArtifact) arg).getArtifact().getRootRelativePathString();
+ } else if (arg instanceof Action) {
+ return "action: " + ((Action) arg).getMnemonic();
+ } else if (arg instanceof LabelAndConfiguration
+ && skyFunctionName == SkyFunctions.TARGET_COMPLETION) {
+ return "configured target: " + ((LabelAndConfiguration) arg).getLabel().toString();
+ }
+ throw new IllegalStateException(
+ "Argument is not Action, TargetCompletion, or OwnedArtifact: " + arg);
+ }
+
+ @Override
+ protected Label getLabel(SkyKey key) {
+ Object arg = key.argument();
+ if (arg instanceof OwnedArtifact) {
+ return ((OwnedArtifact) arg).getArtifact().getOwner();
+ } else if (arg instanceof Action) {
+ return ((Action) arg).getOwner().getLabel();
+ }
+ throw new IllegalStateException("Argument is not Action or OwnedArtifact: " + arg);
+ }
+
+ @Override
+ protected boolean canReportCycle(SkyKey topLevelKey, CycleInfo cycleInfo) {
+ return IS_ARTIFACT_OR_ACTION_SKY_KEY.apply(topLevelKey)
+ && Iterables.all(cycleInfo.getCycle(), IS_ARTIFACT_OR_ACTION_SKY_KEY);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ActionExecutionFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/ActionExecutionFunction.java
new file mode 100644
index 0000000..1420860
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ActionExecutionFunction.java
@@ -0,0 +1,338 @@
+// 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.Function;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.ActionCacheChecker.Token;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ActionExecutionException;
+import com.google.devtools.build.lib.actions.AlreadyReportedActionExecutionException;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.MissingInputFileException;
+import com.google.devtools.build.lib.actions.NotifyOnActionCacheHit;
+import com.google.devtools.build.lib.actions.cache.MetadataHandler;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.Pair;
+import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionException;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+import com.google.devtools.build.skyframe.ValueOrException2;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A builder for {@link ActionExecutionValue}s.
+ */
+public class ActionExecutionFunction implements SkyFunction {
+
+ private static final Predicate<Artifact> IS_SOURCE_ARTIFACT = new Predicate<Artifact>() {
+ @Override
+ public boolean apply(Artifact input) {
+ return input.isSourceArtifact();
+ }
+ };
+
+ private final SkyframeActionExecutor skyframeActionExecutor;
+ private final TimestampGranularityMonitor tsgm;
+
+ public ActionExecutionFunction(SkyframeActionExecutor skyframeActionExecutor,
+ TimestampGranularityMonitor tsgm) {
+ this.skyframeActionExecutor = skyframeActionExecutor;
+ this.tsgm = tsgm;
+ }
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) throws ActionExecutionFunctionException,
+ InterruptedException {
+ Action action = (Action) skyKey.argument();
+ Map<Artifact, FileArtifactValue> inputArtifactData = null;
+ Map<Artifact, Collection<Artifact>> expandedMiddlemen = null;
+ boolean alreadyRan = skyframeActionExecutor.probeActionExecution(action);
+ try {
+ Pair<Map<Artifact, FileArtifactValue>, Map<Artifact, Collection<Artifact>>> checkedInputs =
+ checkInputs(env, action, alreadyRan); // Declare deps on known inputs to action.
+
+ if (checkedInputs != null) {
+ inputArtifactData = checkedInputs.first;
+ expandedMiddlemen = checkedInputs.second;
+ }
+ } catch (ActionExecutionException e) {
+ throw new ActionExecutionFunctionException(e);
+ }
+ // TODO(bazel-team): Non-volatile NotifyOnActionCacheHit actions perform worse in Skyframe than
+ // legacy when they are not at the top of the action graph. In legacy, they are stored
+ // separately, so notifying non-dirty actions is cheap. In Skyframe, they depend on the
+ // BUILD_ID, forcing invalidation of upward transitive closure on each build.
+ if (action.isVolatile() || action instanceof NotifyOnActionCacheHit) {
+ // Volatile build actions may need to execute even if none of their known inputs have changed.
+ // Depending on the buildID ensure that these actions have a chance to execute.
+ PrecomputedValue.BUILD_ID.get(env);
+ }
+ if (env.valuesMissing()) {
+ return null;
+ }
+
+ ActionExecutionValue result;
+ try {
+ result = checkCacheAndExecuteIfNeeded(action, inputArtifactData, expandedMiddlemen, env);
+ } catch (ActionExecutionException e) {
+ // In this case we do not report the error to the action reporter because we have already
+ // done it in SkyframeExecutor.reportErrorIfNotAbortingMode() method. That method
+ // prints the error in the top-level reporter and also dumps the recorded StdErr for the
+ // action. Label can be null in the case of, e.g., the SystemActionOwner (for build-info.txt).
+ throw new ActionExecutionFunctionException(new AlreadyReportedActionExecutionException(e));
+ } finally {
+ declareAdditionalDependencies(env, action);
+ }
+ if (env.valuesMissing()) {
+ return null;
+ }
+
+ return result;
+ }
+
+ private ActionExecutionValue checkCacheAndExecuteIfNeeded(
+ Action action,
+ Map<Artifact, FileArtifactValue> inputArtifactData,
+ Map<Artifact, Collection<Artifact>> expandedMiddlemen,
+ Environment env) throws ActionExecutionException, InterruptedException {
+ // Don't initialize the cache if the result has already been computed and this is just a
+ // rerun.
+ FileAndMetadataCache fileAndMetadataCache = null;
+ MetadataHandler metadataHandler = null;
+ Token token = null;
+ long actionStartTime = System.nanoTime();
+ // inputArtifactData is null exactly when we know that the execution result was already
+ // computed on a prior run of this SkyFunction. If it is null we don't need to initialize
+ // anything -- we will get the result directly from SkyframeActionExecutor's cache.
+ if (inputArtifactData != null) {
+ // Check action cache to see if we need to execute anything. Checking the action cache only
+ // needs to happen on the first run, since a cache hit means we'll return immediately, and
+ // there'll be no second run.
+ fileAndMetadataCache = new FileAndMetadataCache(
+ inputArtifactData,
+ expandedMiddlemen,
+ skyframeActionExecutor.getExecRoot(),
+ action.getOutputs(),
+ // Only give the metadata cache the ability to look up Skyframe values if the action
+ // might have undeclared inputs. If those undeclared inputs are generated, they are
+ // present in Skyframe, so we can save a stat by looking them up directly.
+ action.discoversInputs() ? env : null,
+ tsgm);
+ metadataHandler =
+ skyframeActionExecutor.constructMetadataHandler(fileAndMetadataCache);
+ token = skyframeActionExecutor.checkActionCache(action, metadataHandler, actionStartTime);
+ }
+ if (token == null && inputArtifactData != null) {
+ // We got a hit from the action cache -- no need to execute.
+ return new ActionExecutionValue(
+ fileAndMetadataCache.getOutputData(),
+ fileAndMetadataCache.getAdditionalOutputData());
+ } else {
+ ActionExecutionContext actionExecutionContext = null;
+ if (inputArtifactData != null) {
+ actionExecutionContext = skyframeActionExecutor.constructActionExecutionContext(
+ fileAndMetadataCache,
+ metadataHandler);
+ if (action.discoversInputs()) {
+ skyframeActionExecutor.discoverInputs(action, actionExecutionContext);
+ }
+ }
+ // If this is the second time we are here (because the action discovers inputs, and we had
+ // to restart the value builder after declaring our dependence on newly discovered inputs),
+ // the result returned here is the already-computed result from the first run.
+ // Similarly, if this is a shared action and the other action is the one that executed, we
+ // must use that other action's value, provided here, since it is populated with metadata
+ // for the outputs.
+ // If this action was not shared and this is the first run of the action, this returned
+ // result was computed during the call.
+ return skyframeActionExecutor.executeAction(action, fileAndMetadataCache, token,
+ actionStartTime, actionExecutionContext);
+ }
+ }
+
+ private static Iterable<SkyKey> toKeys(Iterable<Artifact> inputs,
+ Iterable<Artifact> mandatoryInputs) {
+ if (mandatoryInputs == null) {
+ // This is a non inputs-discovering action, so no need to distinguish mandatory from regular
+ // inputs.
+ return Iterables.transform(inputs, new Function<Artifact, SkyKey>() {
+ @Override
+ public SkyKey apply(Artifact artifact) {
+ return ArtifactValue.key(artifact, true);
+ }
+ });
+ } else {
+ Collection<SkyKey> discoveredArtifacts = new HashSet<>();
+ Set<Artifact> mandatory = Sets.newHashSet(mandatoryInputs);
+ for (Artifact artifact : inputs) {
+ discoveredArtifacts.add(ArtifactValue.key(artifact, mandatory.contains(artifact)));
+ }
+
+ // In case the action violates the invariant that getInputs() is a superset of
+ // getMandatoryInputs(), explicitly add the mandatory inputs. See bug about an
+ // "action not in canonical form" error message. Also note that we may add Skyframe edges on
+ // these potentially stale deps due to the way loading inputs from the action cache functions.
+ // In practice, this is safe since C++ actions (the only ones which discover inputs) only add
+ // possibly stale inputs on source artifacts, which we treat as non-mandatory.
+ for (Artifact artifact : mandatory) {
+ discoveredArtifacts.add(ArtifactValue.key(artifact, true));
+ }
+ return discoveredArtifacts;
+ }
+ }
+
+ /**
+ * Declare dependency on all known inputs of action. Throws exception if any are known to be
+ * missing. Some inputs may not yet be in the graph, in which case the builder should abort.
+ */
+ private Pair<Map<Artifact, FileArtifactValue>, Map<Artifact, Collection<Artifact>>> checkInputs(
+ Environment env, Action action, boolean alreadyRan) throws ActionExecutionException {
+ Map<SkyKey, ValueOrException2<MissingInputFileException, ActionExecutionException>> inputDeps =
+ env.getValuesOrThrow(toKeys(action.getInputs(), action.discoversInputs()
+ ? action.getMandatoryInputs() : null), MissingInputFileException.class,
+ ActionExecutionException.class);
+
+ // If the action was already run, then break out early. This avoids the cost of constructing the
+ // input map and expanded middlemen if they're not going to be used.
+ if (alreadyRan) {
+ return null;
+ }
+
+ int missingCount = 0;
+ int actionFailures = 0;
+ boolean catastrophe = false;
+ // Only populate input data if we have the input values, otherwise they'll just go unused.
+ // We still want to loop through the inputs to collect missing deps errors. During the
+ // evaluator "error bubbling", we may get one last chance at reporting errors even though
+ // some deps are stilling missing.
+ boolean populateInputData = !env.valuesMissing();
+ NestedSetBuilder<Label> rootCauses = NestedSetBuilder.stableOrder();
+ Map<Artifact, FileArtifactValue> inputArtifactData =
+ new HashMap<>(populateInputData ? inputDeps.size() : 0);
+ Map<Artifact, Collection<Artifact>> expandedMiddlemen =
+ new HashMap<>(populateInputData ? 128 : 0);
+
+ ActionExecutionException firstActionExecutionException = null;
+ for (Map.Entry<SkyKey, ValueOrException2<MissingInputFileException,
+ ActionExecutionException>> depsEntry : inputDeps.entrySet()) {
+ Artifact input = ArtifactValue.artifact(depsEntry.getKey());
+ try {
+ ArtifactValue value = (ArtifactValue) depsEntry.getValue().get();
+ if (populateInputData && value instanceof AggregatingArtifactValue) {
+ AggregatingArtifactValue aggregatingValue = (AggregatingArtifactValue) value;
+ for (Pair<Artifact, FileArtifactValue> entry : aggregatingValue.getInputs()) {
+ inputArtifactData.put(entry.first, entry.second);
+ }
+ // We have to cache the "digest" of the aggregating value itself, because the action cache
+ // checker may want it.
+ inputArtifactData.put(input, aggregatingValue.getSelfData());
+ expandedMiddlemen.put(input,
+ Collections2.transform(aggregatingValue.getInputs(),
+ Pair.<Artifact, FileArtifactValue>firstFunction()));
+ } else if (populateInputData && value instanceof FileArtifactValue) {
+ // TODO(bazel-team): Make sure middleman "virtual" artifact data is properly processed.
+ inputArtifactData.put(input, (FileArtifactValue) value);
+ }
+ } catch (MissingInputFileException e) {
+ missingCount++;
+ if (input.getOwner() != null) {
+ rootCauses.add(input.getOwner());
+ }
+ } catch (ActionExecutionException e) {
+ actionFailures++;
+ if (firstActionExecutionException == null) {
+ firstActionExecutionException = e;
+ }
+ catastrophe = catastrophe || e.isCatastrophe();
+ rootCauses.addTransitive(e.getRootCauses());
+ }
+ }
+ // We need to rethrow first exception because it can contain useful error message
+ if (firstActionExecutionException != null) {
+ if (missingCount == 0 && actionFailures == 1) {
+ // In the case a single action failed, just propagate the exception upward. This avoids
+ // having to copy the root causes to the upwards transitive closure.
+ throw firstActionExecutionException;
+ }
+ throw new ActionExecutionException(firstActionExecutionException.getMessage(),
+ firstActionExecutionException.getCause(), action, rootCauses.build(), catastrophe);
+ }
+
+ if (missingCount > 0) {
+ for (Label missingInput : rootCauses.build()) {
+ env.getListener().handle(Event.error(action.getOwner().getLocation(), String.format(
+ "%s: missing input file '%s'", action.getOwner().getLabel(), missingInput)));
+ }
+ throw new ActionExecutionException(missingCount + " input file(s) do not exist", action,
+ rootCauses.build(), /*catastrophe=*/false);
+ }
+ return Pair.of(
+ Collections.unmodifiableMap(inputArtifactData),
+ Collections.unmodifiableMap(expandedMiddlemen));
+ }
+
+ private static void declareAdditionalDependencies(Environment env, Action action) {
+ if (action.discoversInputs()) {
+ // TODO(bazel-team): Should this be all inputs, or just source files?
+ env.getValues(toKeys(Iterables.filter(action.getInputs(), IS_SOURCE_ARTIFACT),
+ action.getMandatoryInputs()));
+ }
+ }
+
+ /**
+ * All info/warning messages associated with actions should be always displayed.
+ */
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+
+ /**
+ * Used to declare all the exception types that can be wrapped in the exception thrown by
+ * {@link ActionExecutionFunction#compute}.
+ */
+ private static final class ActionExecutionFunctionException extends SkyFunctionException {
+
+ private final ActionExecutionException actionException;
+
+ public ActionExecutionFunctionException(ActionExecutionException e) {
+ // We conservatively assume that the error is transient. We don't have enough information to
+ // distinguish non-transient errors (e.g. compilation error from a deterministic compiler)
+ // from transient ones (e.g. IO error).
+ // TODO(bazel-team): Have ActionExecutionExceptions declare their transience.
+ super(e, Transience.TRANSIENT);
+ this.actionException = e;
+ }
+
+ @Override
+ public boolean isCatastrophic() {
+ return actionException.isCatastrophe();
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ActionExecutionInactivityWatchdog.java b/src/main/java/com/google/devtools/build/lib/skyframe/ActionExecutionInactivityWatchdog.java
new file mode 100644
index 0000000..87e3e0d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ActionExecutionInactivityWatchdog.java
@@ -0,0 +1,180 @@
+// 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.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.actions.ActionExecutionStatusReporter;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * An object that can monitor whether actions are getting completed in a timely manner.
+ *
+ * <p>If there's nothing happening for a while, a background thread will print (and update) the
+ * "Still waiting for N actions to complete..." message.
+ */
+public final class ActionExecutionInactivityWatchdog {
+
+ /** An object used in monitoring action execution inactivity. */
+ public interface InactivityMonitor {
+
+ /** Returns whether action execution has started. */
+ boolean hasStarted();
+
+ /** Returns the number of enqueued but not yet completed actions. */
+ int getPending();
+
+ /**
+ * Waits for any action to complete, or the timeout to elapse.
+ *
+ * <p>The thread must wait at least for the specified timeout, unless some action completes in
+ * the meantime. It's not allowed to return 0 too early.
+ *
+ * <p>Note that it's acceptable to return (any value) later than specified by the timeout.
+ *
+ * @return the number of actions completed during the wait
+ */
+ int waitForNextCompletion(int timeoutMilliseconds) throws InterruptedException;
+ }
+
+ /** An object that the watchdog can report inactivity to. */
+ public interface InactivityReporter {
+
+ /**
+ * Report that actions are not getting completed in a timely manner.
+ *
+ * <p>Inactivity is typically not reported if tests with streaming output are being run.
+ */
+ void maybeReportInactivity();
+ }
+
+ @VisibleForTesting
+ interface Sleep {
+ void sleep(int durationMilliseconds) throws InterruptedException;
+ }
+
+ private static final class WaitTime {
+ private final int progressIntervalFlagValue;
+ private int prev;
+
+ public WaitTime(int progressIntervalFlagValue) {
+ this.progressIntervalFlagValue = progressIntervalFlagValue;
+ }
+
+ public void reset() {
+ prev = 0;
+ }
+
+ public int next() {
+ prev = ActionExecutionStatusReporter.getWaitTime(progressIntervalFlagValue, prev);
+ return prev;
+ }
+ }
+
+ private final AtomicBoolean isRunning = new AtomicBoolean(false);
+ private final InactivityMonitor monitor;
+ private final InactivityReporter reporter;
+ private final Sleep sleeper;
+ private final Thread thread;
+ private final WaitTime waitTime;
+
+ public ActionExecutionInactivityWatchdog(InactivityMonitor monitor, InactivityReporter reporter,
+ int progressIntervalFlagValue) {
+ this(monitor, reporter, progressIntervalFlagValue, new Sleep() {
+ @Override
+ public void sleep(int durationMilliseconds) throws InterruptedException {
+ Thread.sleep(durationMilliseconds);
+ }
+ });
+ }
+
+ @VisibleForTesting
+ public ActionExecutionInactivityWatchdog(InactivityMonitor monitor, InactivityReporter reporter,
+ int progressIntervalFlagValue, Sleep sleeper) {
+ this.monitor = Preconditions.checkNotNull(monitor);
+ this.reporter = Preconditions.checkNotNull(reporter);
+ this.sleeper = Preconditions.checkNotNull(sleeper);
+ this.waitTime = new WaitTime(progressIntervalFlagValue);
+ this.thread = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ enterWatchdogLoop();
+ }
+ });
+ this.thread.setDaemon(true);
+ this.thread.setName("action-execution-watchdog");
+ }
+
+ /** Starts the watchdog thread. This method should only be called once. */
+ public void start() {
+ Preconditions.checkState(!isRunning.getAndSet(true));
+ thread.start();
+ }
+
+ /**
+ * Stops the watchdog thread. This method should only be called once.
+ *
+ * <p>The method waits for the thread to terminate. If the caller thread is interrupted
+ * in the meantime, the interrupted status will be set.
+ */
+ public void stop() {
+ Preconditions.checkState(isRunning.getAndSet(false));
+ thread.interrupt();
+ try {
+ thread.join();
+ } catch (InterruptedException e) {
+ // When Thread.join throws, the interrupted status is cleared. We need to set it again.
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ private void enterWatchdogLoop() {
+ while (isRunning.get()) {
+ try {
+ // Wait a while for any SkyFunction to finish. The returned number indicates how many
+ // actions completed during the wait. It's possible that this is more than 1, since
+ // this thread may not immediately regain control.
+ int completedActions = monitor.waitForNextCompletion(waitTime.next() * 1000);
+ if (!isRunning.get()) {
+ break;
+ }
+
+ int pending = monitor.getPending();
+ if (!monitor.hasStarted() || completedActions > 0 || pending == 0) {
+ // If no keys have been enqueued yet (execution hasn't started), or some actions
+ // were completed since this thread was notified (we are making visible progress),
+ // or there are currently no enqueued actions waiting to be processed (perhaps all
+ // have completed and we are about to stop monitoring), then there's no need to
+ // display any messages.
+ waitTime.reset();
+
+ // Sleep a while before checking again. Actions might be executing at a nice rate, no
+ // need to worry about inactivity. This extra sleep isn't required but it's nice to
+ // have: without it we would, at times of high action completion rate, unnecessarily
+ // put the monitor into a fast sleep-wake cycle --- not a big problem but wasteful.
+ sleeper.sleep(1000);
+ } else {
+ // If actions are executing but we haven't made any progress in a while (no new
+ // action completion), then reassure the user that we're still running. Next time
+ // wait a little longer.
+ reporter.maybeReportInactivity();
+ }
+ } catch (InterruptedException ie) {
+ Thread.currentThread().interrupt();
+ return;
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ActionExecutionValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/ActionExecutionValue.java
new file mode 100644
index 0000000..de63c3b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ActionExecutionValue.java
@@ -0,0 +1,117 @@
+// 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.annotations.VisibleForTesting;
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.Action.MiddlemanType;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+/**
+ * A value representing an executed action.
+ */
+@Immutable
+@ThreadSafe
+public class ActionExecutionValue implements SkyValue {
+ private final ImmutableMap<Artifact, FileValue> artifactData;
+ private final ImmutableMap<Artifact, FileArtifactValue> additionalOutputData;
+
+ /**
+ * @param artifactData Map from Artifacts to corresponding FileValues.
+ * @param additionalOutputData Map from Artifacts to values if the FileArtifactValue for this
+ * artifact cannot be derived from the corresponding FileValue (see {@link
+ * FileAndMetadataCache#getAdditionalOutputData} for when this is necessary).
+ */
+ ActionExecutionValue(Map<Artifact, FileValue> artifactData,
+ Map<Artifact, FileArtifactValue> additionalOutputData) {
+ this.artifactData = ImmutableMap.copyOf(artifactData);
+ this.additionalOutputData = ImmutableMap.copyOf(additionalOutputData);
+ }
+
+ /**
+ * Returns metadata for a given artifact, if that metadata cannot be inferred from the
+ * corresponding {@link #getData} call for that Artifact. See {@link
+ * FileAndMetadataCache#getAdditionalOutputData} for when that can happen.
+ */
+ @Nullable
+ FileArtifactValue getArtifactValue(Artifact artifact) {
+ return additionalOutputData.get(artifact);
+ }
+
+ /**
+ * @return The data for each non-middleman output of this action, in the form of the {@link
+ * FileValue} that would be created for the file if it were to be read from disk.
+ */
+ FileValue getData(Artifact artifact) {
+ Preconditions.checkState(!additionalOutputData.containsKey(artifact),
+ "Should not be requesting data for already-constructed FileArtifactValue: %s", artifact);
+ return artifactData.get(artifact);
+ }
+
+ /**
+ * @return The map from {@link Artifact} to the corresponding {@link FileValue} that would be
+ * returned by {@link #getData}. Should only be needed by {@link FilesystemValueChecker}.
+ */
+ ImmutableMap<Artifact, FileValue> getAllOutputArtifactData() {
+ return artifactData;
+ }
+
+ @ThreadSafe
+ @VisibleForTesting
+ public static SkyKey key(Action action) {
+ return new SkyKey(SkyFunctions.ACTION_EXECUTION, action);
+ }
+
+ /**
+ * Returns whether the key corresponds to a ActionExecutionValue worth reporting status about.
+ *
+ * <p>If an action can do real work, it's probably worth counting and reporting status about.
+ * Actions that don't really do any work (typically middleman actions) should not be counted
+ * towards enqueued and completed actions.
+ */
+ public static boolean isReportWorthyAction(SkyKey key) {
+ return key.functionName() == SkyFunctions.ACTION_EXECUTION
+ && isReportWorthyAction((Action) key.argument());
+ }
+
+ /**
+ * Returns whether the action is worth reporting status about.
+ *
+ * <p>If an action can do real work, it's probably worth counting and reporting status about.
+ * Actions that don't really do any work (typically middleman actions) should not be counted
+ * towards enqueued and completed actions.
+ */
+ public static boolean isReportWorthyAction(Action action) {
+ return action.getActionType() == MiddlemanType.NORMAL;
+ }
+
+ @Override
+ public String toString() {
+ return Objects.toStringHelper(this)
+ .add("artifactData", artifactData)
+ .add("additionalOutputData", additionalOutputData)
+ .toString();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ActionLookupValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/ActionLookupValue.java
new file mode 100644
index 0000000..1dfa722
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ActionLookupValue.java
@@ -0,0 +1,106 @@
+// 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.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.ArtifactOwner;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.skyframe.SkyFunctionName;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Base class for all values which can provide the generating action of an artifact. The primary
+ * instance of such lookup values is {@link ConfiguredTargetValue}. Values that hold the generating
+ * actions of target completion values and build info artifacts also fall into this category.
+ */
+public class ActionLookupValue implements SkyValue {
+ protected final ImmutableMap<Artifact, Action> generatingActionMap;
+
+ ActionLookupValue(Iterable<Action> actions) {
+ // Duplicate/shared actions get passed in all the time. Blaze is weird. We can't double-register
+ // the generated artifacts in an immutable map builder, so we double-register them in a more
+ // forgiving map, and then use that map to create the immutable one.
+ Map<Artifact, Action> generatingActions = new HashMap<>();
+ for (Action action : actions) {
+ for (Artifact artifact : action.getOutputs()) {
+ generatingActions.put(artifact, action);
+ }
+ }
+ generatingActionMap = ImmutableMap.copyOf(generatingActions);
+ }
+
+ ActionLookupValue(Action action) {
+ this(ImmutableList.of(action));
+ }
+
+ Action getGeneratingAction(Artifact artifact) {
+ return generatingActionMap.get(artifact);
+ }
+
+ /** To be used only when checking consistency of the action graph -- not by other values. */
+ ImmutableMap<Artifact, Action> getMapForConsistencyCheck() {
+ return generatingActionMap;
+ }
+
+ /**
+ * To be used only when setting the owners of deserialized artifacts whose owners were unknown at
+ * creation time -- not by other callers or values.
+ */
+ Iterable<Action> getActionsForFindingArtifactOwners() {
+ return generatingActionMap.values();
+ }
+
+ @VisibleForTesting
+ public static SkyKey key(ActionLookupKey ownerKey) {
+ return ownerKey.getSkyKey();
+ }
+
+ /**
+ * ArtifactOwner is not a SkyKey, but we wish to convert any ArtifactOwner into a SkyKey as
+ * simply as possible. To that end, all subclasses of ActionLookupValue "own" artifacts with
+ * ArtifactOwners that are subclasses of ActionLookupKey. This allows callers to easily find the
+ * value key, while remaining agnostic to what ActionLookupValues actually exist.
+ *
+ * <p>The methods of this class should only be called by {@link ActionLookupValue#key}.
+ */
+ protected abstract static class ActionLookupKey implements ArtifactOwner {
+ @Override
+ public Label getLabel() {
+ return null;
+ }
+
+ /**
+ * Subclasses must override this to specify their specific value type, unless they override
+ * {@link #getSkyKey}, in which case they are free not to implement this method.
+ */
+ abstract SkyFunctionName getType();
+
+ /**
+ * Prefer {@link ActionLookupValue#key} to calling this method directly.
+ *
+ * <p>Subclasses may override if the value key contents should not be the key itself.
+ */
+ SkyKey getSkyKey() {
+ return new SkyKey(getType(), this);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/AggregatingArtifactValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/AggregatingArtifactValue.java
new file mode 100644
index 0000000..8374efe
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/AggregatingArtifactValue.java
@@ -0,0 +1,42 @@
+// 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.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.util.Pair;
+
+import java.util.Collection;
+
+/** Value for aggregating artifacts, which must be expanded to a set of other artifacts. */
+class AggregatingArtifactValue extends ArtifactValue {
+ private final FileArtifactValue selfData;
+ private final ImmutableList<Pair<Artifact, FileArtifactValue>> inputs;
+
+ AggregatingArtifactValue(ImmutableList<Pair<Artifact, FileArtifactValue>> inputs,
+ FileArtifactValue selfData) {
+ this.inputs = inputs;
+ this.selfData = selfData;
+ }
+
+ /** Returns the artifacts that this artifact expands to, together with their data. */
+ Collection<Pair<Artifact, FileArtifactValue>> getInputs() {
+ return inputs;
+ }
+
+ /** Returns the data of the artifact for this value, as computed by the action cache checker. */
+ FileArtifactValue getSelfData() {
+ return selfData;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ArtifactFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/ArtifactFunction.java
new file mode 100644
index 0000000..e277476
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ArtifactFunction.java
@@ -0,0 +1,230 @@
+// 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.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.Action.MiddlemanType;
+import com.google.devtools.build.lib.actions.ActionExecutionException;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.ArtifactOwner;
+import com.google.devtools.build.lib.actions.MissingInputFileException;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.skyframe.ActionLookupValue.ActionLookupKey;
+import com.google.devtools.build.lib.skyframe.ArtifactValue.OwnedArtifact;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.Pair;
+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 java.io.IOException;
+import java.util.Map;
+
+/**
+ * A builder for {@link ArtifactValue}s.
+ */
+class ArtifactFunction implements SkyFunction {
+
+ private final Predicate<PathFragment> allowedMissingInputs;
+
+ ArtifactFunction(Predicate<PathFragment> allowedMissingInputs) {
+ this.allowedMissingInputs = allowedMissingInputs;
+ }
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) throws ArtifactFunctionException {
+ OwnedArtifact ownedArtifact = (OwnedArtifact) skyKey.argument();
+ Artifact artifact = ownedArtifact.getArtifact();
+ if (artifact.isSourceArtifact()) {
+ try {
+ return createSourceValue(artifact, ownedArtifact.isMandatory(), env);
+ } catch (MissingInputFileException e) {
+ // The error is not necessarily truly transient, but we mark it as such because we have
+ // the above side effect of posting an event to the EventBus. Importantly, that event
+ // is potentially used to report root causes.
+ throw new ArtifactFunctionException(e, Transience.TRANSIENT);
+ }
+ }
+
+ Action action = extractActionFromArtifact(artifact, env);
+ if (action == null) {
+ return null;
+ }
+
+ ActionExecutionValue actionValue =
+ (ActionExecutionValue) env.getValue(ActionExecutionValue.key(action));
+ if (actionValue == null) {
+ return null;
+ }
+
+ if (!isAggregatingValue(action)) {
+ try {
+ return createSimpleValue(artifact, actionValue);
+ } catch (IOException e) {
+ ActionExecutionException ex = new ActionExecutionException(e, action,
+ /*catastrophe=*/false);
+ env.getListener().handle(Event.error(ex.getLocation(), ex.getMessage()));
+ // This is a transient error since we did the work that led to the IOException.
+ throw new ArtifactFunctionException(ex, Transience.TRANSIENT);
+ }
+ } else {
+ return createAggregatingValue(artifact, action, actionValue.getArtifactValue(artifact), env);
+ }
+ }
+
+ private ArtifactValue createSourceValue(Artifact artifact, boolean mandatory, Environment env)
+ throws MissingInputFileException {
+ SkyKey fileSkyKey = FileValue.key(RootedPath.toRootedPath(artifact.getRoot().getPath(),
+ artifact.getPath()));
+ FileValue fileValue;
+ try {
+ fileValue = (FileValue) env.getValueOrThrow(fileSkyKey, IOException.class,
+ InconsistentFilesystemException.class, FileSymlinkCycleException.class);
+ } catch (IOException | InconsistentFilesystemException | FileSymlinkCycleException e) {
+ throw makeMissingInputFileExn(artifact, mandatory, e, env.getListener());
+ }
+ if (fileValue == null) {
+ return null;
+ }
+ if (!fileValue.exists()) {
+ if (allowedMissingInputs.apply(((RootedPath) fileSkyKey.argument()).getRelativePath())) {
+ return FileArtifactValue.MISSING_FILE_MARKER;
+ } else {
+ return missingInputFile(artifact, mandatory, null, env.getListener());
+ }
+ }
+ try {
+ return FileArtifactValue.create(artifact, fileValue);
+ } catch (IOException e) {
+ throw makeMissingInputFileExn(artifact, mandatory, e, env.getListener());
+ }
+ }
+
+ private static ArtifactValue missingInputFile(Artifact artifact, boolean mandatory,
+ Exception failure, EventHandler reporter) throws MissingInputFileException {
+ if (!mandatory) {
+ return FileArtifactValue.MISSING_FILE_MARKER;
+ }
+ throw makeMissingInputFileExn(artifact, mandatory, failure, reporter);
+ }
+
+ private static MissingInputFileException makeMissingInputFileExn(Artifact artifact,
+ boolean mandatory, Exception failure, EventHandler reporter) {
+ String extraMsg = (failure == null) ? "" : (":" + failure.getMessage());
+ MissingInputFileException ex = new MissingInputFileException(
+ constructErrorMessage(artifact) + extraMsg, null);
+ if (mandatory) {
+ reporter.handle(Event.error(ex.getLocation(), ex.getMessage()));
+ }
+ return ex;
+ }
+
+ // Non-aggregating artifact -- should contain at most one piece of artifact data.
+ // data may be null if and only if artifact is a middleman artifact.
+ private ArtifactValue createSimpleValue(Artifact artifact, ActionExecutionValue actionValue)
+ throws IOException {
+ ArtifactValue value = actionValue.getArtifactValue(artifact);
+ if (value != null) {
+ return value;
+ }
+ // Middleman artifacts have no corresponding files, so their ArtifactValues should have already
+ // been constructed during execution of the action.
+ Preconditions.checkState(!artifact.isMiddlemanArtifact(), artifact);
+ FileValue data = Preconditions.checkNotNull(actionValue.getData(artifact),
+ "%s %s", artifact, actionValue);
+ Preconditions.checkNotNull(data.getDigest(),
+ "Digest should already have been calculated for %s (%s)", artifact, data);
+ return FileArtifactValue.create(artifact, data);
+ }
+
+ private AggregatingArtifactValue createAggregatingValue(Artifact artifact, Action action,
+ FileArtifactValue value, SkyFunction.Environment env) {
+ // This artifact aggregates other artifacts. Keep track of them so callers can find them.
+ ImmutableList.Builder<Pair<Artifact, FileArtifactValue>> inputs = ImmutableList.builder();
+ for (Map.Entry<SkyKey, SkyValue> entry :
+ env.getValues(ArtifactValue.mandatoryKeys(action.getInputs())).entrySet()) {
+ Artifact input = ArtifactValue.artifact(entry.getKey());
+ ArtifactValue inputValue = (ArtifactValue) entry.getValue();
+ Preconditions.checkNotNull(inputValue, "%s has null dep %s", artifact, input);
+ if (!(inputValue instanceof FileArtifactValue)) {
+ // We do not recurse in aggregating middleman artifacts.
+ Preconditions.checkState(!(inputValue instanceof AggregatingArtifactValue),
+ "%s %s %s", artifact, action, inputValue);
+ continue;
+ }
+ inputs.add(Pair.of(input, (FileArtifactValue) inputValue));
+ }
+ return new AggregatingArtifactValue(inputs.build(), value);
+ }
+
+ /**
+ * Returns whether this value needs to contain the data of all its inputs. Currently only tests to
+ * see if the action is an aggregating middleman action. However, may include runfiles middleman
+ * actions and Fileset artifacts in the future.
+ */
+ private static boolean isAggregatingValue(Action action) {
+ return action.getActionType() == MiddlemanType.AGGREGATING_MIDDLEMAN;
+ }
+
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return Label.print(((OwnedArtifact) skyKey.argument()).getArtifact().getOwner());
+ }
+
+ private Action extractActionFromArtifact(Artifact artifact, SkyFunction.Environment env) {
+ ArtifactOwner artifactOwner = artifact.getArtifactOwner();
+
+ Preconditions.checkState(artifactOwner instanceof ActionLookupKey, "", artifact, artifactOwner);
+ SkyKey actionLookupKey = ActionLookupValue.key((ActionLookupKey) artifactOwner);
+ ActionLookupValue value = (ActionLookupValue) env.getValue(actionLookupKey);
+ if (value == null) {
+ Preconditions.checkState(artifactOwner == CoverageReportValue.ARTIFACT_OWNER,
+ "Not-yet-present artifact owner: %s", artifactOwner);
+ return null;
+ }
+ // The value should already exist (except for the coverage report action output artifacts):
+ // ConfiguredTargetValues were created during the analysis phase, and BuildInfo*Values
+ // were created during the first analysis of a configured target.
+ Preconditions.checkNotNull(value,
+ "Owner %s of %s not in graph %s", artifactOwner, artifact, actionLookupKey);
+ return Preconditions.checkNotNull(value.getGeneratingAction(artifact),
+ "Value %s does not contain generating action of %s", value, artifact);
+ }
+
+ private static final class ArtifactFunctionException extends SkyFunctionException {
+ ArtifactFunctionException(MissingInputFileException e, Transience transience) {
+ super(e, transience);
+ }
+
+ ArtifactFunctionException(ActionExecutionException e, Transience transience) {
+ super(e, transience);
+ }
+ }
+
+ private static String constructErrorMessage(Artifact artifact) {
+ if (artifact.getOwner() == null) {
+ return String.format("missing input file '%s'", artifact.getPath().getPathString());
+ } else {
+ return String.format("missing input file '%s'", artifact.getOwner());
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ArtifactValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/ArtifactValue.java
new file mode 100644
index 0000000..6139d2e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ArtifactValue.java
@@ -0,0 +1,160 @@
+// 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.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.util.Collection;
+
+/**
+ * A value representing an artifact. Source artifacts are checked for existence, while output
+ * artifacts imply creation of the output file.
+ *
+ * <p>There are effectively two kinds of output artifact values. The first corresponds to an
+ * ordinary artifact {@link FileArtifactValue}. It stores the relevant data for the artifact --
+ * digest/mtime and size. The second corresponds to an "aggregating" artifact -- the output of an
+ * aggregating middleman action. It stores the relevant data of all its inputs.
+ */
+@Immutable
+@ThreadSafe
+public abstract class ArtifactValue implements SkyValue {
+
+ @ThreadSafe
+ static SkyKey key(Artifact artifact, boolean isMandatory) {
+ return new SkyKey(SkyFunctions.ARTIFACT, artifact.isSourceArtifact()
+ ? new OwnedArtifact(artifact, isMandatory)
+ : new OwnedArtifact(artifact));
+ }
+
+ private static final Function<Artifact, SkyKey> TO_MANDATORY_KEY =
+ new Function<Artifact, SkyKey>() {
+ @Override
+ public SkyKey apply(Artifact artifact) {
+ return key(artifact, true);
+ }
+ };
+
+ @ThreadSafe
+ public static Iterable<SkyKey> mandatoryKeys(Iterable<Artifact> artifacts) {
+ return Iterables.transform(artifacts, TO_MANDATORY_KEY);
+ }
+
+ private static final Function<OwnedArtifact, Artifact> TO_ARTIFACT =
+ new Function<OwnedArtifact, Artifact>() {
+ @Override
+ public Artifact apply(OwnedArtifact key) {
+ return key.getArtifact();
+ }
+ };
+
+ public static Collection<Artifact> artifacts(Collection<? extends OwnedArtifact> keys) {
+ return Collections2.transform(keys, TO_ARTIFACT);
+ }
+
+ public static Artifact artifact(SkyKey key) {
+ return TO_ARTIFACT.apply((OwnedArtifact) key.argument());
+ }
+
+ /**
+ * Artifacts are compared using just their paths, but in Skyframe, the configured target that owns
+ * an artifact must also be part of the comparison. For example, suppose we build //foo:foo in
+ * configurationA, yielding artifact foo.out. If we change the configuration to configurationB in
+ * such a way that the path to the artifact does not change, requesting foo.out from the graph
+ * will result in the value entry for foo.out under configurationA being returned. This would
+ * prevent caching the graph in different configurations, and also causes big problems with change
+ * pruning, which assumes the invariant that a value's first dependency will always be the same.
+ * In this case, the value entry's old dependency on //foo:foo in configurationA would cause it to
+ * request (//foo:foo, configurationA) from the graph, causing an undesired re-analysis of
+ * (//foo:foo, configurationA).
+ *
+ * <p>In order to prevent that, instead of using Artifacts as keys in the graph, we use
+ * OwnedArtifacts, which compare for equality using both the Artifact, and the owner. The effect
+ * is functionally that of making Artifact.equals() check the owner, but only within Skyframe,
+ * since outside of Skyframe it is quite crucial that Artifacts with different owners be able to
+ * compare equal.
+ */
+ public static class OwnedArtifact {
+ private final Artifact artifact;
+ // Always true for derived artifacts.
+ private final boolean isMandatory;
+
+ /** Constructs an OwnedArtifact wrapper for a source artifact. */
+ private OwnedArtifact(Artifact sourceArtifact, boolean mandatory) {
+ Preconditions.checkArgument(sourceArtifact.isSourceArtifact());
+ this.artifact = Preconditions.checkNotNull(sourceArtifact);
+ this.isMandatory = mandatory;
+ }
+
+ /**
+ * Constructs an OwnedArtifact wrapper for a derived artifact. The mandatory attribute is
+ * not needed because a derived artifact must be a mandatory input for some action in order to
+ * ensure that it is built in the first place. If it fails to build, then that fact is cached
+ * in the node, so any action that has it as a non-mandatory input can retrieve that
+ * information from the node.
+ */
+ private OwnedArtifact(Artifact derivedArtifact) {
+ this.artifact = Preconditions.checkNotNull(derivedArtifact);
+ Preconditions.checkArgument(!derivedArtifact.isSourceArtifact(), derivedArtifact);
+ this.isMandatory = true; // Unused.
+ }
+
+ @Override
+ public int hashCode() {
+ int initialHash = artifact.hashCode() + artifact.getArtifactOwner().hashCode();
+ return isMandatory ? initialHash : 47 * initialHash + 1;
+ }
+
+ @Override
+ public boolean equals(Object that) {
+ if (this == that) {
+ return true;
+ }
+ if (!(that instanceof OwnedArtifact)) {
+ return false;
+ }
+ OwnedArtifact thatOwnedArtifact = ((OwnedArtifact) that);
+ Artifact thatArtifact = thatOwnedArtifact.artifact;
+ return artifact.equals(thatArtifact)
+ && artifact.getArtifactOwner().equals(thatArtifact.getArtifactOwner())
+ && isMandatory == thatOwnedArtifact.isMandatory;
+ }
+
+ Artifact getArtifact() {
+ return artifact;
+ }
+
+ /**
+ * Returns whether the artifact is a mandatory input of its requesting action. May only be
+ * called for source artifacts, since a derived artifact must be a mandatory input of some
+ * action in order to have been built in the first place.
+ */
+ public boolean isMandatory() {
+ Preconditions.checkState(artifact.isSourceArtifact(), artifact);
+ return isMandatory;
+ }
+
+ @Override
+ public String toString() {
+ return artifact.prettyPrint() + " " + artifact.getArtifactOwner();
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/AspectFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/AspectFunction.java
new file mode 100644
index 0000000..f1aa2f6e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/AspectFunction.java
@@ -0,0 +1,187 @@
+// 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.collect.ImmutableList;
+import com.google.common.collect.ListMultimap;
+import com.google.devtools.build.lib.analysis.Aspect;
+import com.google.devtools.build.lib.analysis.CachingAnalysisEnvironment;
+import com.google.devtools.build.lib.analysis.ConfiguredAspectFactory;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget;
+import com.google.devtools.build.lib.analysis.TargetAndConfiguration;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.ConfigMatchingProvider;
+import com.google.devtools.build.lib.events.StoredEventHandler;
+import com.google.devtools.build.lib.packages.AspectFactory;
+import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.NoSuchTargetException;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.skyframe.AspectValue.AspectKey;
+import com.google.devtools.build.lib.skyframe.ConfiguredTargetFunction.DependencyEvaluationException;
+import com.google.devtools.build.lib.skyframe.SkyframeExecutor.BuildViewProvider;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionException;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * The Skyframe function that generates aspects.
+ */
+public final class AspectFunction implements SkyFunction {
+ private final BuildViewProvider buildViewProvider;
+
+ public AspectFunction(BuildViewProvider buildViewProvider) {
+ this.buildViewProvider = buildViewProvider;
+ }
+
+ @Nullable
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env)
+ throws AspectFunctionException {
+ SkyframeBuildView view = buildViewProvider.getSkyframeBuildView();
+ AspectKey key = (AspectKey) skyKey.argument();
+ ConfiguredAspectFactory aspectFactory =
+ (ConfiguredAspectFactory) AspectFactory.Util.create(key.getAspect());
+
+ PackageValue packageValue =
+ (PackageValue) env.getValue(PackageValue.key(key.getLabel().getPackageIdentifier()));
+ if (packageValue == null) {
+ return null;
+ }
+
+ Target target;
+ try {
+ target = packageValue.getPackage().getTarget(key.getLabel().getName());
+ } catch (NoSuchTargetException e) {
+ throw new AspectFunctionException(skyKey, e);
+ }
+
+ if (!(target instanceof Rule)) {
+ throw new AspectFunctionException(new AspectCreationException(
+ "aspects must be attached to rules"));
+ }
+
+ RuleConfiguredTarget associatedTarget = (RuleConfiguredTarget)
+ ((ConfiguredTargetValue) env.getValue(ConfiguredTargetValue.key(
+ key.getLabel(), key.getConfiguration()))).getConfiguredTarget();
+
+ if (associatedTarget == null) {
+ return null;
+ }
+
+ SkyframeDependencyResolver resolver = view.createDependencyResolver(env);
+ if (resolver == null) {
+ return null;
+ }
+
+ TargetAndConfiguration ctgValue =
+ new TargetAndConfiguration(target, key.getConfiguration());
+
+ try {
+ // Get the configuration targets that trigger this rule's configurable attributes.
+ Set<ConfigMatchingProvider> configConditions =
+ ConfiguredTargetFunction.getConfigConditions(target, env, resolver, ctgValue);
+ if (configConditions == null) {
+ // Those targets haven't yet been resolved.
+ return null;
+ }
+
+ ListMultimap<Attribute, ConfiguredTarget> depValueMap =
+ ConfiguredTargetFunction.computeDependencies(env, resolver, ctgValue,
+ aspectFactory.getDefinition(), configConditions);
+
+ return createAspect(env, key, associatedTarget, configConditions, depValueMap);
+ } catch (DependencyEvaluationException e) {
+ throw new AspectFunctionException(e.getRootCauseSkyKey(), e.getCause());
+ }
+ }
+
+ @Nullable
+ private AspectValue createAspect(Environment env, AspectKey key,
+ RuleConfiguredTarget associatedTarget, Set<ConfigMatchingProvider> configConditions,
+ ListMultimap<Attribute, ConfiguredTarget> directDeps)
+ throws AspectFunctionException {
+ SkyframeBuildView view = buildViewProvider.getSkyframeBuildView();
+ BuildConfiguration configuration = associatedTarget.getConfiguration();
+ boolean extendedSanityChecks = configuration != null && configuration.extendedSanityChecks();
+
+ StoredEventHandler events = new StoredEventHandler();
+ CachingAnalysisEnvironment analysisEnvironment = view.createAnalysisEnvironment(
+ key, false, extendedSanityChecks, events, env, true);
+ if (env.valuesMissing()) {
+ return null;
+ }
+
+ ConfiguredAspectFactory aspectFactory =
+ (ConfiguredAspectFactory) AspectFactory.Util.create(key.getAspect());
+ Aspect aspect = view.createAspect(
+ analysisEnvironment, associatedTarget, aspectFactory, directDeps, configConditions);
+
+ events.replayOn(env.getListener());
+ if (events.hasErrors()) {
+ analysisEnvironment.disable(associatedTarget.getTarget());
+ throw new AspectFunctionException(new AspectCreationException(
+ "Analysis of target '" + associatedTarget.getLabel() + "' failed; build aborted"));
+ }
+ Preconditions.checkState(!analysisEnvironment.hasErrors(),
+ "Analysis environment hasError() but no errors reported");
+
+ if (env.valuesMissing()) {
+ return null;
+ }
+
+ analysisEnvironment.disable(associatedTarget.getTarget());
+ Preconditions.checkNotNull(aspect);
+
+ return new AspectValue(
+ aspect, ImmutableList.copyOf(analysisEnvironment.getRegisteredActions()));
+ }
+
+ @Nullable
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+
+ /**
+ * An exception indicating that there was a problem creating an aspect.
+ */
+ public static final class AspectCreationException extends Exception {
+ public AspectCreationException(String message) {
+ super(message);
+ }
+ }
+
+ /**
+ * Used to indicate errors during the computation of an {@link AspectValue}.
+ */
+ private static final class AspectFunctionException extends SkyFunctionException {
+ public AspectFunctionException(Exception e) {
+ super(e, Transience.PERSISTENT);
+ }
+
+ /** Used to rethrow a child error that we cannot handle. */
+ public AspectFunctionException(SkyKey childKey, Exception transitiveError) {
+ super(transitiveError, childKey);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/AspectValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/AspectValue.java
new file mode 100644
index 0000000..9b863bd
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/AspectValue.java
@@ -0,0 +1,109 @@
+// 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.Objects;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.analysis.Aspect;
+import com.google.devtools.build.lib.analysis.ConfiguredAspectFactory;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.skyframe.SkyFunctionName;
+import com.google.devtools.build.skyframe.SkyKey;
+
+/**
+ * An aspect in the context of the Skyframe graph.
+ */
+public final class AspectValue extends ActionLookupValue {
+ /**
+ * The key of an action that is generated by an aspect.
+ */
+ public static final class AspectKey extends ActionLookupKey {
+ private final Label label;
+ private final BuildConfiguration configuration;
+ // TODO(bazel-team): class objects are not really hashable or comparable for equality other than
+ // by reference. We should identify the aspect here in a way that does not rely on comparison
+ // by reference so that keys can be serialized and deserialized properly.
+ private final Class<? extends ConfiguredAspectFactory> aspectFactory;
+
+ private AspectKey(Label label, BuildConfiguration configuration,
+ Class<? extends ConfiguredAspectFactory> aspectFactory) {
+ this.label = label;
+ this.configuration = configuration;
+ this.aspectFactory = aspectFactory;
+ }
+
+ @Override
+ public Label getLabel() {
+ return label;
+ }
+
+ public BuildConfiguration getConfiguration() {
+ return configuration;
+ }
+
+ public Class<? extends ConfiguredAspectFactory> getAspect() {
+ return aspectFactory;
+ }
+
+ @Override
+ SkyFunctionName getType() {
+ return SkyFunctions.ASPECT;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(label, configuration, aspectFactory);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+
+ if (!(other instanceof AspectKey)) {
+ return false;
+ }
+
+ AspectKey that = (AspectKey) other;
+ return Objects.equal(label, that.label)
+ && Objects.equal(configuration, that.configuration)
+ && Objects.equal(aspectFactory, that.aspectFactory);
+ }
+
+ @Override
+ public String toString() {
+ return label + "#" + aspectFactory.getSimpleName() + " "
+ + (configuration == null ? "null" : configuration.shortCacheKey());
+ }
+ }
+
+ private final Aspect aspect;
+
+ public AspectValue(Aspect aspect, Iterable<Action> actions) {
+ super(actions);
+ this.aspect = aspect;
+ }
+
+ public Aspect get() {
+ return aspect;
+ }
+
+ public static SkyKey key(Label label, BuildConfiguration configuration,
+ Class<? extends ConfiguredAspectFactory> aspectFactory) {
+ return new SkyKey(SkyFunctions.ASPECT, new AspectKey(label, configuration, aspectFactory));
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/BrokenDiffAwarenessException.java b/src/main/java/com/google/devtools/build/lib/skyframe/BrokenDiffAwarenessException.java
new file mode 100644
index 0000000..a5b0272
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/BrokenDiffAwarenessException.java
@@ -0,0 +1,27 @@
+// 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;
+
+/**
+ * Thrown on {@link DiffAwareness#getDiff} to indicate that something is wrong with the
+ * {@link DiffAwareness} instance and it should not be used again.
+ */
+public class BrokenDiffAwarenessException extends Exception {
+
+ public BrokenDiffAwarenessException(String msg) {
+ super(Preconditions.checkNotNull(msg));
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/BuildInfoCollectionFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/BuildInfoCollectionFunction.java
new file mode 100644
index 0000000..e717e51
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/BuildInfoCollectionFunction.java
@@ -0,0 +1,86 @@
+// 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.Supplier;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.ArtifactFactory;
+import com.google.devtools.build.lib.actions.Root;
+import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoFactory;
+import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoFactory.BuildInfoContext;
+import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoFactory.BuildInfoKey;
+import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoFactory.BuildInfoType;
+import com.google.devtools.build.lib.skyframe.BuildInfoCollectionValue.BuildInfoKeyAndConfig;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.util.Map;
+
+/**
+ * Creates a {@link BuildInfoCollectionValue}. Only depends on the unique
+ * {@link WorkspaceStatusValue} and the constant {@link PrecomputedValue#BUILD_INFO_FACTORIES}
+ * injected value.
+ */
+public class BuildInfoCollectionFunction implements SkyFunction {
+ // Supplier only because the artifact factory has not yet been created at constructor time.
+ private final Supplier<ArtifactFactory> artifactFactory;
+ private final Root buildDataDirectory;
+
+ BuildInfoCollectionFunction(Supplier<ArtifactFactory> artifactFactory,
+ Root buildDataDirectory) {
+ this.artifactFactory = artifactFactory;
+ this.buildDataDirectory = buildDataDirectory;
+ }
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) {
+ final BuildInfoKeyAndConfig keyAndConfig = (BuildInfoKeyAndConfig) skyKey.argument();
+ WorkspaceStatusValue infoArtifactValue =
+ (WorkspaceStatusValue) env.getValue(WorkspaceStatusValue.SKY_KEY);
+ if (infoArtifactValue == null) {
+ return null;
+ }
+ Map<BuildInfoKey, BuildInfoFactory> buildInfoFactories =
+ PrecomputedValue.BUILD_INFO_FACTORIES.get(env);
+ if (buildInfoFactories == null) {
+ return null;
+ }
+ final ArtifactFactory factory = artifactFactory.get();
+ BuildInfoContext context = new BuildInfoContext() {
+ @Override
+ public Artifact getBuildInfoArtifact(PathFragment rootRelativePath, Root root,
+ BuildInfoType type) {
+ return type == BuildInfoType.NO_REBUILD
+ ? factory.getConstantMetadataArtifact(rootRelativePath, root, keyAndConfig)
+ : factory.getDerivedArtifact(rootRelativePath, root, keyAndConfig);
+ }
+
+ @Override
+ public Root getBuildDataDirectory() {
+ return buildDataDirectory;
+ }
+ };
+
+ return new BuildInfoCollectionValue(buildInfoFactories.get(
+ keyAndConfig.getInfoKey()).create(context, keyAndConfig.getConfig(),
+ infoArtifactValue.getStableArtifact(), infoArtifactValue.getVolatileArtifact()));
+ }
+
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/BuildInfoCollectionValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/BuildInfoCollectionValue.java
new file mode 100644
index 0000000..8958e1b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/BuildInfoCollectionValue.java
@@ -0,0 +1,97 @@
+// 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.devtools.build.lib.analysis.buildinfo.BuildInfoCollection;
+import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoFactory;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.skyframe.SkyFunctionName;
+
+import java.util.Objects;
+
+/**
+ * Value that stores {@link BuildInfoCollection}s generated by {@link BuildInfoFactory} instances.
+ * These collections are used during analysis (see {@code CachingAnalysisEnvironment}).
+ */
+public class BuildInfoCollectionValue extends ActionLookupValue {
+ private final BuildInfoCollection collection;
+
+ BuildInfoCollectionValue(BuildInfoCollection collection) {
+ super(collection.getActions());
+ this.collection = collection;
+ }
+
+ public BuildInfoCollection getCollection() {
+ return collection;
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public String toString() {
+ return com.google.common.base.Objects.toStringHelper(getClass())
+ .add("collection", collection)
+ .add("generatingActionMap", generatingActionMap).toString();
+ }
+
+ /** Key for BuildInfoCollectionValues. */
+ public static class BuildInfoKeyAndConfig extends ActionLookupKey {
+ private final BuildInfoFactory.BuildInfoKey infoKey;
+ private final BuildConfiguration config;
+
+ public BuildInfoKeyAndConfig(BuildInfoFactory.BuildInfoKey key, BuildConfiguration config) {
+ this.infoKey = Preconditions.checkNotNull(key, config);
+ this.config = Preconditions.checkNotNull(config, key);
+ }
+
+ @Override
+ SkyFunctionName getType() {
+ return SkyFunctions.BUILD_INFO_COLLECTION;
+ }
+
+ BuildInfoFactory.BuildInfoKey getInfoKey() {
+ return infoKey;
+ }
+
+ BuildConfiguration getConfig() {
+ return config;
+ }
+
+ @Override
+ public Label getLabel() {
+ return null;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(infoKey, config);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (other == null) {
+ return false;
+ }
+ if (this.getClass() != other.getClass()) {
+ return false;
+ }
+ BuildInfoKeyAndConfig that = (BuildInfoKeyAndConfig) other;
+ return Objects.equals(this.infoKey, that.infoKey) && Objects.equals(this.config, that.config);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/Builder.java b/src/main/java/com/google/devtools/build/lib/skyframe/Builder.java
new file mode 100644
index 0000000..7fdb55c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/Builder.java
@@ -0,0 +1,75 @@
+// 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.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.BuildFailedException;
+import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.actions.TestExecException;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadCompatible;
+import com.google.devtools.build.lib.util.AbruptExitException;
+
+import java.util.Collection;
+import java.util.Set;
+
+/**
+ * A Builder consumes top-level artifacts, targets, and tests,, and executes them in some
+ * topological order, possibly concurrently, using some dependency-checking policy.
+ *
+ * <p> The methods of the Builder interface are typically long-running, but honor the
+ * {@link java.lang.Thread#interrupt} contract: if an interrupt is delivered to the thread in which
+ * a call to buildTargets or buildArtifacts is active, the Builder attempts to terminate the call
+ * prematurely, throwing InterruptedException. No guarantee is made about the timeliness of such
+ * termination, as it depends on the ability of the Actions being executed to be interrupted, but
+ * typically any running subprocesses will be quickly killed.
+ */
+public interface Builder {
+
+ /**
+ * Transitively build all given artifacts, targets, and tests, and all necessary prerequisites
+ * thereof. For sequential implementations of this interface, the top-level requests will be
+ * built in the iteration order of the Set provided; for concurrent implementations, the order
+ * is undefined.
+ *
+ * <p>This method should not be invoked more than once concurrently on the same Builder instance.
+ *
+ * @param artifacts the set of Artifacts to build
+ * @param parallelTests tests to execute in parallel with the other top-level targetsToBuild and
+ * artifacts.
+ * @param exclusiveTests are executed one at a time, only after all other tasks have completed
+ * @param targetsToBuild Set of targets which will be built
+ * @param executor an opaque application-specific value that will be
+ * passed down to the execute() method of any Action executed during
+ * this call
+ * @param builtTargets (out) set of successfully built subset of targetsToBuild. This set is
+ * populated immediately upon confirmation that artifact is built so it will be
+ * valid even if a future action throws ActionExecutionException
+ * @throws BuildFailedException if there were problems establishing the action execution
+ * environment, if the the metadata of any file during the build could not be obtained,
+ * if any input files are missing, or if an action fails during execution
+ * @throws InterruptedException if there was an asynchronous stop request
+ * @throws TestExecException if any test fails
+ */
+ @ThreadCompatible
+ void buildArtifacts(Set<Artifact> artifacts,
+ Set<ConfiguredTarget> parallelTests,
+ Set<ConfiguredTarget> exclusiveTests,
+ Collection<ConfiguredTarget> targetsToBuild,
+ Executor executor,
+ Set<ConfiguredTarget> builtTargets,
+ boolean explain)
+ throws BuildFailedException, AbruptExitException, InterruptedException, TestExecException;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ConfigurationCollectionFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/ConfigurationCollectionFunction.java
new file mode 100644
index 0000000..89828c3
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ConfigurationCollectionFunction.java
@@ -0,0 +1,164 @@
+// 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.Supplier;
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.BuildConfigurationCollection;
+import com.google.devtools.build.lib.analysis.config.BuildConfigurationKey;
+import com.google.devtools.build.lib.analysis.config.BuildOptions;
+import com.google.devtools.build.lib.analysis.config.ConfigurationFactory;
+import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
+import com.google.devtools.build.lib.analysis.config.PackageProviderForConfigurations;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.events.StoredEventHandler;
+import com.google.devtools.build.lib.packages.Package;
+import com.google.devtools.build.lib.skyframe.ConfigurationCollectionValue.ConfigurationCollectionKey;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionException;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * A builder for {@link ConfigurationCollectionValue} instances.
+ */
+public class ConfigurationCollectionFunction implements SkyFunction {
+
+ private final Supplier<ConfigurationFactory> configurationFactory;
+ private final Supplier<Map<String, String>> clientEnv;
+ private final Supplier<Set<Package>> configurationPackages;
+
+ public ConfigurationCollectionFunction(
+ Supplier<ConfigurationFactory> configurationFactory,
+ Supplier<Map<String, String>> clientEnv,
+ Supplier<Set<Package>> configurationPackages) {
+ this.configurationFactory = configurationFactory;
+ this.clientEnv = clientEnv;
+ this.configurationPackages = configurationPackages;
+ }
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) throws InterruptedException,
+ ConfigurationCollectionFunctionException {
+ ConfigurationCollectionKey collectionKey = (ConfigurationCollectionKey) skyKey.argument();
+ try {
+ // We are not using this value, because test_environment can be created from clientEnv. But
+ // we want ConfigurationCollection to be recomputed each time when test_environment changes.
+ PrecomputedValue.TEST_ENVIRONMENT_VARIABLES.get(env);
+ BlazeDirectories directories = PrecomputedValue.BLAZE_DIRECTORIES.get(env);
+ if (env.valuesMissing()) {
+ return null;
+ }
+
+ BuildConfigurationCollection result =
+ getConfigurations(env.getListener(),
+ new SkyframePackageLoaderWithValueEnvironment(env, configurationPackages.get()),
+ new BuildConfigurationKey(collectionKey.getBuildOptions(), directories, clientEnv.get(),
+ collectionKey.getMultiCpu()));
+
+ // BuildConfigurationCollection can be created, but dependencies to some files might be
+ // missing. In that case we need to build configurationCollection again.
+ if (env.valuesMissing()) {
+ return null;
+ }
+
+ for (BuildConfiguration config : result.getTargetConfigurations()) {
+ config.declareSkyframeDependencies(env);
+ }
+ if (env.valuesMissing()) {
+ return null;
+ }
+ return new ConfigurationCollectionValue(result, configurationPackages.get());
+ } catch (InvalidConfigurationException e) {
+ throw new ConfigurationCollectionFunctionException(e);
+ }
+ }
+
+ /** Create the build configurations with the given options. */
+ private BuildConfigurationCollection getConfigurations(EventHandler eventHandler,
+ PackageProviderForConfigurations loadedPackageProvider, BuildConfigurationKey key)
+ throws InvalidConfigurationException {
+ List<BuildConfiguration> targetConfigurations = new ArrayList<>();
+ if (!key.getMultiCpu().isEmpty()) {
+ for (String cpu : key.getMultiCpu()) {
+ BuildConfiguration targetConfiguration = createConfiguration(
+ eventHandler, loadedPackageProvider, key, cpu);
+ if (targetConfiguration == null || targetConfigurations.contains(targetConfiguration)) {
+ continue;
+ }
+ targetConfigurations.add(targetConfiguration);
+ }
+ if (loadedPackageProvider.valuesMissing()) {
+ return null;
+ }
+ } else {
+ BuildConfiguration targetConfiguration = createConfiguration(
+ eventHandler, loadedPackageProvider, key, null);
+ if (targetConfiguration == null) {
+ return null;
+ }
+ targetConfigurations.add(targetConfiguration);
+ }
+ return new BuildConfigurationCollection(targetConfigurations);
+ }
+
+ @Nullable
+ public BuildConfiguration createConfiguration(
+ EventHandler originalEventListener,
+ PackageProviderForConfigurations loadedPackageProvider,
+ BuildConfigurationKey key, String cpuOverride) throws InvalidConfigurationException {
+ StoredEventHandler errorEventListener = new StoredEventHandler();
+ BuildOptions buildOptions = key.getBuildOptions();
+ if (cpuOverride != null) {
+ // TODO(bazel-team): Options classes should be immutable. This is a bit of a hack.
+ buildOptions = buildOptions.clone();
+ buildOptions.get(BuildConfiguration.Options.class).cpu = cpuOverride;
+ }
+
+ BuildConfiguration targetConfig = configurationFactory.get().createConfiguration(
+ loadedPackageProvider, buildOptions, key, errorEventListener);
+ if (targetConfig == null) {
+ return null;
+ }
+ errorEventListener.replayOn(originalEventListener);
+ if (errorEventListener.hasErrors()) {
+ throw new InvalidConfigurationException("Build options are invalid");
+ }
+ return targetConfig;
+ }
+
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+
+ /**
+ * Used to declare all the exception types that can be wrapped in the exception thrown by
+ * {@link ConfigurationCollectionFunction#compute}.
+ */
+ private static final class ConfigurationCollectionFunctionException extends
+ SkyFunctionException {
+ public ConfigurationCollectionFunctionException(InvalidConfigurationException e) {
+ super(e, Transience.PERSISTENT);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ConfigurationCollectionValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/ConfigurationCollectionValue.java
new file mode 100644
index 0000000..30e4fd7
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ConfigurationCollectionValue.java
@@ -0,0 +1,100 @@
+// 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.collect.ImmutableSet;
+import com.google.devtools.build.lib.analysis.config.BuildConfigurationCollection;
+import com.google.devtools.build.lib.analysis.config.BuildOptions;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.packages.Package;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.io.Serializable;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * A Skyframe value representing a build configuration collection.
+ */
+@Immutable
+@ThreadSafe
+public class ConfigurationCollectionValue implements SkyValue {
+
+ private final BuildConfigurationCollection configurationCollection;
+ private final ImmutableSet<Package> configurationPackages;
+
+ ConfigurationCollectionValue(BuildConfigurationCollection configurationCollection,
+ Set<Package> configurationPackages) {
+ this.configurationCollection = Preconditions.checkNotNull(configurationCollection);
+ this.configurationPackages = ImmutableSet.copyOf(configurationPackages);
+ }
+
+ public BuildConfigurationCollection getConfigurationCollection() {
+ return configurationCollection;
+ }
+
+ /**
+ * Returns set of packages required for configuration.
+ */
+ public Set<Package> getConfigurationPackages() {
+ return configurationPackages;
+ }
+
+ @ThreadSafe
+ public static SkyKey key(BuildOptions buildOptions, ImmutableSet<String> multiCpu) {
+ return new SkyKey(SkyFunctions.CONFIGURATION_COLLECTION,
+ new ConfigurationCollectionKey(buildOptions, multiCpu));
+ }
+
+ static final class ConfigurationCollectionKey implements Serializable {
+ private final BuildOptions buildOptions;
+ private final ImmutableSet<String> multiCpu;
+ private final int hashCode;
+
+ public ConfigurationCollectionKey(BuildOptions buildOptions, ImmutableSet<String> multiCpu) {
+ this.buildOptions = Preconditions.checkNotNull(buildOptions);
+ this.multiCpu = Preconditions.checkNotNull(multiCpu);
+ this.hashCode = Objects.hash(buildOptions, multiCpu);
+ }
+
+ public BuildOptions getBuildOptions() {
+ return buildOptions;
+ }
+
+ public ImmutableSet<String> getMultiCpu() {
+ return multiCpu;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof ConfigurationCollectionKey)) {
+ return false;
+ }
+ ConfigurationCollectionKey confObject = (ConfigurationCollectionKey) o;
+ return Objects.equals(multiCpu, confObject.multiCpu)
+ && Objects.equals(buildOptions, confObject.buildOptions);
+ }
+
+ @Override
+ public int hashCode() {
+ return hashCode;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ConfigurationFragmentFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/ConfigurationFragmentFunction.java
new file mode 100644
index 0000000..0393b16
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ConfigurationFragmentFunction.java
@@ -0,0 +1,146 @@
+// 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.Supplier;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration.Fragment;
+import com.google.devtools.build.lib.analysis.config.BuildOptions;
+import com.google.devtools.build.lib.analysis.config.ConfigurationEnvironment;
+import com.google.devtools.build.lib.analysis.config.ConfigurationFragmentFactory;
+import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
+import com.google.devtools.build.lib.analysis.config.PackageProviderForConfigurations;
+import com.google.devtools.build.lib.packages.NoSuchPackageException;
+import com.google.devtools.build.lib.packages.NoSuchTargetException;
+import com.google.devtools.build.lib.packages.Package;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.skyframe.ConfigurationFragmentValue.ConfigurationFragmentKey;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.Label.SyntaxException;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionException;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.io.IOException;
+import java.util.Set;
+
+/**
+ * A builder for {@link ConfigurationFragmentValue}s.
+ */
+public class ConfigurationFragmentFunction implements SkyFunction {
+
+ private final Supplier<ImmutableList<ConfigurationFragmentFactory>> configurationFragments;
+ private final Supplier<Set<Package>> configurationPackages;
+
+ public ConfigurationFragmentFunction(
+ Supplier<ImmutableList<ConfigurationFragmentFactory>> configurationFragments,
+ Supplier<Set<Package>> configurationPackages) {
+ this.configurationFragments = configurationFragments;
+ this.configurationPackages = configurationPackages;
+ }
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) throws InterruptedException,
+ ConfigurationFragmentFunctionException {
+ ConfigurationFragmentKey configurationFragmentKey =
+ (ConfigurationFragmentKey) skyKey.argument();
+ BuildOptions buildOptions = configurationFragmentKey.getBuildOptions();
+ ConfigurationFragmentFactory factory = getFactory(configurationFragmentKey.getFragmentType());
+ try {
+ PackageProviderForConfigurations loadedPackageProvider =
+ new SkyframePackageLoaderWithValueEnvironment(env, configurationPackages.get());
+ ConfigurationEnvironment confEnv = new ConfigurationBuilderEnvironment(loadedPackageProvider);
+ Fragment fragment = factory.create(confEnv, buildOptions);
+
+ if (env.valuesMissing()) {
+ return null;
+ }
+ return new ConfigurationFragmentValue(fragment);
+ } catch (InvalidConfigurationException e) {
+ // TODO(bazel-team): Rework the control-flow here so that we're not actually throwing this
+ // exception with missing Skyframe dependencies.
+ if (env.valuesMissing()) {
+ return null;
+ }
+ throw new ConfigurationFragmentFunctionException(e);
+ }
+ }
+
+ private ConfigurationFragmentFactory getFactory(Class<? extends Fragment> fragmentType) {
+ for (ConfigurationFragmentFactory factory : configurationFragments.get()) {
+ if (factory.creates().equals(fragmentType)) {
+ return factory;
+ }
+ }
+ throw new IllegalStateException(
+ "There is no factory for fragment: " + fragmentType.getSimpleName());
+ }
+
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+
+ /**
+ * A {@link ConfigurationEnvironment} implementation that can create dependencies on files.
+ */
+ private final class ConfigurationBuilderEnvironment implements ConfigurationEnvironment {
+ private final PackageProviderForConfigurations loadedPackageProvider;
+
+ ConfigurationBuilderEnvironment(
+ PackageProviderForConfigurations loadedPackageProvider) {
+ this.loadedPackageProvider = loadedPackageProvider;
+ }
+
+ @Override
+ public Target getTarget(Label label) throws NoSuchPackageException, NoSuchTargetException {
+ return loadedPackageProvider.getLoadedTarget(label);
+ }
+
+ @Override
+ public Path getPath(Package pkg, String fileName) {
+ Path result = pkg.getPackageDirectory().getRelative(fileName);
+ try {
+ loadedPackageProvider.addDependency(pkg, fileName);
+ } catch (IOException | SyntaxException e) {
+ return null;
+ }
+ return result;
+ }
+
+ @Override
+ public <T extends Fragment> T getFragment(BuildOptions buildOptions, Class<T> fragmentType)
+ throws InvalidConfigurationException {
+ return loadedPackageProvider.getFragment(buildOptions, fragmentType);
+ }
+
+ @Override
+ public BlazeDirectories getBlazeDirectories() {
+ return loadedPackageProvider.getDirectories();
+ }
+ }
+
+ /**
+ * Used to declare all the exception types that can be wrapped in the exception thrown by
+ * {@link ConfigurationFragmentFunction#compute}.
+ */
+ private static final class ConfigurationFragmentFunctionException extends SkyFunctionException {
+ public ConfigurationFragmentFunctionException(InvalidConfigurationException e) {
+ super(e, Transience.PERSISTENT);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ConfigurationFragmentValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/ConfigurationFragmentValue.java
new file mode 100644
index 0000000..cc07216
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ConfigurationFragmentValue.java
@@ -0,0 +1,90 @@
+// 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.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration.Fragment;
+import com.google.devtools.build.lib.analysis.config.BuildOptions;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+import javax.annotation.Nullable;
+
+/**
+ * A Skyframe node representing a build configuration fragment.
+ */
+@Immutable
+@ThreadSafe
+public class ConfigurationFragmentValue implements SkyValue {
+
+ @Nullable
+ private final BuildConfiguration.Fragment fragment;
+
+ ConfigurationFragmentValue(BuildConfiguration.Fragment fragment) {
+ this.fragment = fragment;
+ }
+
+ public BuildConfiguration.Fragment getFragment() {
+ return fragment;
+ }
+
+ @ThreadSafe
+ public static SkyKey key(BuildOptions buildOptions, Class<? extends Fragment> fragmentType) {
+ return new SkyKey(SkyFunctions.CONFIGURATION_FRAGMENT,
+ new ConfigurationFragmentKey(buildOptions, fragmentType));
+ }
+
+ static final class ConfigurationFragmentKey implements Serializable {
+ private final BuildOptions buildOptions;
+ private final Class<? extends Fragment> fragmentType;
+
+ public ConfigurationFragmentKey(BuildOptions buildOptions,
+ Class<? extends Fragment> fragmentType) {
+ this.buildOptions = Preconditions.checkNotNull(buildOptions);
+ this.fragmentType = Preconditions.checkNotNull(fragmentType);
+ }
+
+ public BuildOptions getBuildOptions() {
+ return buildOptions;
+ }
+
+ public Class<? extends Fragment> getFragmentType() {
+ return fragmentType;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof ConfigurationFragmentKey)) {
+ return false;
+ }
+ ConfigurationFragmentKey confObject = (ConfigurationFragmentKey) o;
+ return Objects.equals(fragmentType, confObject.fragmentType)
+ && Objects.equals(buildOptions, confObject.buildOptions);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(buildOptions, fragmentType);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ConfiguredTargetFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/ConfiguredTargetFunction.java
new file mode 100644
index 0000000..9fc3df4
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ConfiguredTargetFunction.java
@@ -0,0 +1,578 @@
+// 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.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.ListMultimap;
+import com.google.devtools.build.lib.analysis.Aspect;
+import com.google.devtools.build.lib.analysis.CachingAnalysisEnvironment;
+import com.google.devtools.build.lib.analysis.ConfiguredAspectFactory;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.DependencyResolver.Dependency;
+import com.google.devtools.build.lib.analysis.LabelAndConfiguration;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget;
+import com.google.devtools.build.lib.analysis.TargetAndConfiguration;
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.ConfigMatchingProvider;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.StoredEventHandler;
+import com.google.devtools.build.lib.packages.AspectDefinition;
+import com.google.devtools.build.lib.packages.AspectFactory;
+import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.InputFile;
+import com.google.devtools.build.lib.packages.NoSuchPackageException;
+import com.google.devtools.build.lib.packages.NoSuchTargetException;
+import com.google.devtools.build.lib.packages.NoSuchThingException;
+import com.google.devtools.build.lib.packages.PackageGroup;
+import com.google.devtools.build.lib.packages.RawAttributeMapper;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.packages.TargetUtils;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.skyframe.AspectFunction.AspectCreationException;
+import com.google.devtools.build.lib.skyframe.SkyframeExecutor.BuildViewProvider;
+import com.google.devtools.build.lib.syntax.EvalException;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionException;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+import com.google.devtools.build.skyframe.ValueOrException2;
+import com.google.devtools.build.skyframe.ValueOrException3;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * SkyFunction for {@link ConfiguredTargetValue}s.
+ */
+final class ConfiguredTargetFunction implements SkyFunction {
+
+ /**
+ * Exception class that signals an error during the evaluation of a dependency.
+ */
+ public static class DependencyEvaluationException extends Exception {
+ private final SkyKey rootCauseSkyKey;
+
+ public DependencyEvaluationException(Exception cause) {
+ super(cause);
+ this.rootCauseSkyKey = null;
+ }
+
+ public DependencyEvaluationException(SkyKey rootCauseSkyKey, Exception cause) {
+ super(cause);
+ this.rootCauseSkyKey = rootCauseSkyKey;
+ }
+
+ /**
+ * Returns the key of the root cause or null if the problem was with this target.
+ */
+ public SkyKey getRootCauseSkyKey() {
+ return rootCauseSkyKey;
+ }
+
+ @Override
+ public Exception getCause() {
+ return (Exception) super.getCause();
+ }
+ }
+
+ private static final Function<Dependency, SkyKey> TO_KEYS =
+ new Function<Dependency, SkyKey>() {
+ @Override
+ public SkyKey apply(Dependency input) {
+ return ConfiguredTargetValue.key(input.getLabel(), input.getConfiguration());
+ }
+ };
+
+ private final BuildViewProvider buildViewProvider;
+
+ ConfiguredTargetFunction(BuildViewProvider buildViewProvider) {
+ this.buildViewProvider = buildViewProvider;
+ }
+
+ @Override
+ public SkyValue compute(SkyKey key, Environment env) throws ConfiguredTargetFunctionException,
+ InterruptedException {
+ SkyframeBuildView view = buildViewProvider.getSkyframeBuildView();
+
+ ConfiguredTargetKey configuredTargetKey = (ConfiguredTargetKey) key.argument();
+ LabelAndConfiguration lc = LabelAndConfiguration.of(
+ configuredTargetKey.getLabel(), configuredTargetKey.getConfiguration());
+
+ BuildConfiguration configuration = lc.getConfiguration();
+
+ PackageValue packageValue =
+ (PackageValue) env.getValue(PackageValue.key(lc.getLabel().getPackageIdentifier()));
+ if (packageValue == null) {
+ return null;
+ }
+
+ Target target;
+ try {
+ target = packageValue.getPackage().getTarget(lc.getLabel().getName());
+ } catch (NoSuchTargetException e1) {
+ throw new ConfiguredTargetFunctionException(new NoSuchTargetException(lc.getLabel(),
+ "No such target"));
+ }
+ // TODO(bazel-team): This is problematic - we create the right key, but then end up with a value
+ // that doesn't match; we can even have the same value multiple times. However, I think it's
+ // only triggered in tests (i.e., in normal operation, the configuration passed in is already
+ // null).
+ if (target instanceof InputFile) {
+ // InputFileConfiguredTarget expects its configuration to be null since it's not used.
+ configuration = null;
+ } else if (target instanceof PackageGroup) {
+ // Same for PackageGroupConfiguredTarget.
+ configuration = null;
+ }
+ TargetAndConfiguration ctgValue =
+ new TargetAndConfiguration(target, configuration);
+
+ SkyframeDependencyResolver resolver = view.createDependencyResolver(env);
+ if (resolver == null) {
+ return null;
+ }
+
+ try {
+ // Get the configuration targets that trigger this rule's configurable attributes.
+ Set<ConfigMatchingProvider> configConditions =
+ getConfigConditions(ctgValue.getTarget(), env, resolver, ctgValue);
+ if (configConditions == null) {
+ // Those targets haven't yet been resolved.
+ return null;
+ }
+
+ ListMultimap<Attribute, ConfiguredTarget> depValueMap =
+ computeDependencies(env, resolver, ctgValue, null, configConditions);
+ return createConfiguredTarget(
+ view, env, target, configuration, depValueMap, configConditions);
+ } catch (DependencyEvaluationException e) {
+ throw new ConfiguredTargetFunctionException(e.getRootCauseSkyKey(), e.getCause());
+ }
+ }
+
+ /**
+ * Computes the direct dependencies of a node in the configured target graph (a configured
+ * target or an aspect).
+ *
+ * <p>Returns null if Skyframe hasn't evaluated the required dependencies yet. In this case, the
+ * caller should also return null to Skyframe.
+ *
+ * @param env the Skyframe environment
+ * @param resolver The dependency resolver
+ * @param ctgValue The label and the configuration of the node
+ * @param aspectDefinition the aspect of the node (if null, the node is a configured target,
+ * otherwise it's an asect)
+ * @param configConditions the configuration conditions for evaluating the attributes of the node
+ * @return an attribute -> direct dependency multimap
+ * @throws ConfiguredTargetFunctionException
+ */
+ @Nullable
+ static ListMultimap<Attribute, ConfiguredTarget> computeDependencies(
+ Environment env, SkyframeDependencyResolver resolver, TargetAndConfiguration ctgValue,
+ AspectDefinition aspectDefinition, Set<ConfigMatchingProvider> configConditions)
+ throws DependencyEvaluationException {
+
+ // 1. Create the map from attributes to list of (target, configuration) pairs.
+ ListMultimap<Attribute, Dependency> depValueNames;
+ try {
+ depValueNames = resolver.dependentNodeMap(ctgValue, aspectDefinition, configConditions);
+ } catch (EvalException e) {
+ env.getListener().handle(Event.error(e.getLocation(), e.getMessage()));
+ throw new DependencyEvaluationException(new ConfiguredValueCreationException(e.print()));
+ }
+
+ // 2. Resolve configured target dependencies and handle errors.
+ Map<SkyKey, ConfiguredTarget> depValues =
+ resolveConfiguredTargetDependencies(env, depValueNames.values(), ctgValue.getTarget());
+ if (depValues == null) {
+ return null;
+ }
+
+ // 3. Resolve required aspects.
+ ListMultimap<SkyKey, Aspect> depAspects = resolveAspectDependencies(
+ env, depValues, depValueNames.values());
+ if (depAspects == null) {
+ return null;
+ }
+
+ // 3. Merge the dependent configured targets and aspects into a single map.
+ return mergeAspects(depValueNames, depValues, depAspects);
+ }
+
+ /**
+ * Merges the each direct dependency configured target with the aspects associated with it.
+ *
+ * <p>Note that the combination of a configured target and its associated aspects are not
+ * represented by a Skyframe node. This is because there can possibly be many different
+ * combinations of aspects for a particular configured target, so it would result in a
+ * combinatiorial explosion of Skyframe nodes.
+ */
+ private static ListMultimap<Attribute, ConfiguredTarget> mergeAspects(
+ ListMultimap<Attribute, Dependency> depValueNames,
+ Map<SkyKey, ConfiguredTarget> depConfiguredTargetMap,
+ ListMultimap<SkyKey, Aspect> depAspectMap) {
+ ListMultimap<Attribute, ConfiguredTarget> result = ArrayListMultimap.create();
+
+ for (Map.Entry<Attribute, Dependency> entry : depValueNames.entries()) {
+ Dependency dep = entry.getValue();
+ SkyKey depKey = TO_KEYS.apply(dep);
+ ConfiguredTarget depConfiguredTarget = depConfiguredTargetMap.get(depKey);
+ result.put(entry.getKey(),
+ RuleConfiguredTarget.mergeAspects(depConfiguredTarget, depAspectMap.get(depKey)));
+ }
+
+ return result;
+ }
+
+ /**
+ * Given a list of {@link Dependency} objects, returns a multimap from the {@link SkyKey} of the
+ * dependency to the {@link Aspect} instances that should be merged into it.
+ *
+ * <p>Returns null if the required aspects are not computed yet.
+ */
+ @Nullable
+ private static ListMultimap<SkyKey, Aspect> resolveAspectDependencies(Environment env,
+ Map<SkyKey, ConfiguredTarget> configuredTargetMap, Iterable<Dependency> deps)
+ throws DependencyEvaluationException {
+ ListMultimap<SkyKey, Aspect> result = ArrayListMultimap.create();
+ Set<SkyKey> aspectKeys = new HashSet<>();
+ for (Dependency dep : deps) {
+ for (Class<? extends ConfiguredAspectFactory> depAspect : dep.getAspects()) {
+ aspectKeys.add(AspectValue.key(dep.getLabel(), dep.getConfiguration(), depAspect));
+ }
+ }
+
+ Map<SkyKey, ValueOrException3<
+ AspectCreationException, NoSuchThingException, ConfiguredValueCreationException>>
+ depAspects = env.getValuesOrThrow(aspectKeys, AspectCreationException.class,
+ NoSuchThingException.class, ConfiguredValueCreationException.class);
+
+ for (Dependency dep : deps) {
+ SkyKey depKey = TO_KEYS.apply(dep);
+ ConfiguredTarget depConfiguredTarget = configuredTargetMap.get(depKey);
+ List<AspectValue> aspects = new ArrayList<>();
+ for (Class<? extends ConfiguredAspectFactory> depAspect : dep.getAspects()) {
+ if (!aspectMatchesConfiguredTarget(depConfiguredTarget, depAspect)) {
+ continue;
+ }
+
+ SkyKey aspectKey = AspectValue.key(dep.getLabel(), dep.getConfiguration(), depAspect);
+ AspectValue aspectValue = null;
+ try {
+ aspectValue = (AspectValue) depAspects.get(aspectKey).get();
+ } catch (ConfiguredValueCreationException e) {
+ // The configured target should have been created in resolveConfiguredTargetDependencies()
+ throw new IllegalStateException(e);
+ } catch (NoSuchThingException | AspectCreationException e) {
+ AspectFactory depAspectFactory = AspectFactory.Util.create(depAspect);
+ throw new DependencyEvaluationException(new ConfiguredValueCreationException(
+ String.format("Evaluation of aspect %s on %s failed: %s",
+ depAspectFactory.getDefinition().getName(), dep.getLabel(), e.toString())));
+ }
+
+ if (aspectValue == null) {
+ // Dependent aspect has either not been computed yet or is in error.
+ return null;
+ }
+ result.put(depKey, aspectValue.get());
+ }
+ }
+
+ return result;
+ }
+
+ private static boolean aspectMatchesConfiguredTarget(ConfiguredTarget dep,
+ Class<? extends ConfiguredAspectFactory> aspectFactory) {
+ AspectDefinition aspectDefinition = AspectFactory.Util.create(aspectFactory).getDefinition();
+ for (Class<?> provider : aspectDefinition.getRequiredProviders()) {
+ if (dep.getProvider((Class<? extends TransitiveInfoProvider>) provider) == null) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns which aspects are computable based on the precise set of providers direct dependencies
+ * publish (and not the upper estimate in their rule definition).
+ *
+ * <p>An aspect is computable for a particular configured target if the configured target supplies
+ * all the providers the aspect requires.
+ *
+ * @param upperEstimate a multimap from attribute to the upper estimates computed by
+ * {@link com.google.devtools.build.lib.analysis.DependencyResolver}.
+ * @param configuredTargetDeps a multimap from attribute to the directly dependent configured
+ * targets
+ * @return a multimap from attribute to the more precise {@link Dependency} objects
+ */
+ private static ListMultimap<Attribute, Dependency> getComputableAspects(
+ ListMultimap<Attribute, Dependency> upperEstimate,
+ Map<SkyKey, ConfiguredTarget> configuredTargetDeps) {
+ ListMultimap<Attribute, Dependency> result = ArrayListMultimap.create();
+ for (Map.Entry<Attribute, Dependency> entry : upperEstimate.entries()) {
+ ConfiguredTarget dep =
+ configuredTargetDeps.get(TO_KEYS.apply(entry.getValue()));
+ List<Class<? extends ConfiguredAspectFactory>> depAspects = new ArrayList<>();
+ for (Class<? extends ConfiguredAspectFactory> candidate : entry.getValue().getAspects()) {
+ boolean ok = true;
+ for (Class<?> requiredProvider :
+ AspectFactory.Util.create(candidate).getDefinition().getRequiredProviders()) {
+ if (dep.getProvider((Class<? extends TransitiveInfoProvider>) requiredProvider) == null) {
+ ok = false;
+ break;
+ }
+ }
+
+ if (ok) {
+ depAspects.add(candidate);
+ }
+ }
+
+ result.put(entry.getKey(), new Dependency(
+ entry.getValue().getLabel(), entry.getValue().getConfiguration(),
+ ImmutableSet.copyOf(depAspects)));
+ }
+
+ return result;
+ }
+
+ /**
+ * Returns the set of {@link ConfigMatchingProvider}s that key the configurable attributes
+ * used by this rule.
+ *
+ * <p>>If the configured targets supplying those providers aren't yet resolved by the
+ * dependency resolver, returns null.
+ */
+ @Nullable
+ static Set<ConfigMatchingProvider> getConfigConditions(Target target, Environment env,
+ SkyframeDependencyResolver resolver, TargetAndConfiguration ctgValue)
+ throws DependencyEvaluationException {
+ if (!(target instanceof Rule)) {
+ return ImmutableSet.of();
+ }
+
+ ImmutableSet.Builder<ConfigMatchingProvider> configConditions = ImmutableSet.builder();
+
+ // Collect the labels of the configured targets we need to resolve.
+ ListMultimap<Attribute, LabelAndConfiguration> configLabelMap = ArrayListMultimap.create();
+ RawAttributeMapper attributeMap = RawAttributeMapper.of(((Rule) target));
+ for (Attribute a : ((Rule) target).getAttributes()) {
+ for (Label configLabel : attributeMap.getConfigurabilityKeys(a.getName(), a.getType())) {
+ if (!Type.Selector.isReservedLabel(configLabel)) {
+ configLabelMap.put(a, LabelAndConfiguration.of(
+ configLabel, ctgValue.getConfiguration()));
+ }
+ }
+ }
+ if (configLabelMap.isEmpty()) {
+ return ImmutableSet.of();
+ }
+
+ // Collect the corresponding Skyframe configured target values. Abort early if they haven't
+ // been computed yet.
+ Collection<Dependency> configValueNames =
+ resolver.resolveRuleLabels(ctgValue, null, configLabelMap);
+ Map<SkyKey, ConfiguredTarget> configValues =
+ resolveConfiguredTargetDependencies(env, configValueNames, target);
+ if (configValues == null) {
+ return null;
+ }
+
+ // Get the configured targets as ConfigMatchingProvider interfaces.
+ for (Dependency entry : configValueNames) {
+ ConfiguredTarget value = configValues.get(TO_KEYS.apply(entry));
+ // The code above guarantees that value is non-null here.
+ ConfigMatchingProvider provider = value.getProvider(ConfigMatchingProvider.class);
+ if (provider != null) {
+ configConditions.add(provider);
+ } else {
+ // Not a valid provider for configuration conditions.
+ String message =
+ entry.getLabel() + " is not a valid configuration key for " + target.getLabel();
+ env.getListener().handle(Event.error(TargetUtils.getLocationMaybe(target), message));
+ throw new DependencyEvaluationException(new ConfiguredValueCreationException(message));
+ }
+ }
+
+ return configConditions.build();
+ }
+
+ /***
+ * Resolves the targets referenced in depValueNames and returns their ConfiguredTarget
+ * instances.
+ *
+ * <p>Returns null if not all instances are available yet.
+ *
+ */
+ @Nullable
+ private static Map<SkyKey, ConfiguredTarget> resolveConfiguredTargetDependencies(
+ Environment env, Collection<Dependency> deps, Target target)
+ throws DependencyEvaluationException {
+ boolean ok = !env.valuesMissing();
+ String message = null;
+ Iterable<SkyKey> depKeys = Iterables.transform(deps, TO_KEYS);
+ // TODO(bazel-team): maybe having a two-exception argument is better than typing a generic
+ // Exception here.
+ Map<SkyKey, ValueOrException2<NoSuchTargetException,
+ NoSuchPackageException>> depValuesOrExceptions = env.getValuesOrThrow(depKeys,
+ NoSuchTargetException.class, NoSuchPackageException.class);
+ Map<SkyKey, ConfiguredTarget> depValues = new HashMap<>(depValuesOrExceptions.size());
+ SkyKey childKey = null;
+ NoSuchThingException transitiveChildException = null;
+ for (Map.Entry<SkyKey, ValueOrException2<NoSuchTargetException, NoSuchPackageException>> entry
+ : depValuesOrExceptions.entrySet()) {
+ ConfiguredTargetKey depKey = (ConfiguredTargetKey) entry.getKey().argument();
+ LabelAndConfiguration depLabelAndConfiguration = LabelAndConfiguration.of(
+ depKey.getLabel(), depKey.getConfiguration());
+ Label depLabel = depLabelAndConfiguration.getLabel();
+ ConfiguredTargetValue depValue = null;
+ NoSuchThingException directChildException = null;
+ try {
+ depValue = (ConfiguredTargetValue) entry.getValue().get();
+ } catch (NoSuchTargetException e) {
+ if (depLabel.equals(e.getLabel())) {
+ directChildException = e;
+ } else {
+ childKey = entry.getKey();
+ transitiveChildException = e;
+ }
+ } catch (NoSuchPackageException e) {
+ if (depLabel.getPackageName().equals(e.getPackageName())) {
+ directChildException = e;
+ } else {
+ childKey = entry.getKey();
+ transitiveChildException = e;
+ }
+ }
+ // If an exception wasn't caused by a direct child target value, we'll treat it the same
+ // as any other missing dep by setting ok = false below, and returning null at the end.
+ if (directChildException != null) {
+ // Only update messages for missing targets we depend on directly.
+ message = TargetUtils.formatMissingEdge(target, depLabel, directChildException);
+ env.getListener().handle(Event.error(TargetUtils.getLocationMaybe(target), message));
+ }
+
+ if (depValue == null) {
+ ok = false;
+ } else {
+ depValues.put(entry.getKey(), depValue.getConfiguredTarget());
+ }
+ }
+ if (message != null) {
+ throw new DependencyEvaluationException(new NoSuchTargetException(message));
+ }
+ if (childKey != null) {
+ throw new DependencyEvaluationException(childKey, transitiveChildException);
+ }
+ if (!ok) {
+ return null;
+ } else {
+ return depValues;
+ }
+ }
+
+
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return Label.print(((ConfiguredTargetKey) skyKey.argument()).getLabel());
+ }
+
+ @Nullable
+ private ConfiguredTargetValue createConfiguredTarget(SkyframeBuildView view,
+ Environment env, Target target, BuildConfiguration configuration,
+ ListMultimap<Attribute, ConfiguredTarget> depValueMap,
+ Set<ConfigMatchingProvider> configConditions)
+ throws ConfiguredTargetFunctionException,
+ InterruptedException {
+ boolean extendedSanityChecks = configuration != null && configuration.extendedSanityChecks();
+
+ StoredEventHandler events = new StoredEventHandler();
+ BuildConfiguration ownerConfig = (configuration == null)
+ ? null : configuration.getArtifactOwnerConfiguration();
+ boolean allowRegisteringActions = configuration == null || configuration.isActionsEnabled();
+ CachingAnalysisEnvironment analysisEnvironment = view.createAnalysisEnvironment(
+ new ConfiguredTargetKey(target.getLabel(), ownerConfig), false,
+ extendedSanityChecks, events, env, allowRegisteringActions);
+ if (env.valuesMissing()) {
+ return null;
+ }
+
+ ConfiguredTarget configuredTarget = view.createConfiguredTarget(target, configuration,
+ analysisEnvironment, depValueMap, configConditions);
+
+ events.replayOn(env.getListener());
+ if (events.hasErrors()) {
+ analysisEnvironment.disable(target);
+ throw new ConfiguredTargetFunctionException(new ConfiguredValueCreationException(
+ "Analysis of target '" + target.getLabel() + "' failed; build aborted"));
+ }
+ Preconditions.checkState(!analysisEnvironment.hasErrors(),
+ "Analysis environment hasError() but no errors reported");
+ if (env.valuesMissing()) {
+ return null;
+ }
+
+ analysisEnvironment.disable(target);
+ Preconditions.checkNotNull(configuredTarget, target);
+
+ return new ConfiguredTargetValue(configuredTarget,
+ ImmutableList.copyOf(analysisEnvironment.getRegisteredActions()));
+ }
+
+ /**
+ * An exception indicating that there was a problem during the construction of
+ * a ConfiguredTargetValue.
+ */
+ public static final class ConfiguredValueCreationException extends Exception {
+
+ public ConfiguredValueCreationException(String message) {
+ super(message);
+ }
+ }
+
+ /**
+ * Used to declare all the exception types that can be wrapped in the exception thrown by
+ * {@link ConfiguredTargetFunction#compute}.
+ */
+ public static final class ConfiguredTargetFunctionException extends SkyFunctionException {
+ public ConfiguredTargetFunctionException(NoSuchTargetException e) {
+ super(e, Transience.PERSISTENT);
+ }
+
+ private ConfiguredTargetFunctionException(ConfiguredValueCreationException error) {
+ super(error, Transience.PERSISTENT);
+ };
+
+ private ConfiguredTargetFunctionException(
+ @Nullable SkyKey childKey, Exception transitiveError) {
+ super(transitiveError, childKey);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ConfiguredTargetKey.java b/src/main/java/com/google/devtools/build/lib/skyframe/ConfiguredTargetKey.java
new file mode 100644
index 0000000..ea744c1
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ConfiguredTargetKey.java
@@ -0,0 +1,96 @@
+// 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.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.skyframe.SkyFunctionName;
+
+import java.util.Objects;
+
+import javax.annotation.Nullable;
+
+/**
+ * A (Label, Configuration) pair. Note that this pair may be used to look up the generating action
+ * of an artifact. Callers may want to ensure that they have the correct configuration for this
+ * purpose by passing in {@link BuildConfiguration#getArtifactOwnerConfiguration} in preference to
+ * the raw configuration.
+ */
+public class ConfiguredTargetKey extends ActionLookupValue.ActionLookupKey {
+ private final Label label;
+ @Nullable
+ private final BuildConfiguration configuration;
+
+ public ConfiguredTargetKey(Label label, @Nullable BuildConfiguration configuration) {
+ this.label = Preconditions.checkNotNull(label);
+ this.configuration = configuration;
+ }
+
+ public ConfiguredTargetKey(ConfiguredTarget rule) {
+ this(rule.getTarget().getLabel(), rule.getConfiguration());
+ }
+
+ @Override
+ public Label getLabel() {
+ return label;
+ }
+
+ @Override
+ SkyFunctionName getType() {
+ return SkyFunctions.CONFIGURED_TARGET;
+ }
+
+ @Nullable
+ public BuildConfiguration getConfiguration() {
+ return configuration;
+ }
+
+ @Override
+ public int hashCode() {
+ int configVal = configuration == null ? 79 : configuration.hashCode();
+ return 31 * label.hashCode() + configVal;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (!(obj instanceof ConfiguredTargetKey)) {
+ return false;
+ }
+ ConfiguredTargetKey other = (ConfiguredTargetKey) obj;
+ return Objects.equals(label, other.label) && Objects.equals(configuration, other.configuration);
+ }
+
+ public String prettyPrint() {
+ if (label == null) {
+ return "null";
+ }
+ return (configuration != null && configuration.isHostConfiguration())
+ ? (label.toString() + " (host)") : label.toString();
+ }
+
+ @Override
+ public String toString() {
+ return label + " " + (configuration == null ? "null" : configuration.shortCacheKey());
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ConfiguredTargetValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/ConfiguredTargetValue.java
new file mode 100644
index 0000000..200e05f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ConfiguredTargetValue.java
@@ -0,0 +1,105 @@
+// 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.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.skyframe.SkyKey;
+
+import javax.annotation.Nullable;
+
+/**
+ * A configured target in the context of a Skyframe graph.
+ */
+@Immutable
+@ThreadSafe
+@VisibleForTesting
+public final class ConfiguredTargetValue extends ActionLookupValue {
+
+ // These variables are only non-final because they may be clear()ed to save memory. They are null
+ // only after they are cleared.
+ @Nullable private ConfiguredTarget configuredTarget;
+
+ // We overload this variable to check whether the value has been clear()ed. We don't use a
+ // separate variable in order to save memory.
+ @Nullable private volatile Iterable<Action> actions;
+
+ ConfiguredTargetValue(ConfiguredTarget configuredTarget, Iterable<Action> actions) {
+ super(actions);
+ this.configuredTarget = configuredTarget;
+ this.actions = actions;
+ }
+
+ @VisibleForTesting
+ public ConfiguredTarget getConfiguredTarget() {
+ Preconditions.checkNotNull(actions, configuredTarget);
+ return configuredTarget;
+ }
+
+ @VisibleForTesting
+ public Iterable<Action> getActions() {
+ return Preconditions.checkNotNull(actions, configuredTarget);
+ }
+
+ /**
+ * Clears configured target data from this value, leaving only the artifact->generating action
+ * map.
+ *
+ * <p>Should only be used when user specifies --discard_analysis_cache. Must be called at most
+ * once per value, after which {@link #getConfiguredTarget} and {@link #getActions} cannot be
+ * called.
+ */
+ public void clear() {
+ Preconditions.checkNotNull(actions, configuredTarget);
+ configuredTarget = null;
+ actions = null;
+ }
+
+ @VisibleForTesting
+ public static SkyKey key(Label label, BuildConfiguration configuration) {
+ return key(new ConfiguredTargetKey(label, configuration));
+ }
+
+ static ImmutableList<SkyKey> keys(Iterable<ConfiguredTargetKey> lacs) {
+ ImmutableList.Builder<SkyKey> keys = ImmutableList.builder();
+ for (ConfiguredTargetKey lac : lacs) {
+ keys.add(key(lac));
+ }
+ return keys.build();
+ }
+
+ /**
+ * Returns a label of ConfiguredTargetValue.
+ */
+ @ThreadSafe
+ static Label extractLabel(SkyKey value) {
+ Object valueName = value.argument();
+ Preconditions.checkState(valueName instanceof ConfiguredTargetKey, valueName);
+ return ((ConfiguredTargetKey) valueName).getLabel();
+ }
+
+ @Override
+ public String toString() {
+ return "ConfiguredTargetValue: "
+ + configuredTarget + ", actions: " + (actions == null ? null : Iterables.toString(actions));
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ContainingPackageLookupFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/ContainingPackageLookupFunction.java
new file mode 100644
index 0000000..58cb67d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ContainingPackageLookupFunction.java
@@ -0,0 +1,55 @@
+// 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.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import javax.annotation.Nullable;
+
+/**
+ * SkyFunction for {@link ContainingPackageLookupValue}s.
+ */
+public class ContainingPackageLookupFunction implements SkyFunction {
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) {
+ PackageIdentifier dir = (PackageIdentifier) skyKey.argument();
+ PackageLookupValue pkgLookupValue = null;
+ pkgLookupValue = (PackageLookupValue) env.getValue(PackageLookupValue.key(dir));
+ if (pkgLookupValue == null) {
+ return null;
+ }
+
+ if (pkgLookupValue.packageExists()) {
+ return ContainingPackageLookupValue.withContainingPackage(dir, pkgLookupValue.getRoot());
+ }
+
+ PathFragment parentDir = dir.getPackageFragment().getParentDirectory();
+ if (parentDir == null) {
+ return ContainingPackageLookupValue.noContainingPackage();
+ }
+ PackageIdentifier parentId = new PackageIdentifier(dir.getRepository(), parentDir);
+ return env.getValue(ContainingPackageLookupValue.key(parentId));
+ }
+
+ @Nullable
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ContainingPackageLookupValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/ContainingPackageLookupValue.java
new file mode 100644
index 0000000..16516b5
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ContainingPackageLookupValue.java
@@ -0,0 +1,111 @@
+// 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.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+/**
+ * A value that represents the result of looking for the existence of a package that owns a
+ * specific directory path. Compare with {@link PackageLookupValue}, which deals with existence of
+ * a specific package.
+ */
+public abstract class ContainingPackageLookupValue implements SkyValue {
+ /** Returns whether there is a containing package. */
+ public abstract boolean hasContainingPackage();
+
+ /** If there is a containing package, returns its name. */
+ abstract PackageIdentifier getContainingPackageName();
+
+ /** If there is a containing package, returns its package root */
+ public abstract Path getContainingPackageRoot();
+
+ public static SkyKey key(PackageIdentifier id) {
+ Preconditions.checkArgument(!id.getPackageFragment().isAbsolute(), id);
+ return new SkyKey(SkyFunctions.CONTAINING_PACKAGE_LOOKUP, id);
+ }
+
+ static ContainingPackageLookupValue noContainingPackage() {
+ return NoContainingPackage.INSTANCE;
+ }
+
+ static ContainingPackageLookupValue withContainingPackage(PackageIdentifier pkgId, Path root) {
+ return new ContainingPackage(pkgId, root);
+ }
+
+ private static class NoContainingPackage extends ContainingPackageLookupValue {
+ private static final NoContainingPackage INSTANCE = new NoContainingPackage();
+
+ @Override
+ public boolean hasContainingPackage() {
+ return false;
+ }
+
+ @Override
+ public PackageIdentifier getContainingPackageName() {
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public Path getContainingPackageRoot() {
+ throw new IllegalStateException();
+ }
+ }
+
+ private static class ContainingPackage extends ContainingPackageLookupValue {
+ private final PackageIdentifier containingPackage;
+ private final Path containingPackageRoot;
+
+ private ContainingPackage(PackageIdentifier pkgId, Path containingPackageRoot) {
+ this.containingPackage = pkgId;
+ this.containingPackageRoot = containingPackageRoot;
+ }
+
+ @Override
+ public boolean hasContainingPackage() {
+ return true;
+ }
+
+ @Override
+ public PackageIdentifier getContainingPackageName() {
+ return containingPackage;
+ }
+
+ @Override
+ public Path getContainingPackageRoot() {
+ return containingPackageRoot;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof ContainingPackage)) {
+ return false;
+ }
+ ContainingPackage other = (ContainingPackage) obj;
+ return containingPackage.equals(other.containingPackage)
+ && containingPackageRoot.equals(other.containingPackageRoot);
+ }
+
+ @Override
+ public int hashCode() {
+ return containingPackage.hashCode();
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/CoverageReportFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/CoverageReportFunction.java
new file mode 100644
index 0000000..8d6fe3d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/CoverageReportFunction.java
@@ -0,0 +1,49 @@
+// 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.devtools.build.lib.actions.Action;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+/**
+ * A Skyframe function to calculate the coverage report Action and Artifacts.
+ */
+public class CoverageReportFunction implements SkyFunction {
+ CoverageReportFunction() {}
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) {
+ Preconditions.checkState(
+ CoverageReportValue.SKY_KEY.equals(skyKey), String.format(
+ "Expected %s for SkyKey but got %s instead", CoverageReportValue.SKY_KEY, skyKey));
+
+ Action action = PrecomputedValue.COVERAGE_REPORT_KEY.get(env);
+ if (action == null) {
+ return null;
+ }
+
+ return new CoverageReportValue(
+ action.getOutputs(),
+ action);
+ }
+
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/CoverageReportValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/CoverageReportValue.java
new file mode 100644
index 0000000..862e381
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/CoverageReportValue.java
@@ -0,0 +1,55 @@
+// 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.collect.ImmutableSet;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.ArtifactOwner;
+import com.google.devtools.build.skyframe.SkyFunctionName;
+import com.google.devtools.build.skyframe.SkyKey;
+
+/**
+ * A SkyValue to store the coverage report Action and Artifacts.
+ */
+public class CoverageReportValue extends ActionLookupValue {
+ private final ImmutableSet<Artifact> coverageReportArtifacts;
+
+ // There should only ever be one CoverageReportValue value in the graph.
+ public static final SkyKey SKY_KEY = new SkyKey(SkyFunctions.COVERAGE_REPORT, "COVERAGE_REPORT");
+ public static final ArtifactOwner ARTIFACT_OWNER = new CoverageReportKey();
+
+ public CoverageReportValue(ImmutableSet<Artifact> coverageReportArtifacts,
+ Action coverageReportAction) {
+ super(coverageReportAction);
+ this.coverageReportArtifacts = coverageReportArtifacts;
+ }
+
+ public ImmutableSet<Artifact> getCoverageReportArtifacts() {
+ return coverageReportArtifacts;
+ }
+
+ private static class CoverageReportKey extends ActionLookupKey {
+ @Override
+ SkyFunctionName getType() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ SkyKey getSkyKey() {
+ return SKY_KEY;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/DiffAwareness.java b/src/main/java/com/google/devtools/build/lib/skyframe/DiffAwareness.java
new file mode 100644
index 0000000..d0f4c99
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/DiffAwareness.java
@@ -0,0 +1,82 @@
+// 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.devtools.build.lib.vfs.ModifiedFileSet;
+import com.google.devtools.build.lib.vfs.Path;
+
+import java.io.Closeable;
+
+import javax.annotation.Nullable;
+
+/**
+ * Interface for computing modifications of files under a package path entry.
+ *
+ * <p> Skyframe has a {@link DiffAwareness} instance per package-path entry, and each instance is
+ * responsible for all files under its path entry. At the beginning of each incremental build,
+ * skyframe queries for changes using {@link #getDiff}. Ideally, {@link #getDiff} should be
+ * constant-time; if it were linear in the number of files of interest, we might as well just
+ * detect modifications manually.
+ */
+public interface DiffAwareness extends Closeable {
+
+ /** Factory for creating {@link DiffAwareness} instances. */
+ public interface Factory {
+ /**
+ * Returns a {@link DiffAwareness} instance suitable for managing changes to files under the
+ * given package path entry, or {@code null} if this factory cannot create such an instance.
+ *
+ * <p> Skyframe has a collection of factories, and will create a {@link DiffAwareness} instance
+ * per package path entry using one of the factories that returns a non-null value.
+ */
+ @Nullable
+ DiffAwareness maybeCreate(Path pathEntry);
+ }
+
+ /** Opaque view of the filesystem under a package path entry at a specific point in time. */
+ interface View {
+ }
+
+ /**
+ * Returns the live view of the filesystem under the package path entry.
+ *
+ * @throws BrokenDiffAwarenessException if something is wrong and the caller should discard this
+ * {@link DiffAwareness} instance. The {@link DiffAwareness} is expected to close itself in
+ * this case.
+ */
+ View getCurrentView() throws BrokenDiffAwarenessException;
+
+ /**
+ * Returns the set of files of interest that have been modified between the given two views.
+ *
+ * <p>The given views must have come from previous calls to {@link #getCurrentView} on the
+ * {@link DiffAwareness} instance (i.e. using a {@link View} from another instance is not
+ * supported).
+ *
+ * @throws IncompatibleViewException if the given views are not compatible with this
+ * {@link DiffAwareness} instance. This probably indicates a bug.
+ * @throws BrokenDiffAwarenessException if something is wrong and the caller should discard this
+ * {@link DiffAwareness} instance. The {@link DiffAwareness} is expected to close itself in
+ * this case.
+ */
+ ModifiedFileSet getDiff(View oldView, View newView)
+ throws IncompatibleViewException, BrokenDiffAwarenessException;
+
+ /**
+ * Must be called whenever the {@link DiffAwareness} object is to be discarded. Using a
+ * {@link DiffAwareness} instance after calling {@link #close} on it is unspecified behavior.
+ */
+ @Override
+ void close();
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/DiffAwarenessManager.java b/src/main/java/com/google/devtools/build/lib/skyframe/DiffAwarenessManager.java
new file mode 100644
index 0000000..d1bb81e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/DiffAwarenessManager.java
@@ -0,0 +1,188 @@
+// 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.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.Reporter;
+import com.google.devtools.build.lib.skyframe.DiffAwareness.View;
+import com.google.devtools.build.lib.vfs.ModifiedFileSet;
+import com.google.devtools.build.lib.vfs.Path;
+
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+/**
+ * Helper class to make it easier to correctly use the {@link DiffAwareness} interface in a
+ * sequential manner.
+ */
+public final class DiffAwarenessManager {
+
+ private final ImmutableSet<? extends DiffAwareness.Factory> diffAwarenessFactories;
+ private Map<Path, DiffAwarenessState> currentDiffAwarenessStates = Maps.newHashMap();
+ private final Reporter reporter;
+
+ public DiffAwarenessManager(Iterable<? extends DiffAwareness.Factory> diffAwarenessFactories,
+ Reporter reporter) {
+ this.diffAwarenessFactories = ImmutableSet.copyOf(diffAwarenessFactories);
+ this.reporter = reporter;
+ }
+
+ private static class DiffAwarenessState {
+ private final DiffAwareness diffAwareness;
+ /**
+ * The {@link View} that should be the baseline for the next {@link #getDiff} call, or
+ * {@code null} if the next {@link #getDiff} will be the first incremental one.
+ */
+ @Nullable
+ private View baselineView;
+
+ private DiffAwarenessState(DiffAwareness diffAwareness, @Nullable View baselineView) {
+ this.diffAwareness = diffAwareness;
+ this.baselineView = baselineView;
+ }
+ }
+
+ /** Reset internal {@link DiffAwareness} state. */
+ public void reset() {
+ for (DiffAwarenessState diffAwarenessState : currentDiffAwarenessStates.values()) {
+ diffAwarenessState.diffAwareness.close();
+ }
+ currentDiffAwarenessStates.clear();
+ }
+
+ /** A set of modified files that should be marked as processed. */
+ public interface ProcessableModifiedFileSet {
+ ModifiedFileSet getModifiedFileSet();
+
+ /**
+ * This should be called when the changes have been noted. Otherwise, the result from the next
+ * call to {@link #getDiff} will be from the baseline of the old, unprocessed, diff.
+ */
+ void markProcessed();
+ }
+
+ /**
+ * Gets the set of changed files since the last call with this path entry, or
+ * {@code ModifiedFileSet.EVERYTHING_MODIFIED} if this is the first such call.
+ */
+ public ProcessableModifiedFileSet getDiff(Path pathEntry) {
+ DiffAwarenessState diffAwarenessState = maybeGetDiffAwarenessState(pathEntry);
+ if (diffAwarenessState == null) {
+ return BrokenProcessableModifiedFileSet.INSTANCE;
+ }
+ DiffAwareness diffAwareness = diffAwarenessState.diffAwareness;
+ View newView;
+ try {
+ newView = diffAwareness.getCurrentView();
+ } catch (BrokenDiffAwarenessException e) {
+ handleBrokenDiffAwareness(pathEntry, e);
+ return BrokenProcessableModifiedFileSet.INSTANCE;
+ }
+
+ View baselineView = diffAwarenessState.baselineView;
+ if (baselineView == null) {
+ diffAwarenessState.baselineView = newView;
+ return BrokenProcessableModifiedFileSet.INSTANCE;
+ }
+
+ ModifiedFileSet diff;
+ try {
+ diff = diffAwareness.getDiff(baselineView, newView);
+ } catch (BrokenDiffAwarenessException e) {
+ handleBrokenDiffAwareness(pathEntry, e);
+ return BrokenProcessableModifiedFileSet.INSTANCE;
+ } catch (IncompatibleViewException e) {
+ throw new IllegalStateException(pathEntry + " " + baselineView + " " + newView, e);
+ }
+ ProcessableModifiedFileSet result = new ProcessableModifiedFileSetImpl(diff, pathEntry,
+ newView);
+ return result;
+ }
+
+ private void handleBrokenDiffAwareness(Path pathEntry, BrokenDiffAwarenessException e) {
+ currentDiffAwarenessStates.remove(pathEntry);
+ reporter.handle(Event.warn(e.getMessage() + "... temporarily falling back to manually "
+ + "checking files for changes"));
+ }
+
+ /**
+ * Returns the current diff awareness for the given path entry, or a fresh one if there is no
+ * current one, or otherwise {@code null} if no factory could make a fresh one.
+ */
+ @Nullable
+ private DiffAwarenessState maybeGetDiffAwarenessState(Path pathEntry) {
+ DiffAwarenessState diffAwarenessState = currentDiffAwarenessStates.get(pathEntry);
+ if (diffAwarenessState != null) {
+ return diffAwarenessState;
+ }
+ for (DiffAwareness.Factory factory : diffAwarenessFactories) {
+ DiffAwareness newDiffAwareness = factory.maybeCreate(pathEntry);
+ if (newDiffAwareness != null) {
+ diffAwarenessState = new DiffAwarenessState(newDiffAwareness, /*previousView=*/null);
+ currentDiffAwarenessStates.put(pathEntry, diffAwarenessState);
+ return diffAwarenessState;
+ }
+ }
+ return null;
+ }
+
+ private class ProcessableModifiedFileSetImpl implements ProcessableModifiedFileSet {
+
+ private final ModifiedFileSet modifiedFileSet;
+ private final Path pathEntry;
+ /**
+ * The {@link View} that should be the baseline on the next {@link #getDiff} call after
+ * {@link #markProcessed} is called.
+ */
+ private final View nextView;
+
+ private ProcessableModifiedFileSetImpl(ModifiedFileSet modifiedFileSet, Path pathEntry,
+ View nextView) {
+ this.modifiedFileSet = modifiedFileSet;
+ this.pathEntry = pathEntry;
+ this.nextView = nextView;
+ }
+
+ @Override
+ public ModifiedFileSet getModifiedFileSet() {
+ return modifiedFileSet;
+ }
+
+ @Override
+ public void markProcessed() {
+ DiffAwarenessState diffAwarenessState = currentDiffAwarenessStates.get(pathEntry);
+ if (diffAwarenessState != null) {
+ diffAwarenessState.baselineView = nextView;
+ }
+ }
+ }
+
+ private static class BrokenProcessableModifiedFileSet implements ProcessableModifiedFileSet {
+
+ private static final BrokenProcessableModifiedFileSet INSTANCE =
+ new BrokenProcessableModifiedFileSet();
+
+ @Override
+ public ModifiedFileSet getModifiedFileSet() {
+ return ModifiedFileSet.EVERYTHING_MODIFIED;
+ }
+
+ @Override
+ public void markProcessed() {
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/DirectoryListingFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/DirectoryListingFunction.java
new file mode 100644
index 0000000..93c3d75
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/DirectoryListingFunction.java
@@ -0,0 +1,72 @@
+// 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.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.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import javax.annotation.Nullable;
+
+/**
+ * A {@link SkyFunction} for {@link DirectoryListingValue}s.
+ */
+final class DirectoryListingFunction implements SkyFunction {
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env)
+ throws DirectoryListingFunctionException {
+ RootedPath dirRootedPath = (RootedPath) skyKey.argument();
+
+ FileValue dirFileValue = (FileValue) env.getValue(FileValue.key(dirRootedPath));
+ if (dirFileValue == null) {
+ return null;
+ }
+
+ RootedPath realDirRootedPath = dirFileValue.realRootedPath();
+ if (!dirFileValue.isDirectory()) {
+ // Recall that the directory is assumed to exist (see DirectoryListingValue#key).
+ throw new DirectoryListingFunctionException(new InconsistentFilesystemException(
+ dirRootedPath.asPath() + " is no longer an existing directory. Did you delete it during "
+ + "the build?"));
+ }
+
+ DirectoryListingStateValue directoryListingStateValue =
+ (DirectoryListingStateValue) env.getValue(DirectoryListingStateValue.key(
+ realDirRootedPath));
+ if (directoryListingStateValue == null) {
+ return null;
+ }
+
+ return DirectoryListingValue.value(dirRootedPath, dirFileValue, directoryListingStateValue);
+ }
+
+ @Nullable
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+
+ /**
+ * Used to declare all the exception types that can be wrapped in the exception thrown by
+ * {@link DirectoryListingFunction#compute}.
+ */
+ private static final class DirectoryListingFunctionException extends SkyFunctionException {
+ public DirectoryListingFunctionException(InconsistentFilesystemException e) {
+ super(e, Transience.TRANSIENT);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/DirectoryListingStateFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/DirectoryListingStateFunction.java
new file mode 100644
index 0000000..6e47a2d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/DirectoryListingStateFunction.java
@@ -0,0 +1,68 @@
+// 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.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.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.io.IOException;
+
+/**
+ * A {@link SkyFunction} for {@link DirectoryListingStateValue}s.
+ *
+ * <p>Merely calls DirectoryListingStateValue#create, but also has special handling for
+ * directories outside the package roots (see {@link ExternalFilesHelper}).
+ */
+public class DirectoryListingStateFunction implements SkyFunction {
+
+ private final ExternalFilesHelper externalFilesHelper;
+
+ public DirectoryListingStateFunction(ExternalFilesHelper externalFilesHelper) {
+ this.externalFilesHelper = externalFilesHelper;
+ }
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env)
+ throws DirectoryListingStateFunctionException {
+ RootedPath dirRootedPath = (RootedPath) skyKey.argument();
+ externalFilesHelper.maybeAddDepOnBuildId(dirRootedPath, env);
+ if (env.valuesMissing()) {
+ return null;
+ }
+ try {
+ return DirectoryListingStateValue.create(dirRootedPath);
+ } catch (IOException e) {
+ throw new DirectoryListingStateFunctionException(e);
+ }
+ }
+
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+
+ /**
+ * Used to declare all the exception types that can be wrapped in the exception thrown by
+ * {@link DirectoryListingStateFunction#compute}.
+ */
+ private static final class DirectoryListingStateFunctionException
+ extends SkyFunctionException {
+ public DirectoryListingStateFunctionException(IOException e) {
+ super(e, Transience.TRANSIENT);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/DirectoryListingStateValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/DirectoryListingStateValue.java
new file mode 100644
index 0000000..87b9748
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/DirectoryListingStateValue.java
@@ -0,0 +1,214 @@
+// 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.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.vfs.Dirent;
+import com.google.devtools.build.lib.vfs.Dirent.Type;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.lib.vfs.Symlinks;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.BitSet;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.Objects;
+
+/**
+ * Encapsulates the filesystem operations needed to get the directory entries of a directory.
+ *
+ * <p>This class is an implementation detail of {@link DirectoryListingValue}.
+ */
+final class DirectoryListingStateValue implements SkyValue {
+
+ private final CompactSortedDirents compactSortedDirents;
+
+ private DirectoryListingStateValue(Collection<Dirent> dirents) {
+ this.compactSortedDirents = CompactSortedDirents.create(dirents);
+ }
+
+ @VisibleForTesting
+ public static DirectoryListingStateValue createForTesting(Collection<Dirent> dirents) {
+ return new DirectoryListingStateValue(dirents);
+ }
+
+ public static DirectoryListingStateValue create(RootedPath dirRootedPath) throws IOException {
+ Collection<Dirent> dirents = dirRootedPath.asPath().readdir(Symlinks.NOFOLLOW);
+ return new DirectoryListingStateValue(dirents);
+ }
+
+ @ThreadSafe
+ public static SkyKey key(RootedPath rootedPath) {
+ return new SkyKey(SkyFunctions.DIRECTORY_LISTING_STATE, rootedPath);
+ }
+
+ /**
+ * Returns the directory entries for this directory, in a stable order.
+ *
+ * <p>Symlinks are not expanded.
+ */
+ public Iterable<Dirent> getDirents() {
+ return compactSortedDirents;
+ }
+
+ @Override
+ public int hashCode() {
+ return compactSortedDirents.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof DirectoryListingStateValue)) {
+ return false;
+ }
+ DirectoryListingStateValue other = (DirectoryListingStateValue) obj;
+ return compactSortedDirents.equals(other.compactSortedDirents);
+ }
+
+ /** A space-efficient, sorted, immutable dirent structure. */
+ private static class CompactSortedDirents implements Iterable<Dirent>, Serializable {
+
+ private final String[] names;
+ private final BitSet packedTypes;
+
+ private CompactSortedDirents(String[] names, BitSet packedTypes) {
+ this.names = names;
+ this.packedTypes = packedTypes;
+ }
+
+ public static CompactSortedDirents create(Collection<Dirent> dirents) {
+ final Dirent[] direntArray = dirents.toArray(new Dirent[dirents.size()]);
+ Integer[] indices = new Integer[dirents.size()];
+ for (int i = 0; i < dirents.size(); i++) {
+ indices[i] = i;
+ }
+ Arrays.sort(indices,
+ new Comparator<Integer>() {
+ @Override
+ public int compare(Integer o1, Integer o2) {
+ return direntArray[o1].getName().compareTo(direntArray[o2].getName());
+ }
+ });
+ String[] names = new String[dirents.size()];
+ BitSet packedTypes = new BitSet(dirents.size() * 2);
+ for (int i = 0; i < dirents.size(); i++) {
+ Dirent dirent = direntArray[indices[i]];
+ names[i] = dirent.getName();
+ packType(packedTypes, dirent.getType(), i);
+ }
+ return new CompactSortedDirents(names, packedTypes);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof CompactSortedDirents)) {
+ return false;
+ }
+ if (this == obj) {
+ return true;
+ }
+ CompactSortedDirents other = (CompactSortedDirents) obj;
+ return Arrays.equals(names, other.names) && packedTypes.equals(other.packedTypes);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(Arrays.hashCode(names), packedTypes);
+ }
+
+ @Override
+ public Iterator<Dirent> iterator() {
+ return new Iterator<Dirent>() {
+
+ private int i = 0;
+
+ @Override
+ public boolean hasNext() {
+ return i < size();
+ }
+
+ @Override
+ public Dirent next() {
+ return direntAt(i++);
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ };
+ }
+
+ private int size() {
+ return names.length;
+ }
+
+ /** Returns the type of the ith dirent. */
+ private Dirent.Type unpackType(int i) {
+ int start = i * 2;
+ boolean upper = packedTypes.get(start);
+ boolean lower = packedTypes.get(start + 1);
+ if (!upper && !lower) {
+ return Type.FILE;
+ } else if (!upper && lower){
+ return Type.DIRECTORY;
+ } else if (upper && !lower) {
+ return Type.SYMLINK;
+ } else {
+ return Type.UNKNOWN;
+ }
+ }
+
+ /** Sets the type of the ith dirent. */
+ private static void packType(BitSet bitSet, Dirent.Type type, int i) {
+ int start = i * 2;
+ switch (type) {
+ case FILE:
+ pack(bitSet, start, false, false);
+ break;
+ case DIRECTORY:
+ pack(bitSet, start, false, true);
+ break;
+ case SYMLINK:
+ pack(bitSet, start, true, false);
+ break;
+ case UNKNOWN:
+ pack(bitSet, start, true, true);
+ break;
+ default:
+ throw new IllegalStateException("Unknown dirent type: " + type);
+ }
+ }
+
+ private static void pack(BitSet bitSet, int start, boolean upper, boolean lower) {
+ bitSet.set(start, upper);
+ bitSet.set(start + 1, lower);
+ }
+
+ private Dirent direntAt(int i) {
+ Preconditions.checkState(i >= 0 && i < size(), "i: %s, size: %s", i, size());
+ return new Dirent(names[i], unpackType(i));
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/DirectoryListingValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/DirectoryListingValue.java
new file mode 100644
index 0000000..3fe6dba
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/DirectoryListingValue.java
@@ -0,0 +1,134 @@
+// 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.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.vfs.Dirent;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.util.Objects;
+
+/**
+ * A value that represents the list of files in a given directory under a given package path root.
+ * Anything in Skyframe that cares about the contents of a directory should have a dependency
+ * on the corresponding {@link DirectoryListingValue}.
+ *
+ * <p>This value only depends on the FileValue corresponding to the directory. In particular, note
+ * that it does not depend on any of its child entries.
+ *
+ * <p>Note that symlinks in dirents are <b>not</b> expanded. Dependents of the value are responsible
+ * for expanding the symlink entries by referring to FileValues that correspond to the symlinks.
+ * This is a little onerous, but correct: we do not need to reread the directory when a symlink
+ * inside it changes, therefore this value should not be invalidated in that case.
+ */
+@Immutable
+@ThreadSafe
+abstract class DirectoryListingValue implements SkyValue {
+
+ /**
+ * Returns the directory entries for this directory, in a stable order.
+ *
+ * <p>Symlinks are not expanded.
+ */
+ public abstract Iterable<Dirent> getDirents();
+
+ /**
+ * Returns a {@link SkyKey} for getting the directory entries of the given directory. The
+ * given path is assumed to be an existing directory (e.g. via {@link FileValue#isDirectory} or
+ * from a directory listing on its parent directory).
+ */
+ @ThreadSafe
+ static SkyKey key(RootedPath directoryUnderRoot) {
+ return new SkyKey(SkyFunctions.DIRECTORY_LISTING, directoryUnderRoot);
+ }
+
+ static DirectoryListingValue value(RootedPath dirRootedPath, FileValue dirFileValue,
+ DirectoryListingStateValue realDirectoryListingStateValue) {
+ return dirFileValue.realRootedPath().equals(dirRootedPath)
+ ? new RegularDirectoryListingValue(realDirectoryListingStateValue)
+ : new DifferentRealPathDirectoryListingValue(dirFileValue.realRootedPath(),
+ realDirectoryListingStateValue);
+ }
+
+ @ThreadSafe
+ private static final class RegularDirectoryListingValue extends DirectoryListingValue {
+
+ private final DirectoryListingStateValue directoryListingStateValue;
+
+ private RegularDirectoryListingValue(DirectoryListingStateValue directoryListingStateValue) {
+ this.directoryListingStateValue = directoryListingStateValue;
+ }
+
+ @Override
+ public Iterable<Dirent> getDirents() {
+ return directoryListingStateValue.getDirents();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof RegularDirectoryListingValue)) {
+ return false;
+ }
+ RegularDirectoryListingValue other = (RegularDirectoryListingValue) obj;
+ return directoryListingStateValue.equals(other.directoryListingStateValue);
+ }
+
+ @Override
+ public int hashCode() {
+ return directoryListingStateValue.hashCode();
+ }
+ }
+
+ @ThreadSafe
+ private static final class DifferentRealPathDirectoryListingValue extends DirectoryListingValue {
+
+ private final RootedPath realDirRootedPath;
+ private final DirectoryListingStateValue directoryListingStateValue;
+
+ private DifferentRealPathDirectoryListingValue(RootedPath realDirRootedPath,
+ DirectoryListingStateValue directoryListingStateValue) {
+ this.realDirRootedPath = realDirRootedPath;
+ this.directoryListingStateValue = directoryListingStateValue;
+ }
+
+ @Override
+ public Iterable<Dirent> getDirents() {
+ return directoryListingStateValue.getDirents();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof DifferentRealPathDirectoryListingValue)) {
+ return false;
+ }
+ DifferentRealPathDirectoryListingValue other = (DifferentRealPathDirectoryListingValue) obj;
+ return realDirRootedPath.equals(other.realDirRootedPath)
+ && directoryListingStateValue.equals(other.directoryListingStateValue);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(realDirRootedPath, directoryListingStateValue);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ErrorReadingSkylarkExtensionException.java b/src/main/java/com/google/devtools/build/lib/skyframe/ErrorReadingSkylarkExtensionException.java
new file mode 100644
index 0000000..8593afb
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ErrorReadingSkylarkExtensionException.java
@@ -0,0 +1,21 @@
+// 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;
+
+/** Indicates some sort of IO error while dealing with a Skylark extension. */
+public class ErrorReadingSkylarkExtensionException extends Exception {
+ public ErrorReadingSkylarkExtensionException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ExternalFilesHelper.java b/src/main/java/com/google/devtools/build/lib/skyframe/ExternalFilesHelper.java
new file mode 100644
index 0000000..ce858de
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ExternalFilesHelper.java
@@ -0,0 +1,97 @@
+// 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.collect.ImmutableSet;
+import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.skyframe.SkyFunction;
+
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
+
+/** Common utilities for dealing with files outside the package roots. */
+class ExternalFilesHelper {
+
+ private final AtomicReference<PathPackageLocator> pkgLocator;
+ private final Set<Path> immutableDirs;
+
+ ExternalFilesHelper(AtomicReference<PathPackageLocator> pkgLocator) {
+ this(pkgLocator, ImmutableSet.<Path>of());
+ }
+
+ ExternalFilesHelper(AtomicReference<PathPackageLocator> pkgLocator, Set<Path> immutableDirs) {
+ this.pkgLocator = pkgLocator;
+ this.immutableDirs = immutableDirs;
+ }
+
+ private enum FileType {
+ // A file inside the package roots.
+ INTERNAL_FILE,
+
+ // A file outside the package roots that we may pretends is immutable.
+ EXTERNAL_IMMUTABLE_FILE,
+
+ // A file outside the package roots about which we may make no other assumptions.
+ EXTERNAL_MUTABLE_FILE,
+ }
+
+ private FileType getFileType(RootedPath rootedPath) {
+ // TODO(bazel-team): This is inefficient when there are a lot of package roots or there are a
+ // lot of immutable directories. Consider either explicitly preventing this case or using a more
+ // efficient approach here (e.g. use a trie for determing if a file is under an immutable
+ // directory).
+ if (!pkgLocator.get().getPathEntries().contains(rootedPath.getRoot())) {
+ Path path = rootedPath.asPath();
+ for (Path immutableDir : immutableDirs) {
+ if (path.startsWith(immutableDir)) {
+ return FileType.EXTERNAL_IMMUTABLE_FILE;
+ }
+ }
+ return FileType.EXTERNAL_MUTABLE_FILE;
+ }
+ return FileType.INTERNAL_FILE;
+ }
+
+ public boolean shouldAssumeImmutable(RootedPath rootedPath) {
+ return getFileType(rootedPath) == FileType.EXTERNAL_IMMUTABLE_FILE;
+ }
+
+ public void maybeAddDepOnBuildId(RootedPath rootedPath, SkyFunction.Environment env) {
+ if (getFileType(rootedPath) == FileType.EXTERNAL_MUTABLE_FILE) {
+ // For files outside the package roots that are not assumed to be immutable, add a dependency
+ // on the build_id. This is sufficient for correctness; all other files will be handled by
+ // diff awareness of their respective package path, but these files need to be addressed
+ // separately.
+ //
+ // Using the build_id here seems to introduce a performance concern because the upward
+ // transitive closure of these external files will get eagerly invalidated on each
+ // incremental build (e.g. if every file had a transitive dependency on the filesystem root,
+ // then we'd have a big performance problem). But this a non-issue by design:
+ // - We don't add a dependency on the parent directory at the package root boundary, so the
+ // only transitive dependencies from files inside the package roots to external files are
+ // through symlinks. So the upwards transitive closure of external files is small.
+ // - The only way external source files get into the skyframe graph in the first place is
+ // through symlinks outside the package roots, which we neither want to encourage nor
+ // optimize for since it is not common. So the set of external files is small.
+ //
+ // The above reasoning doesn't hold for bazel, because external repositories
+ // (e.g. new_local_repository) cause lots of external symlinks to be present in the build.
+ // So bazel pretends that these external repositories are immutable to avoid the performance
+ // penalty described above.
+ PrecomputedValue.dependOnBuildId(env);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/FileAndMetadataCache.java b/src/main/java/com/google/devtools/build/lib/skyframe/FileAndMetadataCache.java
new file mode 100644
index 0000000..2a0de78
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/FileAndMetadataCache.java
@@ -0,0 +1,466 @@
+// 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.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Interner;
+import com.google.common.collect.Interners;
+import com.google.common.collect.Sets;
+import com.google.common.io.BaseEncoding;
+import com.google.devtools.build.lib.actions.ActionInput;
+import com.google.devtools.build.lib.actions.ActionInputFileCache;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.cache.Digest;
+import com.google.devtools.build.lib.actions.cache.DigestUtils;
+import com.google.devtools.build.lib.actions.cache.Metadata;
+import com.google.devtools.build.lib.actions.cache.MetadataHandler;
+import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor;
+import com.google.devtools.build.lib.vfs.FileStatus;
+import com.google.devtools.build.lib.vfs.FileStatusWithDigest;
+import com.google.devtools.build.lib.vfs.FileStatusWithDigestAdapter;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.lib.vfs.Symlinks;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.protobuf.ByteString;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import javax.annotation.Nullable;
+
+/**
+ * Cache provided by an {@link ActionExecutionFunction}, allowing Blaze to obtain data from the
+ * graph and to inject data (e.g. file digests) back into the graph.
+ *
+ * <p>Data for the action's inputs is injected into this cache on construction, using the graph as
+ * the source of truth.
+ *
+ * <p>As well, this cache collects data about the action's output files, which is used in three
+ * ways. First, it is served as requested during action execution, primarily by the {@code
+ * ActionCacheChecker} when determining if the action must be rerun, and then after the action is
+ * run, to gather information about the outputs. Second, it is accessed by {@link
+ * ArtifactFunction}s in order to construct {@link ArtifactValue}s. Third, the {@link
+ * FilesystemValueChecker} uses it to determine the set of output files to check for inter-build
+ * modifications. Because all these use cases are slightly different, we must occasionally store two
+ * versions of the data for a value (see {@link #getAdditionalOutputData} for more.
+ */
+@VisibleForTesting
+public class FileAndMetadataCache implements ActionInputFileCache, MetadataHandler {
+ /** This should never be read directly. Use {@link #getInputFileArtifactValue} instead. */
+ private final Map<Artifact, FileArtifactValue> inputArtifactData;
+ private final Map<Artifact, Collection<Artifact>> expandedInputMiddlemen;
+ private final File execRoot;
+ private final Map<ByteString, Artifact> reverseMap = new ConcurrentHashMap<>();
+ private final ConcurrentMap<Artifact, FileValue> outputArtifactData =
+ new ConcurrentHashMap<>();
+ // See #getAdditionalOutputData for documentation of this field.
+ private final ConcurrentMap<Artifact, FileArtifactValue> additionalOutputData =
+ new ConcurrentHashMap<>();
+ private final Set<Artifact> injectedArtifacts = Sets.newConcurrentHashSet();
+ private final ImmutableSet<Artifact> outputs;
+ @Nullable private final SkyFunction.Environment env;
+ private final TimestampGranularityMonitor tsgm;
+
+ private static final Interner<ByteString> BYTE_INTERNER = Interners.newWeakInterner();
+
+ public FileAndMetadataCache(Map<Artifact, FileArtifactValue> inputArtifactData,
+ Map<Artifact, Collection<Artifact>> expandedInputMiddlemen, File execRoot,
+ Iterable<Artifact> outputs, @Nullable SkyFunction.Environment env,
+ TimestampGranularityMonitor tsgm) {
+ this.inputArtifactData = Preconditions.checkNotNull(inputArtifactData);
+ this.expandedInputMiddlemen = Preconditions.checkNotNull(expandedInputMiddlemen);
+ this.execRoot = Preconditions.checkNotNull(execRoot);
+ this.outputs = ImmutableSet.copyOf(outputs);
+ this.env = env;
+ this.tsgm = tsgm;
+ }
+
+ @Override
+ public Metadata getMetadataMaybe(Artifact artifact) {
+ try {
+ return getMetadata(artifact);
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ private static Metadata metadataFromValue(FileArtifactValue value) throws FileNotFoundException {
+ if (value == FileArtifactValue.MISSING_FILE_MARKER) {
+ throw new FileNotFoundException();
+ }
+ // If the file is empty or a directory, we need to return the mtime because the action cache
+ // uses mtime to determine if this artifact has changed. We do not optimize for this code
+ // path (by storing the mtime somewhere) because we eventually may be switching to use digests
+ // for empty files. We want this code path to go away somehow too for directories (maybe by
+ // implementing FileSet in Skyframe).
+ return value.getSize() > 0
+ ? new Metadata(value.getDigest())
+ : new Metadata(value.getModifiedTime());
+ }
+
+ @Override
+ public Metadata getMetadata(Artifact artifact) throws IOException {
+ Metadata metadata = getRealMetadata(artifact);
+ return artifact.isConstantMetadata() ? Metadata.CONSTANT_METADATA : metadata;
+ }
+
+ @Nullable
+ private FileArtifactValue getInputFileArtifactValue(ActionInput input) {
+ FileArtifactValue value = inputArtifactData.get(input);
+ if (value != null) {
+ return value;
+ }
+ if (outputs.contains(input)) {
+ // When this method is called to calculate the metadata of an artifact, the artifact may be an
+ // output artifact. Don't try to do anything then.
+ return null;
+ }
+ if (!(input instanceof Artifact)) {
+ // Maybe we're being asked for some strange constructed ActionInput coming from runfiles or
+ // similar. We have no information about such things.
+ return null;
+ }
+ // TODO(bazel-team): Remove this codepath once Skyframe has native input discovery, so all
+ // inputs will already have metadata known.
+ // ActionExecutionFunction may have passed in null environment if this action does not
+ // discover inputs. In which case we should not have gotten here.
+ Preconditions.checkNotNull(env, input);
+ Artifact artifact = (Artifact) input;
+ if (artifact.isSourceArtifact()) {
+ // We might have no artifact data for discovered source inputs, and it's not worth storing
+ // it in this cache, because it won't be reused across actions -- while we could request an
+ // artifact from the graph, we would have to be tolerant to it not yet being present in the
+ // graph yet, which adds complexity. Instead, we let the undeclared inputs handler stat it, so
+ // it can be reused.
+ return null;
+ } else {
+ // This getValue call is not expected to return null, because if the artifact is a
+ // transitive dependency of this action (as it should be), it will already have been built,
+ // so this call will return a value.
+ // This getValue call is theoretically less efficient for a subsequent incremental build
+ // than it would be to do a bulk getValues call after action execution, as is done for
+ // source inputs. However, since almost all nodes requested here are transitive deps of an
+ // already-declared dependency, change pruning will request these values serially, but they
+ // will already have been built. So the only penalty is restarting ParallelEvaluator#run, as
+ // opposed to traversing the entire downward transitive closure on a single thread.
+ value = (FileArtifactValue) env.getValue(
+ FileArtifactValue.key(artifact, /*argument ignored for derived artifacts*/false));
+ return value;
+ }
+ }
+
+ /**
+ * We cache data for constant-metadata artifacts, even though it is technically unnecessary,
+ * because the data stored in this cache is consumed by various parts of Blaze via the {@link
+ * ActionExecutionValue} (for now, {@link FilesystemValueChecker} and {@link ArtifactFunction}).
+ * It is simpler for those parts if every output of the action is present in the cache. However,
+ * we must not return the actual metadata for a constant-metadata artifact.
+ */
+ private Metadata getRealMetadata(Artifact artifact) throws IOException {
+ FileArtifactValue value = getInputFileArtifactValue(artifact);
+ if (value != null) {
+ return metadataFromValue(value);
+ }
+ if (artifact.isSourceArtifact()) {
+ // A discovered input we didn't have data for.
+ // TODO(bazel-team): Change this to an assertion once Skyframe has native input discovery, so
+ // all inputs will already have metadata known.
+ return null;
+ } else if (artifact.isMiddlemanArtifact()) {
+ // A middleman artifact's data was either already injected from the action cache checker using
+ // #setDigestForVirtualArtifact, or it has the default middleman value.
+ value = additionalOutputData.get(artifact);
+ if (value != null) {
+ return metadataFromValue(value);
+ }
+ value = FileArtifactValue.DEFAULT_MIDDLEMAN;
+ FileArtifactValue oldValue = additionalOutputData.putIfAbsent(artifact, value);
+ checkInconsistentData(artifact, oldValue, value);
+ return metadataFromValue(value);
+ }
+ FileValue fileValue = outputArtifactData.get(artifact);
+ if (fileValue != null) {
+ // Non-middleman artifacts should only have additionalOutputData if they have
+ // outputArtifactData. We don't assert this because of concurrency possibilities, but at least
+ // we don't check additionalOutputData unless we expect that we might see the artifact there.
+ value = additionalOutputData.get(artifact);
+ // If additional output data is present for this artifact, we use it in preference to the
+ // usual calculation.
+ if (value != null) {
+ return metadataFromValue(value);
+ }
+ if (!fileValue.exists()) {
+ throw new FileNotFoundException(artifact.prettyPrint() + " does not exist");
+ }
+ return new Metadata(Preconditions.checkNotNull(fileValue.getDigest(), artifact));
+ }
+ // We do not cache exceptions besides nonexistence here, because it is unlikely that the file
+ // will be requested from this cache too many times.
+ fileValue = fileValueFromArtifact(artifact, null, tsgm);
+ FileValue oldFileValue = outputArtifactData.putIfAbsent(artifact, fileValue);
+ checkInconsistentData(artifact, oldFileValue, value);
+ return maybeStoreAdditionalData(artifact, fileValue, null);
+ }
+
+ /** Expands one of the input middlemen artifacts of the corresponding action. */
+ public Collection<Artifact> expandInputMiddleman(Artifact middlemanArtifact) {
+ Preconditions.checkState(middlemanArtifact.isMiddlemanArtifact(), middlemanArtifact);
+ Collection<Artifact> result = expandedInputMiddlemen.get(middlemanArtifact);
+ // Note that result may be null for non-aggregating middlemen.
+ return result == null ? ImmutableSet.<Artifact>of() : result;
+ }
+
+ /**
+ * Check that the new {@code data} we just calculated for an {@code artifact} agrees with the
+ * {@code oldData} (presumably calculated concurrently), if it was present.
+ */
+ // Not private only because used by SkyframeActionExecutor's metadata handler.
+ static void checkInconsistentData(Artifact artifact,
+ @Nullable Object oldData, Object data) throws IOException {
+ if (oldData != null && !oldData.equals(data)) {
+ // Another thread checked this file since we looked at the map, and got a different answer
+ // than we did. Presumably the user modified the file between reads.
+ throw new IOException("Data for " + artifact.prettyPrint() + " changed to " + data
+ + " after it was calculated as " + oldData);
+ }
+ }
+
+ /**
+ * See {@link #getAdditionalOutputData} for why we sometimes need to store additional data, even
+ * for normal (non-middleman) artifacts.
+ */
+ @Nullable
+ private Metadata maybeStoreAdditionalData(Artifact artifact, FileValue data,
+ @Nullable byte[] injectedDigest) throws IOException {
+ if (!data.exists()) {
+ // Nonexistent files should only occur before executing an action.
+ throw new FileNotFoundException(artifact.prettyPrint() + " does not exist");
+ }
+ boolean isFile = data.isFile();
+ boolean useDigest = DigestUtils.useFileDigest(artifact, isFile, isFile ? data.getSize() : 0);
+ if (useDigest && data.getDigest() != null) {
+ // We do not need to store the FileArtifactValue separately -- the digest is in the file value
+ // and that is all that is needed for this file's metadata.
+ return new Metadata(data.getDigest());
+ }
+ // Unfortunately, the FileValue does not contain enough information for us to calculate the
+ // corresponding FileArtifactValue -- either the metadata must use the modified time, which we
+ // do not expose in the FileValue, or the FileValue didn't store the digest So we store the
+ // metadata separately.
+ // Use the FileValue's digest if no digest was injected, or if the file can't be digested.
+ injectedDigest = injectedDigest != null || !isFile ? injectedDigest : data.getDigest();
+ FileArtifactValue value =
+ FileArtifactValue.create(artifact, isFile, isFile ? data.getSize() : 0, injectedDigest);
+ FileArtifactValue oldValue = additionalOutputData.putIfAbsent(artifact, value);
+ checkInconsistentData(artifact, oldValue, value);
+ return metadataFromValue(value);
+ }
+
+ @Override
+ public void setDigestForVirtualArtifact(Artifact artifact, Digest digest) {
+ Preconditions.checkState(artifact.isMiddlemanArtifact(), artifact);
+ Preconditions.checkNotNull(digest, artifact);
+ additionalOutputData.put(artifact,
+ FileArtifactValue.createMiddleman(digest.asMetadata().digest));
+ }
+
+ @Override
+ public void injectDigest(ActionInput output, FileStatus statNoFollow, byte[] digest) {
+ if (output instanceof Artifact) {
+ Artifact artifact = (Artifact) output;
+ Preconditions.checkState(injectedArtifacts.add(artifact), artifact);
+ FileValue fileValue;
+ try {
+ // This call may do an unnecessary call to Path#getFastDigest to see if the digest is
+ // readily available. We cannot pass the digest in, though, because if it is not available
+ // from the filesystem, this FileValue will not compare equal to another one created for the
+ // same file, because the other one will be missing its digest.
+ fileValue = fileValueFromArtifact(artifact, FileStatusWithDigestAdapter.adapt(statNoFollow),
+ tsgm);
+ byte[] fileDigest = fileValue.getDigest();
+ Preconditions.checkState(fileDigest == null || Arrays.equals(digest, fileDigest),
+ "%s %s %s", artifact, digest, fileDigest);
+ outputArtifactData.put(artifact, fileValue);
+ } catch (IOException e) {
+ // Do nothing - we just failed to inject metadata. Real error handling will be done later,
+ // when somebody will try to access that file.
+ return;
+ }
+ // If needed, insert additional data. Note that this can only be true if the file is empty or
+ // the filesystem does not support fast digests. Since we usually only inject digests when
+ // running with a filesystem that supports fast digests, this is fairly unlikely.
+ try {
+ maybeStoreAdditionalData(artifact, fileValue, digest);
+ } catch (IOException e) {
+ if (fileValue.getSize() != 0) {
+ // Empty files currently have their mtimes examined, and so could throw. No other files
+ // should throw, since all filesystem access has already been done.
+ throw new IllegalStateException(
+ "Filesystem should not have been accessed while injecting data for "
+ + artifact.prettyPrint(), e);
+ }
+ // Ignore exceptions for empty files, as above.
+ }
+ }
+ }
+
+ @Override
+ public void discardMetadata(Collection<Artifact> artifactList) {
+ Preconditions.checkState(injectedArtifacts.isEmpty(),
+ "Artifacts cannot be injected before action execution: %s", injectedArtifacts);
+ outputArtifactData.keySet().removeAll(artifactList);
+ additionalOutputData.keySet().removeAll(artifactList);
+ }
+
+ @Override
+ public boolean artifactExists(Artifact artifact) {
+ return getMetadataMaybe(artifact) != null;
+ }
+
+ @Override
+ public boolean isRegularFile(Artifact artifact) {
+ // Currently this method is used only for genrule input directory checks. If we need to call
+ // this on output artifacts too, this could be more efficient.
+ FileArtifactValue value = getInputFileArtifactValue(artifact);
+ if (value != null && value.getDigest() != null) {
+ return true;
+ }
+ return artifact.getPath().isFile();
+ }
+
+ @Override
+ public boolean isInjected(Artifact artifact) {
+ return injectedArtifacts.contains(artifact);
+ }
+
+ /**
+ * @return data for output files that was computed during execution. Should include data for all
+ * non-middleman artifacts.
+ */
+ Map<Artifact, FileValue> getOutputData() {
+ return outputArtifactData;
+ }
+
+ /**
+ * Returns data for any output files whose metadata was not computable from the corresponding
+ * entry in {@link #getOutputData}.
+ *
+ * <p>There are three reasons why we might not be able to compute metadata for an artifact from
+ * the FileValue. First, middleman artifacts have no corresponding FileValues. Second, if
+ * computing a file's digest is not fast, the FileValue does not do so, so a file on a filesystem
+ * without fast digests has to have its metadata stored separately. Third, some files' metadata
+ * (directories, empty files) contain their mtimes, which the FileValue does not expose, so that
+ * has to be stored separately.
+ *
+ * <p>Note that for files that need digests, we can't easily inject the digest in the FileValue
+ * because it would complicate equality-checking on subsequent builds -- if our filesystem doesn't
+ * do fast digests, the comparison value would not have a digest.
+ */
+ Map<Artifact, FileArtifactValue> getAdditionalOutputData() {
+ return additionalOutputData;
+ }
+
+ @Override
+ public long getSizeInBytes(ActionInput input) throws IOException {
+ FileArtifactValue metadata = getInputFileArtifactValue(input);
+ if (metadata != null) {
+ return metadata.getSize();
+ }
+ return -1;
+ }
+
+ @Nullable
+ @Override
+ public File getFileFromDigest(ByteString digest) throws IOException {
+ Artifact artifact = reverseMap.get(digest);
+ if (artifact != null) {
+ String relPath = artifact.getExecPathString();
+ return relPath.startsWith("/") ? new File(relPath) : new File(execRoot, relPath);
+ }
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public ByteString getDigest(ActionInput input) throws IOException {
+ FileArtifactValue value = getInputFileArtifactValue(input);
+ if (value != null) {
+ byte[] bytes = value.getDigest();
+ if (bytes != null) {
+ ByteString digest = ByteString.copyFrom(BaseEncoding.base16().lowerCase().encode(bytes)
+ .getBytes(StandardCharsets.US_ASCII));
+ reverseMap.put(BYTE_INTERNER.intern(digest), (Artifact) input);
+ return digest;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public boolean contentsAvailableLocally(ByteString digest) {
+ return reverseMap.containsKey(digest);
+ }
+
+ static FileValue fileValueFromArtifact(Artifact artifact,
+ @Nullable FileStatusWithDigest statNoFollow, TimestampGranularityMonitor tsgm)
+ throws IOException {
+ Path path = artifact.getPath();
+ RootedPath rootedPath =
+ RootedPath.toRootedPath(artifact.getRoot().getPath(), artifact.getRootRelativePath());
+ if (statNoFollow == null) {
+ statNoFollow = FileStatusWithDigestAdapter.adapt(path.statIfFound(Symlinks.NOFOLLOW));
+ if (statNoFollow == null) {
+ return FileValue.value(rootedPath, FileStateValue.NONEXISTENT_FILE_STATE_NODE,
+ rootedPath, FileStateValue.NONEXISTENT_FILE_STATE_NODE);
+ }
+ }
+ Path realPath = path;
+ // We use FileStatus#isSymbolicLink over Path#isSymbolicLink to avoid the unnecessary stat
+ // done by the latter.
+ if (statNoFollow.isSymbolicLink()) {
+ realPath = path.resolveSymbolicLinks();
+ // We need to protect against symlink cycles since FileValue#value assumes it's dealing with a
+ // file that's not in a symlink cycle.
+ if (realPath.equals(path)) {
+ throw new IOException("symlink cycle");
+ }
+ }
+ RootedPath realRootedPath = RootedPath.toRootedPathMaybeUnderRoot(realPath,
+ ImmutableList.of(artifact.getRoot().getPath()));
+ FileStateValue fileStateValue;
+ FileStateValue realFileStateValue;
+ try {
+ fileStateValue = FileStateValue.createWithStatNoFollow(rootedPath, statNoFollow, tsgm);
+ // TODO(bazel-team): consider avoiding a 'stat' here when the symlink target hasn't changed
+ // and is a source file (since changes to those are checked separately).
+ realFileStateValue = realPath.equals(path) ? fileStateValue
+ : FileStateValue.create(realRootedPath, tsgm);
+ } catch (InconsistentFilesystemException e) {
+ throw new IOException(e);
+ }
+ return FileValue.value(rootedPath, fileStateValue, realRootedPath, realFileStateValue);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/FileArtifactValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/FileArtifactValue.java
new file mode 100644
index 0000000..7acc38c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/FileArtifactValue.java
@@ -0,0 +1,148 @@
+// 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.Objects;
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.cache.DigestUtils;
+import com.google.devtools.build.lib.vfs.FileStatus;
+import com.google.devtools.build.lib.vfs.Path;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+import javax.annotation.Nullable;
+
+/**
+ * Stores the data of an artifact corresponding to a file. This file may be an ordinary file, in
+ * which case we would expect to see a digest and size; a directory, in which case we would expect
+ * to see an mtime; or an empty file, where we would expect to see a size (=0), mtime, and digest
+ */
+public class FileArtifactValue extends ArtifactValue {
+ /** Data for Middleman artifacts that did not have data specified. */
+ static final FileArtifactValue DEFAULT_MIDDLEMAN = new FileArtifactValue(null, 0, 0);
+ /** Data that marks that a file is not present on the filesystem. */
+ static final FileArtifactValue MISSING_FILE_MARKER = new FileArtifactValue(null, 1, 0);
+
+ @Nullable private final byte[] digest;
+ private final long mtime;
+ private final long size;
+
+ private FileArtifactValue(byte[] digest, long size) {
+ Preconditions.checkState(size >= 0, "size must be non-negative: %s %s", digest, size);
+ this.digest = Preconditions.checkNotNull(digest, size);
+ this.size = size;
+ this.mtime = -1;
+ }
+
+ // Only used by empty files (non-null digest) and directories (null digest).
+ private FileArtifactValue(byte[] digest, long mtime, long size) {
+ Preconditions.checkState(mtime >= 0, "mtime must be non-negative: %s %s", mtime, size);
+ Preconditions.checkState(size == 0, "size must be zero: %s %s", mtime, size);
+ this.digest = digest;
+ this.size = size;
+ this.mtime = mtime;
+ }
+
+ static FileArtifactValue create(Artifact artifact) throws IOException {
+ Path path = artifact.getPath();
+ FileStatus stat = path.stat();
+ boolean isFile = stat.isFile();
+ return create(artifact, isFile, isFile ? stat.getSize() : 0, null);
+ }
+
+ static FileArtifactValue create(Artifact artifact, FileValue fileValue) throws IOException {
+ boolean isFile = fileValue.isFile();
+ return create(artifact, isFile, isFile ? fileValue.getSize() : 0,
+ isFile ? fileValue.getDigest() : null);
+ }
+
+ static FileArtifactValue create(Artifact artifact, boolean isFile, long size,
+ @Nullable byte[] digest) throws IOException {
+ if (isFile && digest == null) {
+ digest = DigestUtils.getDigestOrFail(artifact.getPath(), size);
+ }
+ if (!DigestUtils.useFileDigest(artifact, isFile, size)) {
+ // In this case, we need to store the mtime because the action cache uses mtime to determine
+ // if this artifact has changed. This is currently true for empty files and directories. We
+ // do not optimize for this code path (by storing the mtime in a FileValue) because we do not
+ // like it and may remove this special-casing for empty files in the future. We want this code
+ // path to go away somehow too for directories (maybe by implementing FileSet
+ // in Skyframe)
+ return new FileArtifactValue(digest, artifact.getPath().getLastModifiedTime(), size);
+ }
+ Preconditions.checkState(digest != null, artifact);
+ return new FileArtifactValue(digest, size);
+ }
+
+ static FileArtifactValue createMiddleman(byte[] digest) {
+ Preconditions.checkNotNull(digest);
+ // The Middleman artifact values have size 1 because we want their digests to be used. This hack
+ // can be removed once empty files are digested.
+ return new FileArtifactValue(digest, /*size=*/1);
+ }
+
+ @Nullable
+ byte[] getDigest() {
+ return digest;
+ }
+
+ /** Gets the size of the file. Directories have size 0. */
+ long getSize() {
+ return size;
+ }
+
+ /**
+ * Gets last modified time of file. Should only be called if {@link DigestUtils#useFileDigest} was
+ * false for this artifact -- namely, either it is a directory or an empty file. Note that since
+ * we store directory sizes as 0, all files for which this method can be called have size 0.
+ */
+ long getModifiedTime() {
+ Preconditions.checkState(size == 0, "%s %s %s", digest, mtime, size);
+ return mtime;
+ }
+
+ @Override
+ public int hashCode() {
+ // Hash digest by content, not reference. Note that digest is the only array in this array.
+ return Arrays.deepHashCode(new Object[] {size, mtime, digest});
+ }
+
+ /**
+ * Two FileArtifactValues will only compare equal if they have the same content. This differs
+ * from the {@code Metadata#equivalence} method, which allows for comparison using mtime if
+ * one object does not have a digest available.
+ */
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof FileArtifactValue)) {
+ return false;
+ }
+ FileArtifactValue that = (FileArtifactValue) other;
+ return this.mtime == that.mtime && this.size == that.size
+ && Arrays.equals(this.digest, that.digest);
+ }
+
+ @Override
+ public String toString() {
+ return Objects.toStringHelper(FileArtifactValue.class)
+ .add("digest", digest)
+ .add("mtime", mtime)
+ .add("size", size).toString();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/FileContentsProxy.java b/src/main/java/com/google/devtools/build/lib/skyframe/FileContentsProxy.java
new file mode 100644
index 0000000..344c364
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/FileContentsProxy.java
@@ -0,0 +1,66 @@
+// 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 java.io.Serializable;
+import java.util.Objects;
+
+/**
+ * In case we can't get a fast digest from the filesystem, we store this metadata as a proxy to
+ * the file contents. Currently it is a pair of the mtime and "value id" (which is right now just
+ * the ivalue number). We may wish to augment this object with the following data:
+ * a. the device number
+ * b. the ctime, which cannot be tampered with in userspace
+ *
+ * <p>For an example of why mtime alone is insufficient, note that 'mv' preserves timestamps. So if
+ * files 'a' and 'b' initially have the same timestamp, then we would think 'b' is unchanged after
+ * the user executes `mv a b` between two builds.
+ */
+public final class FileContentsProxy implements Serializable {
+ private final long mtime;
+ private final long valueId;
+
+ private FileContentsProxy(long mtime, long valueId) {
+ this.mtime = mtime;
+ this.valueId = valueId;
+ }
+
+ public static FileContentsProxy create(long mtime, long valueId) {
+ return new FileContentsProxy(mtime, valueId);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+
+ if (!(other instanceof FileContentsProxy)) {
+ return false;
+ }
+
+ FileContentsProxy that = (FileContentsProxy) other;
+ return mtime == that.mtime && valueId == that.valueId;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mtime, valueId);
+ }
+
+ @Override
+ public String toString() {
+ return "mtime: " + mtime + " valueId: " + valueId;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/FileFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/FileFunction.java
new file mode 100644
index 0000000..ad3cb74
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/FileFunction.java
@@ -0,0 +1,217 @@
+// 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.collect.ImmutableList;
+import com.google.common.collect.Sets;
+import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
+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 java.util.LinkedHashSet;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.annotation.Nullable;
+
+/**
+ * A {@link SkyFunction} for {@link FileValue}s.
+ *
+ * <p>Most of the complexity in the implementation is associated to handling symlinks. Namely,
+ * this class makes sure that {@code FileValue}s corresponding to symlinks are correctly invalidated
+ * if the destination of the symlink is invalidated. Directory symlinks are also covered.
+ */
+public class FileFunction implements SkyFunction {
+
+ private final AtomicReference<PathPackageLocator> pkgLocator;
+ private final ExternalFilesHelper externalFilesHelper;
+
+ public FileFunction(AtomicReference<PathPackageLocator> pkgLocator,
+ ExternalFilesHelper externalFilesHelper) {
+ this.pkgLocator = pkgLocator;
+ this.externalFilesHelper = externalFilesHelper;
+ }
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) throws FileFunctionException {
+ RootedPath rootedPath = (RootedPath) skyKey.argument();
+ RootedPath realRootedPath = rootedPath;
+ FileStateValue realFileStateValue = null;
+ PathFragment relativePath = rootedPath.getRelativePath();
+
+ // Resolve ancestor symlinks, but only if the current file is not the filesystem root (has no
+ // parent) or a package path root (treated opaquely and handled by skyframe's DiffAwareness
+ // interface) or otherwise assumed to be immutable (handling ancestors would add dependencies
+ // too aggressively). Note that this is the first thing we do - if an ancestor is part of a
+ // symlink cycle, we want to detect that quickly as it gives a more informative error message
+ // than we'd get doing bogus filesystem operations.
+ if (!relativePath.equals(PathFragment.EMPTY_FRAGMENT)
+ && !externalFilesHelper.shouldAssumeImmutable(rootedPath)) {
+ Pair<RootedPath, FileStateValue> resolvedState =
+ resolveFromAncestors(rootedPath, env);
+ if (resolvedState == null) {
+ return null;
+ }
+ realRootedPath = resolvedState.getFirst();
+ realFileStateValue = resolvedState.getSecond();
+ }
+
+ FileStateValue fileStateValue = (FileStateValue) env.getValue(FileStateValue.key(rootedPath));
+ if (fileStateValue == null) {
+ return null;
+ }
+ if (realFileStateValue == null) {
+ realFileStateValue = fileStateValue;
+ }
+
+ LinkedHashSet<RootedPath> seenPaths = Sets.newLinkedHashSet();
+ while (realFileStateValue.getType().equals(FileStateValue.Type.SYMLINK)) {
+ if (!seenPaths.add(realRootedPath)) {
+ FileSymlinkCycleException fileSymlinkCycleException =
+ makeFileSymlinkCycleException(realRootedPath, seenPaths);
+ if (env.getValue(FileSymlinkCycleUniquenessValue.key(fileSymlinkCycleException.getCycle()))
+ == null) {
+ // Note that this dependency is merely to ensure that each unique cycle gets reported
+ // exactly once.
+ return null;
+ }
+ throw new FileFunctionException(fileSymlinkCycleException);
+ }
+ Pair<RootedPath, FileStateValue> resolvedState = getSymlinkTargetRootedPath(realRootedPath,
+ realFileStateValue.getSymlinkTarget(), env);
+ if (resolvedState == null) {
+ return null;
+ }
+ realRootedPath = resolvedState.getFirst();
+ realFileStateValue = resolvedState.getSecond();
+ }
+ return FileValue.value(rootedPath, fileStateValue, realRootedPath, realFileStateValue);
+ }
+
+ /**
+ * Returns the path and file state of {@code rootedPath}, accounting for ancestor symlinks, or
+ * {@code null} if there was a missing dep.
+ */
+ @Nullable
+ private Pair<RootedPath, FileStateValue> resolveFromAncestors(RootedPath rootedPath,
+ Environment env) throws FileFunctionException {
+ PathFragment relativePath = rootedPath.getRelativePath();
+ RootedPath realRootedPath = rootedPath;
+ FileValue parentFileValue = null;
+ if (!relativePath.equals(PathFragment.EMPTY_FRAGMENT)) {
+ RootedPath parentRootedPath = RootedPath.toRootedPath(rootedPath.getRoot(),
+ relativePath.getParentDirectory());
+ parentFileValue = (FileValue) env.getValue(FileValue.key(parentRootedPath));
+ if (parentFileValue == null) {
+ return null;
+ }
+ PathFragment baseName = new PathFragment(relativePath.getBaseName());
+ RootedPath parentRealRootedPath = parentFileValue.realRootedPath();
+ realRootedPath = RootedPath.toRootedPath(parentRealRootedPath.getRoot(),
+ parentRealRootedPath.getRelativePath().getRelative(baseName));
+ }
+ FileStateValue realFileStateValue =
+ (FileStateValue) env.getValue(FileStateValue.key(realRootedPath));
+ if (realFileStateValue == null) {
+ return null;
+ }
+ if (realFileStateValue.getType() != FileStateValue.Type.NONEXISTENT
+ && parentFileValue != null && !parentFileValue.isDirectory()) {
+ String type = realFileStateValue.getType().toString().toLowerCase();
+ String message = type + " " + rootedPath.asPath() + " exists but its parent "
+ + "directory " + parentFileValue.realRootedPath().asPath() + " doesn't exist.";
+ throw new FileFunctionException(new InconsistentFilesystemException(message),
+ Transience.TRANSIENT);
+ }
+ return Pair.of(realRootedPath, realFileStateValue);
+ }
+
+ /**
+ * Returns the symlink target and file state of {@code rootedPath}'s symlink to
+ * {@code symlinkTarget}, accounting for ancestor symlinks, or {@code null} if there was a
+ * missing dep.
+ */
+ @Nullable
+ private Pair<RootedPath, FileStateValue> getSymlinkTargetRootedPath(RootedPath rootedPath,
+ PathFragment symlinkTarget, Environment env) throws FileFunctionException {
+ if (symlinkTarget.isAbsolute()) {
+ Path path = rootedPath.asPath().getFileSystem().getRootDirectory().getRelative(
+ symlinkTarget);
+ return resolveFromAncestors(
+ RootedPath.toRootedPathMaybeUnderRoot(path, pkgLocator.get().getPathEntries()), env);
+ }
+ Path path = rootedPath.asPath();
+ Path symlinkTargetPath;
+ if (path.getParentDirectory() != null) {
+ RootedPath parentRootedPath = RootedPath.toRootedPathMaybeUnderRoot(
+ path.getParentDirectory(), pkgLocator.get().getPathEntries());
+ FileValue parentFileValue = (FileValue) env.getValue(FileValue.key(parentRootedPath));
+ if (parentFileValue == null) {
+ return null;
+ }
+ symlinkTargetPath = parentFileValue.realRootedPath().asPath().getRelative(symlinkTarget);
+ } else {
+ // This means '/' is a symlink to 'symlinkTarget'.
+ symlinkTargetPath = path.getRelative(symlinkTarget);
+ }
+ RootedPath symlinkTargetRootedPath = RootedPath.toRootedPathMaybeUnderRoot(symlinkTargetPath,
+ pkgLocator.get().getPathEntries());
+ return resolveFromAncestors(symlinkTargetRootedPath, env);
+ }
+
+ private FileSymlinkCycleException makeFileSymlinkCycleException(RootedPath startOfCycle,
+ Iterable<RootedPath> symlinkPaths) {
+ boolean inPathToCycle = true;
+ ImmutableList.Builder<RootedPath> pathToCycleBuilder = ImmutableList.builder();
+ ImmutableList.Builder<RootedPath> cycleBuilder = ImmutableList.builder();
+ for (RootedPath path : symlinkPaths) {
+ if (path.equals(startOfCycle)) {
+ inPathToCycle = false;
+ }
+ if (inPathToCycle) {
+ pathToCycleBuilder.add(path);
+ } else {
+ cycleBuilder.add(path);
+ }
+ }
+ return new FileSymlinkCycleException(pathToCycleBuilder.build(), cycleBuilder.build());
+ }
+
+ @Nullable
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+
+ /**
+ * Used to declare all the exception types that can be wrapped in the exception thrown by
+ * {@link FileFunction#compute}.
+ */
+ private static final class FileFunctionException extends SkyFunctionException {
+
+ public FileFunctionException(InconsistentFilesystemException e, Transience transience) {
+ super(e, transience);
+ }
+
+ public FileFunctionException(FileSymlinkCycleException e) {
+ super(e, Transience.PERSISTENT);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/FileStateFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/FileStateFunction.java
new file mode 100644
index 0000000..ec2e871
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/FileStateFunction.java
@@ -0,0 +1,76 @@
+// 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.devtools.build.lib.util.io.TimestampGranularityMonitor;
+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.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.io.IOException;
+
+/**
+ * A {@link SkyFunction} for {@link FileStateValue}s.
+ *
+ * <p>Merely calls FileStateValue#create, but also has special handling for files outside the
+ * package roots (see {@link ExternalFilesHelper}).
+ */
+public class FileStateFunction implements SkyFunction {
+
+ private final TimestampGranularityMonitor tsgm;
+ private final ExternalFilesHelper externalFilesHelper;
+
+ public FileStateFunction(TimestampGranularityMonitor tsgm,
+ ExternalFilesHelper externalFilesHelper) {
+ this.tsgm = tsgm;
+ this.externalFilesHelper = externalFilesHelper;
+ }
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) throws FileStateFunctionException {
+ RootedPath rootedPath = (RootedPath) skyKey.argument();
+ externalFilesHelper.maybeAddDepOnBuildId(rootedPath, env);
+ if (env.valuesMissing()) {
+ return null;
+ }
+ try {
+ return FileStateValue.create(rootedPath, tsgm);
+ } catch (IOException e) {
+ throw new FileStateFunctionException(e);
+ } catch (InconsistentFilesystemException e) {
+ throw new FileStateFunctionException(e);
+ }
+ }
+
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+
+ /**
+ * Used to declare all the exception types that can be wrapped in the exception thrown by
+ * {@link FileStateFunction#compute}.
+ */
+ private static final class FileStateFunctionException extends SkyFunctionException {
+ public FileStateFunctionException(IOException e) {
+ super(e, Transience.TRANSIENT);
+ }
+
+ public FileStateFunctionException(InconsistentFilesystemException e) {
+ super(e, Transience.TRANSIENT);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/FileStateValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/FileStateValue.java
new file mode 100644
index 0000000..8631ff3
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/FileStateValue.java
@@ -0,0 +1,317 @@
+// 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.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor;
+import com.google.devtools.build.lib.vfs.FileStatus;
+import com.google.devtools.build.lib.vfs.FileStatusWithDigest;
+import com.google.devtools.build.lib.vfs.FileStatusWithDigestAdapter;
+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.lib.vfs.Symlinks;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Objects;
+
+import javax.annotation.Nullable;
+
+/**
+ * Encapsulates the filesystem operations needed to get state for a path. This is at least a
+ * 'lstat' to determine what type of file the path is.
+ * <ul>
+ * <li> For a non-existent file, the non existence is noted.
+ * <li> For a symlink, the symlink target is noted.
+ * <li> For a directory, the existence is noted.
+ * <li> For a file, the existence is noted, along with metadata about the file (e.g.
+ * file digest). See {@link FileFileStateValue}.
+ * <ul>
+ *
+ * <p>This class is an implementation detail of {@link FileValue} and should not be used outside of
+ * {@link FileFunction}. Instead, {@link FileValue} should be used by consumers that care about
+ * files.
+ *
+ * <p>All subclasses must implement {@link #equals} and {@link #hashCode} properly.
+ */
+abstract class FileStateValue implements SkyValue {
+
+ public static final FileStateValue DIRECTORY_FILE_STATE_NODE = DirectoryFileStateValue.INSTANCE;
+ public static final FileStateValue NONEXISTENT_FILE_STATE_NODE =
+ NonexistentFileStateValue.INSTANCE;
+
+ public enum Type {
+ FILE,
+ DIRECTORY,
+ SYMLINK,
+ NONEXISTENT,
+ }
+
+ protected FileStateValue() {
+ }
+
+ public static FileStateValue create(RootedPath rootedPath,
+ @Nullable TimestampGranularityMonitor tsgm) throws InconsistentFilesystemException,
+ IOException {
+ Path path = rootedPath.asPath();
+ // Stat, but don't throw an exception for the common case of a nonexistent file. This still
+ // throws an IOException in case any other IO error is encountered.
+ FileStatus stat = path.statIfFound(Symlinks.NOFOLLOW);
+ if (stat == null) {
+ return NONEXISTENT_FILE_STATE_NODE;
+ }
+ return createWithStatNoFollow(rootedPath, FileStatusWithDigestAdapter.adapt(stat), tsgm);
+ }
+
+ public static FileStateValue createWithStatNoFollow(RootedPath rootedPath,
+ FileStatusWithDigest statNoFollow, @Nullable TimestampGranularityMonitor tsgm)
+ throws InconsistentFilesystemException, IOException {
+ Path path = rootedPath.asPath();
+ if (statNoFollow.isFile()) {
+ return FileFileStateValue.fromPath(path, statNoFollow, tsgm);
+ } else if (statNoFollow.isDirectory()) {
+ return DIRECTORY_FILE_STATE_NODE;
+ } else if (statNoFollow.isSymbolicLink()) {
+ return new SymlinkFileStateValue(path.readSymbolicLink());
+ }
+ throw new InconsistentFilesystemException("according to stat, existing path " + path + " is "
+ + "neither a file nor directory nor symlink.");
+ }
+
+ @ThreadSafe
+ static SkyKey key(RootedPath rootedPath) {
+ return new SkyKey(SkyFunctions.FILE_STATE, rootedPath);
+ }
+
+ abstract Type getType();
+
+ PathFragment getSymlinkTarget() {
+ throw new IllegalStateException();
+ }
+
+ long getSize() {
+ throw new IllegalStateException();
+ }
+
+ @Nullable
+ byte[] getDigest() {
+ throw new IllegalStateException();
+ }
+
+ /**
+ * Implementation of {@link FileStateValue} for files that exist.
+ *
+ * <p>A union of (digest, mtime). We use digests only if a fast digest lookup is available from
+ * the filesystem. If not, we fall back to mtime-based digests. This avoids the case where Blaze
+ * must read all files involved in the build in order to check for modifications in the case
+ * where fast digest lookups are not available.
+ */
+ @ThreadSafe
+ private static final class FileFileStateValue extends FileStateValue {
+ private final long size;
+ // Only needed for empty-file equality-checking. Otherwise is always -1.
+ // TODO(bazel-team): Consider getting rid of this special case for empty files.
+ private final long mtime;
+ @Nullable private final byte[] digest;
+ @Nullable private final FileContentsProxy contentsProxy;
+
+ private FileFileStateValue(long size, long mtime, byte[] digest,
+ FileContentsProxy contentsProxy) {
+ Preconditions.checkState((digest == null) != (contentsProxy == null));
+ this.size = size;
+ // mtime is forced to be -1 so that we do not accidentally depend on it for non-empty files,
+ // which should only be compared using digests.
+ this.mtime = size == 0 ? mtime : -1;
+ this.digest = digest;
+ this.contentsProxy = contentsProxy;
+ }
+
+ /**
+ * Create a FileFileStateValue instance corresponding to the given existing file.
+ * @param stat must be of type "File". (Not a symlink).
+ */
+ public static FileFileStateValue fromPath(Path path, FileStatusWithDigest stat,
+ @Nullable TimestampGranularityMonitor tsgm)
+ throws InconsistentFilesystemException {
+ Preconditions.checkState(stat.isFile(), path);
+ try {
+ byte[] digest = stat.getDigest();
+ if (digest == null) {
+ digest = path.getFastDigest();
+ }
+ if (digest == null) {
+ long mtime = stat.getLastModifiedTime();
+ // Note that TimestampGranularityMonitor#notifyDependenceOnFileTime is a thread-safe
+ // method.
+ if (tsgm != null) {
+ tsgm.notifyDependenceOnFileTime(mtime);
+ }
+ return new FileFileStateValue(stat.getSize(), stat.getLastModifiedTime(), null,
+ FileContentsProxy.create(mtime, stat.getNodeId()));
+ } else {
+ // We are careful here to avoid putting the value ID into FileMetadata if we already have
+ // a digest. Arbitrary filesystems may do weird things with the value ID; a digest is more
+ // robust.
+ return new FileFileStateValue(stat.getSize(), stat.getLastModifiedTime(), digest, null);
+ }
+ } catch (IOException e) {
+ String errorMessage = e.getMessage() != null
+ ? "error '" + e.getMessage() + "'" : "an error";
+ throw new InconsistentFilesystemException("'stat' said " + path + " is a file but then we "
+ + "later encountered " + errorMessage + " which indicates that " + path + " no longer "
+ + "exists. Did you delete it during the build?");
+ }
+ }
+
+ @Override
+ public Type getType() {
+ return Type.FILE;
+ }
+
+ @Override
+ public long getSize() {
+ return size;
+ }
+
+ @Override
+ @Nullable
+ public byte[] getDigest() {
+ return digest;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof FileFileStateValue) {
+ FileFileStateValue other = (FileFileStateValue) obj;
+ return size == other.size && mtime == other.mtime && Arrays.equals(digest, other.digest)
+ && Objects.equals(contentsProxy, other.contentsProxy);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(size, mtime, Arrays.hashCode(digest), contentsProxy);
+ }
+
+ @Override
+ public String toString() {
+ return "[size: " + size + " " + (mtime != -1 ? "mtime: " + mtime : "")
+ + (digest != null ? "digest: " + Arrays.toString(digest) : contentsProxy) + "]";
+ }
+ }
+
+ /** Implementation of {@link FileStateValue} for directories that exist. */
+ private static final class DirectoryFileStateValue extends FileStateValue {
+
+ public static final DirectoryFileStateValue INSTANCE = new DirectoryFileStateValue();
+
+ private DirectoryFileStateValue() {
+ }
+
+ @Override
+ public Type getType() {
+ return Type.DIRECTORY;
+ }
+
+ @Override
+ public String toString() {
+ return "directory";
+ }
+
+ // This object is normally a singleton, but deserialization produces copies.
+ @Override
+ public boolean equals(Object obj) {
+ return obj instanceof DirectoryFileStateValue;
+ }
+
+ @Override
+ public int hashCode() {
+ return 7654321;
+ }
+ }
+
+ /** Implementation of {@link FileStateValue} for symlinks. */
+ private static final class SymlinkFileStateValue extends FileStateValue {
+
+ private final PathFragment symlinkTarget;
+
+ private SymlinkFileStateValue(PathFragment symlinkTarget) {
+ this.symlinkTarget = symlinkTarget;
+ }
+
+ @Override
+ public Type getType() {
+ return Type.SYMLINK;
+ }
+
+ @Override
+ public PathFragment getSymlinkTarget() {
+ return symlinkTarget;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof SymlinkFileStateValue)) {
+ return false;
+ }
+ SymlinkFileStateValue other = (SymlinkFileStateValue) obj;
+ return symlinkTarget.equals(other.symlinkTarget);
+ }
+
+ @Override
+ public int hashCode() {
+ return symlinkTarget.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return "symlink to " + symlinkTarget;
+ }
+ }
+
+ /** Implementation of {@link FileStateValue} for nonexistent files. */
+ private static final class NonexistentFileStateValue extends FileStateValue {
+
+ public static final NonexistentFileStateValue INSTANCE = new NonexistentFileStateValue();
+
+ private NonexistentFileStateValue() {
+ }
+
+ @Override
+ public Type getType() {
+ return Type.NONEXISTENT;
+ }
+
+ @Override
+ public String toString() {
+ return "nonexistent";
+ }
+
+ // This object is normally a singleton, but deserialization produces copies.
+ @Override
+ public boolean equals(Object obj) {
+ return obj instanceof NonexistentFileStateValue;
+ }
+
+ @Override
+ public int hashCode() {
+ return 8765432;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/FileSymlinkCycleException.java b/src/main/java/com/google/devtools/build/lib/skyframe/FileSymlinkCycleException.java
new file mode 100644
index 0000000..d57fc42
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/FileSymlinkCycleException.java
@@ -0,0 +1,49 @@
+// 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.collect.ImmutableList;
+import com.google.devtools.build.lib.vfs.RootedPath;
+
+/** Exception indicating that a cycle was found in the filesystem. */
+public class FileSymlinkCycleException extends Exception {
+
+ private final ImmutableList<RootedPath> pathToCycle;
+ private final ImmutableList<RootedPath> cycle;
+
+ FileSymlinkCycleException(ImmutableList<RootedPath> pathToCycle,
+ ImmutableList<RootedPath> cycle) {
+ // The cycle itself has already been reported by FileSymlinkCycleUniquenessValue, but we still
+ // want to have a readable #getMessage.
+ super("Symlink cycle");
+ this.pathToCycle = pathToCycle;
+ this.cycle = cycle;
+ }
+
+ /**
+ * The symlink path to the symlink cycle. For example, suppose 'a' -> 'b' -> 'c' -> 'd' -> 'c'.
+ * The path to the cycle is 'a', 'b'.
+ */
+ ImmutableList<RootedPath> getPathToCycle() {
+ return pathToCycle;
+ }
+
+ /**
+ * The symlink cycle. For example, suppose 'a' -> 'b' -> 'c' -> 'd' -> 'c'.
+ * The cycle is 'c', 'd'.
+ */
+ ImmutableList<RootedPath> getCycle() {
+ return cycle;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/FileSymlinkCycleUniquenessFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/FileSymlinkCycleUniquenessFunction.java
new file mode 100644
index 0000000..a0604b5
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/FileSymlinkCycleUniquenessFunction.java
@@ -0,0 +1,45 @@
+// 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.collect.ImmutableList;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+/** A value builder that has the side effect of reporting a file symlink cycle. */
+public class FileSymlinkCycleUniquenessFunction implements SkyFunction {
+
+ @SuppressWarnings("unchecked") // Cast from Object to ImmutableList<RootedPath>.
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) {
+ StringBuilder cycleMessage = new StringBuilder("circular symlinks detected\n");
+ cycleMessage.append("[start of symlink cycle]\n");
+ for (RootedPath rootedPath : (ImmutableList<RootedPath>) skyKey.argument()) {
+ cycleMessage.append(rootedPath.asPath() + "\n");
+ }
+ cycleMessage.append("[end of symlink cycle]");
+ // The purpose of this value builder is the side effect of emitting an error message exactly
+ // once per build per unique cycle.
+ env.getListener().handle(Event.error(cycleMessage.toString()));
+ return FileSymlinkCycleUniquenessValue.INSTANCE;
+ }
+
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/FileSymlinkCycleUniquenessValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/FileSymlinkCycleUniquenessValue.java
new file mode 100644
index 0000000..627276d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/FileSymlinkCycleUniquenessValue.java
@@ -0,0 +1,57 @@
+// 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.collect.ImmutableList;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+/**
+ * A value for ensuring that a file symlink cycle is reported exactly once. This is achieved by
+ * forcing the same value key for two logically equivalent cycles (e.g. ['a' -> 'b' -> 'c' -> 'a']
+ * and ['b' -> 'c' -> 'a' -> 'b'], and letting Skyframe do its magic.
+ */
+class FileSymlinkCycleUniquenessValue implements SkyValue {
+
+ public static final FileSymlinkCycleUniquenessValue INSTANCE =
+ new FileSymlinkCycleUniquenessValue();
+
+ private FileSymlinkCycleUniquenessValue() {
+ }
+
+ static SkyKey key(ImmutableList<RootedPath> cycle) {
+ Preconditions.checkState(!cycle.isEmpty());
+ return new SkyKey(SkyFunctions.FILE_SYMLINK_CYCLE_UNIQUENESS, canonicalize(cycle));
+ }
+
+ private static ImmutableList<RootedPath> canonicalize(ImmutableList<RootedPath> cycle) {
+ int minPos = 0;
+ String minString = cycle.get(0).toString();
+ for (int i = 1; i < cycle.size(); i++) {
+ String candidateString = cycle.get(i).toString();
+ if (candidateString.compareTo(minString) < 0) {
+ minPos = i;
+ minString = candidateString;
+ }
+ }
+ ImmutableList.Builder<RootedPath> builder = ImmutableList.builder();
+ for (int i = 0; i < cycle.size(); i++) {
+ int pos = (minPos + i) % cycle.size();
+ builder.add(cycle.get(pos));
+ }
+ return builder.build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/FileValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/FileValue.java
new file mode 100644
index 0000000..1850fd9
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/FileValue.java
@@ -0,0 +1,279 @@
+// 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.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.skyframe.FileStateValue.Type;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.util.Objects;
+
+import javax.annotation.Nullable;
+
+/**
+ * A value that corresponds to a file (or directory or symlink or non-existent file), fully
+ * accounting for symlinks (e.g. proper dependencies on ancestor symlinks so as to be incrementally
+ * correct). Anything in Skyframe that cares about the fully resolved path of a file (e.g. anything
+ * that cares about the contents of a file) should have a dependency on the corresponding
+ * {@link FileValue}.
+ *
+ * <p>
+ * Note that the existence of a file value does not imply that the file exists on the filesystem.
+ * File values for missing files will be created on purpose in order to facilitate incremental
+ * builds in the case those files have reappeared.
+ *
+ * <p>
+ * This class contains the relevant metadata for a file, although not the contents. Note that
+ * since a FileValue doesn't store its corresponding SkyKey, it's possible for the FileValues for
+ * two different paths to be the same.
+ *
+ * <p>
+ * This should not be used for build outputs; use {@link ArtifactValue} for those.
+ */
+@Immutable
+@ThreadSafe
+public abstract class FileValue implements SkyValue {
+
+ boolean exists() {
+ return realFileStateValue().getType() != Type.NONEXISTENT;
+ }
+
+ public boolean isSymlink() {
+ return false;
+ }
+
+ /**
+ * Returns true if this value corresponds to a file or symlink to an existing file. If so, its
+ * parent directory is guaranteed to exist.
+ */
+ public boolean isFile() {
+ return realFileStateValue().getType() == Type.FILE;
+ }
+
+ /**
+ * Returns true if the file is a directory or a symlink to an existing directory. If so, its
+ * parent directory is guaranteed to exist.
+ */
+ public boolean isDirectory() {
+ return realFileStateValue().getType() == Type.DIRECTORY;
+ }
+
+ /**
+ * Returns the real rooted path of the file, taking ancestor symlinks into account. For example,
+ * the rooted path ['root']/['a/b'] is really ['root']/['c/b'] if 'a' is a symlink to 'b'. Note
+ * that ancestor symlinks outside the root boundary are not taken into consideration.
+ */
+ public abstract RootedPath realRootedPath();
+
+ abstract FileStateValue realFileStateValue();
+
+ /**
+ * Returns the unresolved link target if {@link #isSymlink()}.
+ *
+ * <p>This is useful if the caller wants to, for example, duplicate a relative symlink. An actual
+ * example could be a build rule that copies a set of input files to the output directory, but
+ * upon encountering symbolic links it can decide between copying or following them.
+ */
+ PathFragment getUnresolvedLinkTarget() {
+ throw new IllegalStateException(this.toString());
+ }
+
+ long getSize() {
+ Preconditions.checkState(isFile(), this);
+ return realFileStateValue().getSize();
+ }
+
+ @Nullable
+ byte[] getDigest() {
+ Preconditions.checkState(isFile(), this);
+ return realFileStateValue().getDigest();
+ }
+
+ /**
+ * Returns a key for building a file value for the given root-relative path.
+ */
+ @ThreadSafe
+ public static SkyKey key(RootedPath rootedPath) {
+ return new SkyKey(SkyFunctions.FILE, rootedPath);
+ }
+
+ /**
+ * Only intended to be used by {@link FileFunction}. Should not be used for symlink cycles.
+ */
+ static FileValue value(RootedPath rootedPath, FileStateValue fileStateValue,
+ RootedPath realRootedPath, FileStateValue realFileStateValue) {
+ if (rootedPath.equals(realRootedPath)) {
+ Preconditions.checkState(fileStateValue.getType() != FileStateValue.Type.SYMLINK,
+ "rootedPath: %s, fileStateValue: %s, realRootedPath: %s, realFileStateValue: %s",
+ rootedPath, fileStateValue, realRootedPath, realFileStateValue);
+ return new RegularFileValue(rootedPath, fileStateValue);
+ } else {
+ if (fileStateValue.getType() == FileStateValue.Type.SYMLINK) {
+ return new SymlinkFileValue(realRootedPath, realFileStateValue,
+ fileStateValue.getSymlinkTarget());
+ } else {
+ return new DifferentRealPathFileValue(realRootedPath, realFileStateValue);
+ }
+ }
+ }
+
+ /**
+ * Implementation of {@link FileValue} for files whose fully resolved path is the same as the
+ * requested path. For example, this is the case for the path "foo/bar/baz" if neither 'foo' nor
+ * 'foo/bar' nor 'foo/bar/baz' are symlinks.
+ */
+ private static final class RegularFileValue extends FileValue {
+
+ private final RootedPath rootedPath;
+ private final FileStateValue fileStateValue;
+
+ private RegularFileValue(RootedPath rootedPath, FileStateValue fileState) {
+ this.rootedPath = Preconditions.checkNotNull(rootedPath);
+ this.fileStateValue = Preconditions.checkNotNull(fileState);
+ }
+
+ @Override
+ public RootedPath realRootedPath() {
+ return rootedPath;
+ }
+
+ @Override
+ FileStateValue realFileStateValue() {
+ return fileStateValue;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (obj.getClass() != RegularFileValue.class) {
+ return false;
+ }
+ RegularFileValue other = (RegularFileValue) obj;
+ return rootedPath.equals(other.rootedPath) && fileStateValue.equals(other.fileStateValue);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(rootedPath, fileStateValue);
+ }
+
+ @Override
+ public String toString() {
+ return rootedPath + ", " + fileStateValue;
+ }
+ }
+
+ /**
+ * Base class for {@link FileValue}s for files whose fully resolved path is different than the
+ * requested path. For example, this is the case for the path "foo/bar/baz" if at least one of
+ * 'foo', 'foo/bar', or 'foo/bar/baz' is a symlink.
+ */
+ private static class DifferentRealPathFileValue extends FileValue {
+
+ protected final RootedPath realRootedPath;
+ protected final FileStateValue realFileStateValue;
+
+ private DifferentRealPathFileValue(RootedPath realRootedPath,
+ FileStateValue realFileStateValue) {
+ this.realRootedPath = Preconditions.checkNotNull(realRootedPath);
+ this.realFileStateValue = Preconditions.checkNotNull(realFileStateValue);
+ }
+
+ @Override
+ public RootedPath realRootedPath() {
+ return realRootedPath;
+ }
+
+ @Override
+ FileStateValue realFileStateValue() {
+ return realFileStateValue;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (obj.getClass() != DifferentRealPathFileValue.class) {
+ return false;
+ }
+ DifferentRealPathFileValue other = (DifferentRealPathFileValue) obj;
+ return realRootedPath.equals(other.realRootedPath)
+ && realFileStateValue.equals(other.realFileStateValue);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(realRootedPath, realFileStateValue);
+ }
+
+ @Override
+ public String toString() {
+ return realRootedPath + ", " + realFileStateValue + " (symlink ancestor)";
+ }
+ }
+
+ /** Implementation of {@link FileValue} for files that are symlinks. */
+ private static final class SymlinkFileValue extends DifferentRealPathFileValue {
+ private final PathFragment linkValue;
+
+ private SymlinkFileValue(RootedPath realRootedPath, FileStateValue realFileState,
+ PathFragment linkTarget) {
+ super(realRootedPath, realFileState);
+ this.linkValue = linkTarget;
+ }
+
+ @Override
+ public boolean isSymlink() {
+ return true;
+ }
+
+ @Override
+ public PathFragment getUnresolvedLinkTarget() {
+ return linkValue;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (obj.getClass() != SymlinkFileValue.class) {
+ return false;
+ }
+ SymlinkFileValue other = (SymlinkFileValue) obj;
+ return realRootedPath.equals(other.realRootedPath)
+ && realFileStateValue.equals(other.realFileStateValue)
+ && linkValue.equals(other.linkValue);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(realRootedPath, realFileStateValue, linkValue, Boolean.TRUE);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("symlink (real_path=%s, real_state=%s, link_value=%s)",
+ realRootedPath, realFileStateValue, linkValue);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/FilesetEntryFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/FilesetEntryFunction.java
new file mode 100644
index 0000000..a559206
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/FilesetEntryFunction.java
@@ -0,0 +1,320 @@
+// 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.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import com.google.devtools.build.lib.actions.FilesetOutputSymlink;
+import com.google.devtools.build.lib.actions.FilesetTraversalParams;
+import com.google.devtools.build.lib.actions.FilesetTraversalParams.DirectTraversal;
+import com.google.devtools.build.lib.skyframe.RecursiveFilesystemTraversalFunction.DanglingSymlinkException;
+import com.google.devtools.build.lib.skyframe.RecursiveFilesystemTraversalFunction.RecursiveFilesystemTraversalException;
+import com.google.devtools.build.lib.skyframe.RecursiveFilesystemTraversalValue.ResolvedFile;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionException;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.TreeMap;
+
+/** SkyFunction for {@link FilesetEntryValue}. */
+public final class FilesetEntryFunction implements SkyFunction {
+
+ private static final class MissingDepException extends Exception {}
+
+ private static final class FilesetEntryFunctionException extends SkyFunctionException {
+ FilesetEntryFunctionException(RecursiveFilesystemTraversalException e) {
+ super(e, Transience.PERSISTENT);
+ }
+ }
+
+ @Override
+ public SkyValue compute(SkyKey key, Environment env) throws FilesetEntryFunctionException {
+ FilesetTraversalParams t = (FilesetTraversalParams) key.argument();
+ Preconditions.checkState(
+ t.getNestedTraversal().isPresent() != t.getDirectTraversal().isPresent(),
+ "Exactly one of the nested and direct traversals must be specified: %s", t);
+
+ // Create the set of excluded files. Only top-level files can be excluded, i.e. ones that are
+ // directly under the root if the root is a directory.
+ Set<String> exclusions = createExclusionSet(t.getExcludedFiles());
+
+ // The map of output symlinks. Each key is the path of a output symlink that the Fileset must
+ // create, relative to the Fileset.out directory, and each value specifies extra information
+ // about the link (its target, associated metadata and again its name).
+ Map<PathFragment, FilesetOutputSymlink> outputSymlinks = new LinkedHashMap<>();
+
+ if (t.getNestedTraversal().isPresent()) {
+ // The "nested" traversal parameters are present if and only if FilesetEntry.srcdir specifies
+ // another Fileset (a "nested" one).
+ FilesetEntryValue nested = (FilesetEntryValue) env.getValue(
+ FilesetEntryValue.key(t.getNestedTraversal().get()));
+ if (env.valuesMissing()) {
+ return null;
+ }
+
+ for (FilesetOutputSymlink s : nested.getSymlinks()) {
+ maybeStoreSymlink(s, t.getDestPath(), exclusions, outputSymlinks);
+ }
+ } else {
+ // The "nested" traversal params are absent if and only if the "direct" traversal params are
+ // present, which is the case when the FilesetEntry specifies a package's BUILD file, a
+ // directory or a list of files.
+
+ // The root of the direct traversal is defined as follows.
+ //
+ // If FilesetEntry.files is specified, then a TraversalRequest is created for each entry, the
+ // root being the respective entry itself. These are all traversed for they may be
+ // directories or symlinks to directories, and we need to establish Skyframe dependencies on
+ // their contents for incremental correctness. If an entry is indeed a directory (but not when
+ // it's a symlink to one) then we have to create symlinks to each of their childen.
+ // (NB: there seems to be no good reason for this, it's just how legacy Fileset works. We may
+ // want to consider creating a symlink just for the directory and not for its child elements.)
+ //
+ // If FilesetEntry.files is not specified, then srcdir refers to either a BUILD file or a
+ // directory. For the former, the root will be the parent of the BUILD file. For the latter,
+ // the root will be srcdir itself.
+ DirectTraversal direct = t.getDirectTraversal().get();
+
+ RecursiveFilesystemTraversalValue rftv;
+ try {
+ // Traverse the filesystem to establish skyframe dependencies.
+ rftv = traverse(env, createErrorInfo(t), direct);
+ } catch (MissingDepException e) {
+ return null;
+ }
+
+ // The root can only be absent for the EMPTY rftv instance.
+ if (!rftv.getResolvedRoot().isPresent()) {
+ return FilesetEntryValue.EMPTY;
+ }
+
+ ResolvedFile resolvedRoot = rftv.getResolvedRoot().get();
+
+ // Handle dangling symlinks gracefully be returning empty results.
+ if (!resolvedRoot.type.exists()) {
+ return FilesetEntryValue.EMPTY;
+ }
+
+ // The prefix to remove is the entire path of the root. This is OK:
+ // - when the root is a file, this removes the entire path, but the traversal's destination
+ // path is actually the name of the output symlink, so this works out correctly
+ // - when the root is a directory or a symlink to one then we need to strip off the
+ // directory's path from every result (this is how the output symlinks must be created)
+ // before making them relative to the destination path
+ PathFragment prefixToRemove = direct.getRoot().getRelativePart();
+
+ Iterable<ResolvedFile> results = null;
+
+ if (direct.isRecursive()
+ || (resolvedRoot.type.isDirectory() && !resolvedRoot.type.isSymlink())) {
+ // The traversal is recursive (requested for an entire FilesetEntry.srcdir) or it was
+ // requested for a FilesetEntry.files entry which turned out to be a directory. We need to
+ // create an output symlink for every file in it and all of its subdirectories. Only
+ // exception is when the subdirectory is really a symlink to a directory -- no output
+ // shall be created for the contents of those.
+ // Now we create Dir objects to model the filesystem tree. The object employs a trick to
+ // find directory symlinks: directory symlinks have corresponding ResolvedFile entries and
+ // are added as files too, while their children, also added as files, contain the path of
+ // the parent. Finding and discarding the children is easy if we traverse the tree from
+ // root to leaf.
+ DirectoryTree root = new DirectoryTree();
+ for (ResolvedFile f : rftv.getTransitiveFiles().toCollection()) {
+ PathFragment path = f.getNameInSymlinkTree().relativeTo(prefixToRemove);
+ if (path.segmentCount() > 0) {
+ path = t.getDestPath().getRelative(path);
+ DirectoryTree dir = root;
+ for (int i = 0; i < path.segmentCount() - 1; ++i) {
+ dir = dir.addOrGetSubdir(path.getSegment(i));
+ }
+ dir.maybeAddFile(f);
+ }
+ }
+ // Here's where the magic happens. The returned iterable will yield all files in the
+ // directory that are not under symlinked directories, as well as all directory symlinks.
+ results = root.iterateFiles();
+ } else {
+ // If we're on this branch then the traversal was done for just one entry in
+ // FilesetEntry.files (which was not a directory, so it was either a file, a symlink to one
+ // or a symlink to a directory), meaning we'll have only one output symlink.
+ results = ImmutableList.of(resolvedRoot);
+ }
+
+ // Create one output symlink for each entry in the results.
+ for (ResolvedFile f : results) {
+ PathFragment targetName;
+ try {
+ targetName = f.getTargetInSymlinkTree(direct.isFollowingSymlinks());
+ } catch (DanglingSymlinkException e) {
+ throw new FilesetEntryFunctionException(e);
+ }
+
+ // Metadata field must be present. It can only be absent when stripped by tests.
+ String metadata = Integer.toHexString(f.metadata.get().hashCode());
+
+ // The linkName has to be under the traversal's root, which is also the prefix to remove.
+ PathFragment linkName = f.getNameInSymlinkTree().relativeTo(prefixToRemove);
+ maybeStoreSymlink(linkName, targetName, metadata, t.getDestPath(), exclusions,
+ outputSymlinks);
+ }
+ }
+
+ return FilesetEntryValue.of(ImmutableSet.copyOf(outputSymlinks.values()));
+ }
+
+ /** Stores an output symlink unless it's excluded or would overwrite an existing one. */
+ private static void maybeStoreSymlink(FilesetOutputSymlink nestedLink, PathFragment destPath,
+ Set<String> exclusions, Map<PathFragment, FilesetOutputSymlink> result) {
+ maybeStoreSymlink(nestedLink.name, nestedLink.target, nestedLink.metadata, destPath,
+ exclusions, result);
+ }
+
+ /** Stores an output symlink unless it's excluded or would overwrite an existing one. */
+ private static void maybeStoreSymlink(PathFragment linkName, PathFragment linkTarget,
+ String metadata, PathFragment destPath, Set<String> exclusions,
+ Map<PathFragment, FilesetOutputSymlink> result) {
+ if (!exclusions.contains(linkName.getPathString())) {
+ linkName = destPath.getRelative(linkName);
+ if (!result.containsKey(linkName)) {
+ result.put(linkName, new FilesetOutputSymlink(linkName, linkTarget, metadata));
+ }
+ }
+ }
+
+ private static Set<String> createExclusionSet(Set<String> input) {
+ return Sets.filter(input, new Predicate<String>() {
+ @Override
+ public boolean apply(String e) {
+ // Keep the top-level exclusions only. Do not look for "/" but count the path segments
+ // instead, in anticipation of future Windows support.
+ return new PathFragment(e).segmentCount() == 1;
+ }
+ });
+ }
+
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+
+ private RecursiveFilesystemTraversalValue traverse(Environment env, String errorInfo,
+ DirectTraversal traversal) throws MissingDepException {
+ SkyKey depKey = RecursiveFilesystemTraversalValue.key(
+ new RecursiveFilesystemTraversalValue.TraversalRequest(traversal.getRoot().asRootedPath(),
+ traversal.isGenerated(), traversal.getCrossPackageBoundary(), traversal.isPackage(),
+ errorInfo));
+ RecursiveFilesystemTraversalValue v = (RecursiveFilesystemTraversalValue) env.getValue(depKey);
+ if (env.valuesMissing()) {
+ throw new MissingDepException();
+ }
+ return v;
+ }
+
+ private static String createErrorInfo(FilesetTraversalParams t) {
+ if (t.getDirectTraversal().isPresent()) {
+ DirectTraversal direct = t.getDirectTraversal().get();
+ return String.format("Fileset '%s' traversing %s %s", t.getOwnerLabel(),
+ direct.isPackage() ? "package" : "file (or directory)",
+ direct.getRoot().getRelativePart().getPathString());
+ } else {
+ return String.format("Fileset '%s' traversing another Fileset", t.getOwnerLabel());
+ }
+ }
+
+ /**
+ * Models a FilesetEntryFunction's portion of the symlink output tree created by a Fileset rule.
+ *
+ * <p>A Fileset rule's output is computed by zero or more {@link FilesetEntryFunction}s, resulting
+ * in one {@link FilesetEntryValue} for each. Each of those represents a portion of the grand
+ * output tree of the Fileset. These portions are later merged and written to the fileset manifest
+ * file, which is then consumed by a tool that ultimately creates the symlinks in the filesystem.
+ *
+ * <p>Because the Fileset doesn't process the lists in the FilesetEntryValues any further than
+ * merging them, they have to adhere to the conventions of the manifest file. One of these is that
+ * files are alphabetically ordered (enables the consumer of the manifest to work faster than
+ * otherwise) and another is that the contents of regular directories are listed, but contents
+ * of directory symlinks are not, only the symlinks are. (Other details of the manifest file are
+ * not relevant here.)
+ *
+ * <p>See {@link DirectoryTree#iterateFiles()} for more details.
+ */
+ private static final class DirectoryTree {
+ // Use TreeMaps for the benefit of alphabetically ordered iteration.
+ public final Map<String, ResolvedFile> files = new TreeMap<>();
+ public final Map<String, DirectoryTree> dirs = new TreeMap<>();
+
+ DirectoryTree addOrGetSubdir(String name) {
+ DirectoryTree result = dirs.get(name);
+ if (result == null) {
+ result = new DirectoryTree();
+ dirs.put(name, result);
+ }
+ return result;
+ }
+
+ void maybeAddFile(ResolvedFile r) {
+ String name = r.getNameInSymlinkTree().getBaseName();
+ if (!files.containsKey(name)) {
+ files.put(name, r);
+ }
+ }
+
+ /**
+ * Lazily yields all files in this directory and all of its subdirectories.
+ *
+ * <p>The function first yields all the files directly under this directory, in alphabetical
+ * order. Then come the contents of subdirectories, processed recursively in the same fashion
+ * as this directory, and also in alphabetical order.
+ *
+ * <p>If a directory symlink is encountered its contents are not listed, only the symlink is.
+ */
+ Iterable<ResolvedFile> iterateFiles() {
+ // 1. Filter directory symlinks. If the symlink target contains files, those were added
+ // as normal files so their parent directory (the symlink) would show up in the dirs map
+ // (as a directory) as well as in the files map (as a symlink to a directory).
+ final Set<String> fileNames = files.keySet();
+ Iterable<Map.Entry<String, DirectoryTree>> noDirSymlinkes = Iterables.filter(dirs.entrySet(),
+ new Predicate<Map.Entry<String, DirectoryTree>>() {
+ @Override
+ public boolean apply(Map.Entry<String, DirectoryTree> input) {
+ return !fileNames.contains(input.getKey());
+ }
+ });
+
+ // 2. Extract the iterables of the true subdirectories.
+ Iterable<Iterable<ResolvedFile>> subdirIters = Iterables.transform(noDirSymlinkes,
+ new Function<Map.Entry<String, DirectoryTree>, Iterable<ResolvedFile>>() {
+ @Override
+ public Iterable<ResolvedFile> apply(Entry<String, DirectoryTree> input) {
+ return input.getValue().iterateFiles();
+ }
+ });
+
+ // 3. Just concat all subdirectory iterations for one, seamless iteration.
+ Iterable<ResolvedFile> dirsIter = Iterables.concat(subdirIters);
+
+ return Iterables.concat(files.values(), dirsIter);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/FilesetEntryValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/FilesetEntryValue.java
new file mode 100644
index 0000000..e7b6580
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/FilesetEntryValue.java
@@ -0,0 +1,49 @@
+// 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.collect.ImmutableSet;
+import com.google.devtools.build.lib.actions.FilesetOutputSymlink;
+import com.google.devtools.build.lib.actions.FilesetTraversalParams;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+/** Output symlinks produced by a whole FilesetEntry or by a single file in FilesetEntry.files. */
+public final class FilesetEntryValue implements SkyValue {
+ static final FilesetEntryValue EMPTY =
+ new FilesetEntryValue(ImmutableSet.<FilesetOutputSymlink>of());
+
+ private final ImmutableSet<FilesetOutputSymlink> symlinks;
+
+ private FilesetEntryValue(ImmutableSet<FilesetOutputSymlink> symlinks) {
+ this.symlinks = symlinks;
+ }
+
+ static FilesetEntryValue of(ImmutableSet<FilesetOutputSymlink> symlinks) {
+ if (symlinks.isEmpty()) {
+ return EMPTY;
+ } else {
+ return new FilesetEntryValue(symlinks);
+ }
+ }
+
+ /** Returns the list of output symlinks. */
+ public ImmutableSet<FilesetOutputSymlink> getSymlinks() {
+ return symlinks;
+ }
+
+ public static SkyKey key(FilesetTraversalParams params) {
+ return new SkyKey(SkyFunctions.FILESET_ENTRY, params);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/FilesystemValueChecker.java b/src/main/java/com/google/devtools/build/lib/skyframe/FilesystemValueChecker.java
new file mode 100644
index 0000000..be4f4e8
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/FilesystemValueChecker.java
@@ -0,0 +1,398 @@
+// 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.base.Predicate;
+import com.google.common.base.Supplier;
+import com.google.common.base.Suppliers;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.concurrent.ExecutorShutdownUtil;
+import com.google.devtools.build.lib.concurrent.Sharder;
+import com.google.devtools.build.lib.concurrent.ThrowableRecordingRunnableWrapper;
+import com.google.devtools.build.lib.util.LoggingUtil;
+import com.google.devtools.build.lib.util.Pair;
+import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor;
+import com.google.devtools.build.lib.vfs.BatchStat;
+import com.google.devtools.build.lib.vfs.FileStatusWithDigest;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.skyframe.Differencer;
+import com.google.devtools.build.skyframe.MemoizingEvaluator;
+import com.google.devtools.build.skyframe.SkyFunctionName;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.annotation.Nullable;
+
+/**
+ * A helper class to find dirty values by accessing the filesystem directly (contrast with
+ * {@link DiffAwareness}).
+ */
+class FilesystemValueChecker {
+
+ private static final int DIRTINESS_CHECK_THREADS = 50;
+ private static final Logger LOG = Logger.getLogger(FilesystemValueChecker.class.getName());
+
+ private static final Predicate<SkyKey> FILE_STATE_AND_DIRECTORY_LISTING_STATE_FILTER =
+ SkyFunctionName.functionIsIn(ImmutableSet.of(SkyFunctions.FILE_STATE,
+ SkyFunctions.DIRECTORY_LISTING_STATE));
+ private static final Predicate<SkyKey> ACTION_FILTER =
+ SkyFunctionName.functionIs(SkyFunctions.ACTION_EXECUTION);
+
+ private final TimestampGranularityMonitor tsgm;
+ private final Supplier<Map<SkyKey, SkyValue>> valuesSupplier;
+ private AtomicInteger modifiedOutputFilesCounter = new AtomicInteger(0);
+
+ FilesystemValueChecker(final MemoizingEvaluator evaluator, TimestampGranularityMonitor tsgm) {
+ this.tsgm = tsgm;
+
+ // Construct the full map view of the entire graph at most once ("memoized"), lazily. If
+ // getDirtyFilesystemValues(Iterable<SkyKey>) is called on an empty Iterable, we avoid having
+ // to create the Map of value keys to values. This is useful in the case where the graph
+ // getValues() method could be slow.
+ this.valuesSupplier = Suppliers.memoize(new Supplier<Map<SkyKey, SkyValue>>() {
+ @Override
+ public Map<SkyKey, SkyValue> get() {
+ return evaluator.getValues();
+ }
+ });
+ }
+
+ Iterable<SkyKey> getFilesystemSkyKeys() {
+ return Iterables.filter(valuesSupplier.get().keySet(),
+ FILE_STATE_AND_DIRECTORY_LISTING_STATE_FILTER);
+ }
+
+ Differencer.Diff getDirtyFilesystemSkyKeys() throws InterruptedException {
+ return getDirtyFilesystemValues(getFilesystemSkyKeys());
+ }
+
+ /**
+ * Check the given file and directory values for modifications. {@code values} is assumed to only
+ * have {@link FileValue}s and {@link DirectoryListingStateValue}s.
+ */
+ Differencer.Diff getDirtyFilesystemValues(Iterable<SkyKey> values)
+ throws InterruptedException {
+ return getDirtyValues(values, FILE_STATE_AND_DIRECTORY_LISTING_STATE_FILTER,
+ new DirtyChecker() {
+ @Override
+ public DirtyResult check(SkyKey key, SkyValue oldValue, TimestampGranularityMonitor tsgm) {
+ if (key.functionName() == SkyFunctions.FILE_STATE) {
+ return checkFileStateValue((RootedPath) key.argument(), (FileStateValue) oldValue,
+ tsgm);
+ } else if (key.functionName() == SkyFunctions.DIRECTORY_LISTING_STATE) {
+ return checkDirectoryListingStateValue((RootedPath) key.argument(),
+ (DirectoryListingStateValue) oldValue);
+ } else {
+ throw new IllegalStateException("Unexpected key type " + key);
+ }
+ }
+ });
+ }
+
+ /**
+ * Return a collection of action values which have output files that are not in-sync with
+ * the on-disk file value (were modified externally).
+ */
+ public Collection<SkyKey> getDirtyActionValues(@Nullable final BatchStat batchStatter)
+ throws InterruptedException {
+ // CPU-bound (usually) stat() calls, plus a fudge factor.
+ LOG.info("Accumulating dirty actions");
+ final int numOutputJobs = Runtime.getRuntime().availableProcessors() * 4;
+ final Set<SkyKey> actionSkyKeys =
+ Sets.filter(valuesSupplier.get().keySet(), ACTION_FILTER);
+ final Sharder<Pair<SkyKey, ActionExecutionValue>> outputShards =
+ new Sharder<>(numOutputJobs, actionSkyKeys.size());
+
+ for (SkyKey key : actionSkyKeys) {
+ outputShards.add(Pair.of(key, (ActionExecutionValue) valuesSupplier.get().get(key)));
+ }
+ LOG.info("Sharded action values for batching");
+
+ ExecutorService executor = Executors.newFixedThreadPool(
+ numOutputJobs,
+ new ThreadFactoryBuilder().setNameFormat("FileSystem Output File Invalidator %d").build());
+
+ Collection<SkyKey> dirtyKeys = Sets.newConcurrentHashSet();
+ ThrowableRecordingRunnableWrapper wrapper =
+ new ThrowableRecordingRunnableWrapper("FileSystemValueChecker#getDirtyActionValues");
+
+ modifiedOutputFilesCounter.set(0);
+ for (List<Pair<SkyKey, ActionExecutionValue>> shard : outputShards) {
+ Runnable job = (batchStatter == null)
+ ? outputStatJob(dirtyKeys, shard)
+ : batchStatJob(dirtyKeys, shard, batchStatter);
+ executor.submit(wrapper.wrap(job));
+ }
+
+ boolean interrupted = ExecutorShutdownUtil.interruptibleShutdown(executor);
+ Throwables.propagateIfPossible(wrapper.getFirstThrownError());
+ LOG.info("Completed output file stat checks");
+ if (interrupted) {
+ throw new InterruptedException();
+ }
+ return dirtyKeys;
+ }
+
+ private Runnable batchStatJob(final Collection<SkyKey> dirtyKeys,
+ final List<Pair<SkyKey, ActionExecutionValue>> shard,
+ final BatchStat batchStatter) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ Map<Artifact, Pair<SkyKey, ActionExecutionValue>> artifactToKeyAndValue = new HashMap<>();
+ for (Pair<SkyKey, ActionExecutionValue> keyAndValue : shard) {
+ ActionExecutionValue actionValue = keyAndValue.getSecond();
+ if (actionValue == null) {
+ dirtyKeys.add(keyAndValue.getFirst());
+ } else {
+ for (Artifact artifact : actionValue.getAllOutputArtifactData().keySet()) {
+ artifactToKeyAndValue.put(artifact, keyAndValue);
+ }
+ }
+ }
+
+ List<Artifact> artifacts = ImmutableList.copyOf(artifactToKeyAndValue.keySet());
+ List<FileStatusWithDigest> stats;
+ try {
+ stats = batchStatter.batchStat(/*includeDigest=*/true, /*includeLinks=*/true,
+ Artifact.asPathFragments(artifacts));
+ } catch (IOException e) {
+ // Batch stat did not work. Log an exception and fall back on system calls.
+ LoggingUtil.logToRemote(Level.WARNING, "Unable to process batch stat", e);
+ outputStatJob(dirtyKeys, shard).run();
+ return;
+ } catch (InterruptedException e) {
+ // We handle interrupt in the main thread.
+ return;
+ }
+
+ Preconditions.checkState(artifacts.size() == stats.size(),
+ "artifacts.size() == %s stats.size() == %s", artifacts.size(), stats.size());
+ for (int i = 0; i < artifacts.size(); i++) {
+ Artifact artifact = artifacts.get(i);
+ FileStatusWithDigest stat = stats.get(i);
+ Pair<SkyKey, ActionExecutionValue> keyAndValue = artifactToKeyAndValue.get(artifact);
+ ActionExecutionValue actionValue = keyAndValue.getSecond();
+ SkyKey key = keyAndValue.getFirst();
+ FileValue lastKnownData = actionValue.getAllOutputArtifactData().get(artifact);
+ try {
+ FileValue newData = FileAndMetadataCache.fileValueFromArtifact(artifact, stat, tsgm);
+ if (!newData.equals(lastKnownData)) {
+ modifiedOutputFilesCounter.getAndIncrement();
+ dirtyKeys.add(key);
+ }
+ } catch (IOException e) {
+ // This is an unexpected failure getting a digest or symlink target.
+ modifiedOutputFilesCounter.getAndIncrement();
+ dirtyKeys.add(key);
+ }
+ }
+ }
+ };
+ }
+
+ private Runnable outputStatJob(final Collection<SkyKey> dirtyKeys,
+ final List<Pair<SkyKey, ActionExecutionValue>> shard) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ for (Pair<SkyKey, ActionExecutionValue> keyAndValue : shard) {
+ ActionExecutionValue value = keyAndValue.getSecond();
+ if (value == null || actionValueIsDirtyWithDirectSystemCalls(value)) {
+ dirtyKeys.add(keyAndValue.getFirst());
+ }
+ }
+ }
+ };
+ }
+
+ /**
+ * Returns number of modified output files inside of dirty actions.
+ */
+ int getNumberOfModifiedOutputFiles() {
+ return modifiedOutputFilesCounter.get();
+ }
+
+ private boolean actionValueIsDirtyWithDirectSystemCalls(ActionExecutionValue actionValue) {
+ boolean isDirty = false;
+ for (Map.Entry<Artifact, FileValue> entry :
+ actionValue.getAllOutputArtifactData().entrySet()) {
+ Artifact artifact = entry.getKey();
+ FileValue lastKnownData = entry.getValue();
+ try {
+ if (!FileAndMetadataCache.fileValueFromArtifact(artifact, null, tsgm).equals(
+ lastKnownData)) {
+ modifiedOutputFilesCounter.getAndIncrement();
+ isDirty = true;
+ }
+ } catch (IOException e) {
+ // This is an unexpected failure getting a digest or symlink target.
+ modifiedOutputFilesCounter.getAndIncrement();
+ isDirty = true;
+ }
+ }
+ return isDirty;
+ }
+
+ private BatchDirtyResult getDirtyValues(Iterable<SkyKey> values,
+ Predicate<SkyKey> keyFilter,
+ final DirtyChecker checker) throws InterruptedException {
+ ExecutorService executor = Executors.newFixedThreadPool(DIRTINESS_CHECK_THREADS,
+ new ThreadFactoryBuilder().setNameFormat("FileSystem Value Invalidator %d").build());
+
+ final BatchDirtyResult batchResult = new BatchDirtyResult();
+ ThrowableRecordingRunnableWrapper wrapper =
+ new ThrowableRecordingRunnableWrapper("FilesystemValueChecker#getDirtyValues");
+ for (final SkyKey key : values) {
+ Preconditions.checkState(keyFilter.apply(key), key);
+ final SkyValue value = valuesSupplier.get().get(key);
+ executor.execute(wrapper.wrap(new Runnable() {
+ @Override
+ public void run() {
+ if (value == null) {
+ // value will be null if the value is in error or part of a cycle.
+ // TODO(bazel-team): This is overly conservative.
+ batchResult.add(key, /*newValue=*/null);
+ return;
+ }
+ DirtyResult result = checker.check(key, value, tsgm);
+ if (result.isDirty()) {
+ batchResult.add(key, result.getNewValue());
+ }
+ }
+ }));
+ }
+
+ boolean interrupted = ExecutorShutdownUtil.interruptibleShutdown(executor);
+ Throwables.propagateIfPossible(wrapper.getFirstThrownError());
+ if (interrupted) {
+ throw new InterruptedException();
+ }
+ return batchResult;
+ }
+
+ private static DirtyResult checkFileStateValue(RootedPath rootedPath,
+ FileStateValue fileStateValue, TimestampGranularityMonitor tsgm) {
+ try {
+ FileStateValue newValue = FileStateValue.create(rootedPath, tsgm);
+ return newValue.equals(fileStateValue)
+ ? DirtyResult.NOT_DIRTY : DirtyResult.dirtyWithNewValue(newValue);
+ } catch (InconsistentFilesystemException | IOException e) {
+ // TODO(bazel-team): An IOException indicates a failure to get a file digest or a symlink
+ // target, not a missing file. Such a failure really shouldn't happen, so failing early
+ // may be better here.
+ return DirtyResult.DIRTY;
+ }
+ }
+
+ private static DirtyResult checkDirectoryListingStateValue(RootedPath dirRootedPath,
+ DirectoryListingStateValue directoryListingStateValue) {
+ try {
+ DirectoryListingStateValue newValue = DirectoryListingStateValue.create(dirRootedPath);
+ return newValue.equals(directoryListingStateValue)
+ ? DirtyResult.NOT_DIRTY : DirtyResult.dirtyWithNewValue(newValue);
+ } catch (IOException e) {
+ return DirtyResult.DIRTY;
+ }
+ }
+
+ /**
+ * Result of a batch call to {@link DirtyChecker#check}. Partitions the dirty values based on
+ * whether we have a new value available for them or not.
+ */
+ private static class BatchDirtyResult implements Differencer.Diff {
+
+ private final Set<SkyKey> concurrentDirtyKeysWithoutNewValues =
+ Collections.newSetFromMap(new ConcurrentHashMap<SkyKey, Boolean>());
+ private final ConcurrentHashMap<SkyKey, SkyValue> concurrentDirtyKeysWithNewValues =
+ new ConcurrentHashMap<>();
+
+ private void add(SkyKey key, @Nullable SkyValue newValue) {
+ if (newValue == null) {
+ concurrentDirtyKeysWithoutNewValues.add(key);
+ } else {
+ concurrentDirtyKeysWithNewValues.put(key, newValue);
+ }
+ }
+
+ @Override
+ public Iterable<SkyKey> changedKeysWithoutNewValues() {
+ return concurrentDirtyKeysWithoutNewValues;
+ }
+
+ @Override
+ public Map<SkyKey, ? extends SkyValue> changedKeysWithNewValues() {
+ return concurrentDirtyKeysWithNewValues;
+ }
+ }
+
+ private static class DirtyResult {
+
+ static final DirtyResult NOT_DIRTY = new DirtyResult(false, null);
+ static final DirtyResult DIRTY = new DirtyResult(true, null);
+
+ private final boolean isDirty;
+ @Nullable private final SkyValue newValue;
+
+ private DirtyResult(boolean isDirty, @Nullable SkyValue newValue) {
+ this.isDirty = isDirty;
+ this.newValue = newValue;
+ }
+
+ boolean isDirty() {
+ return isDirty;
+ }
+
+ /**
+ * If {@code isDirty()}, then either returns the new value for the value or {@code null} if
+ * the new value wasn't computed. In the case where the value is dirty and a new value is
+ * available, then the new value can be injected into the skyframe graph. Otherwise, the value
+ * should simply be invalidated.
+ */
+ @Nullable
+ SkyValue getNewValue() {
+ Preconditions.checkState(isDirty());
+ return newValue;
+ }
+
+ static DirtyResult dirtyWithNewValue(SkyValue newValue) {
+ return new DirtyResult(true, newValue);
+ }
+ }
+
+ private static interface DirtyChecker {
+ DirtyResult check(SkyKey key, SkyValue oldValue, TimestampGranularityMonitor tsgm);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/GlobDescriptor.java b/src/main/java/com/google/devtools/build/lib/skyframe/GlobDescriptor.java
new file mode 100644
index 0000000..5baeae8
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/GlobDescriptor.java
@@ -0,0 +1,113 @@
+// 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.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.util.StringCanonicalizer;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+/**
+ * A descriptor for a glob request.
+ *
+ * <p>{@code subdir} must be empty or point to an existing directory.</p>
+ *
+ * <p>{@code pattern} must be valid, as indicated by {@code UnixGlob#checkPatternForError}.
+ */
+@ThreadSafe
+public final class GlobDescriptor implements Serializable {
+ final PackageIdentifier packageId;
+ final PathFragment subdir;
+ final String pattern;
+ final boolean excludeDirs;
+
+ /**
+ * Constructs a GlobDescriptor.
+ *
+ * @param packageId the name of the owner package (must be an existing package)
+ * @param subdir the subdirectory being looked at (must exist and must be a directory. It's
+ * assumed that there are no other packages between {@code packageName} and
+ * {@code subdir}.
+ * @param pattern a valid glob pattern
+ * @param excludeDirs true if directories should be excluded from results
+ */
+ GlobDescriptor(PackageIdentifier packageId, PathFragment subdir, String pattern,
+ boolean excludeDirs) {
+ this.packageId = Preconditions.checkNotNull(packageId);
+ this.subdir = Preconditions.checkNotNull(subdir);
+ this.pattern = Preconditions.checkNotNull(StringCanonicalizer.intern(pattern));
+ this.excludeDirs = excludeDirs;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("<GlobDescriptor packageName=%s subdir=%s pattern=%s excludeDirs=%s>",
+ packageId, subdir, pattern, excludeDirs);
+ }
+
+ /**
+ * Returns the package that "owns" this glob.
+ *
+ * <p>The glob evaluation code ensures that the boundaries of this package are not crossed.
+ */
+ public PackageIdentifier getPackageId() {
+ return packageId;
+ }
+
+ /**
+ * Returns the subdirectory of the package under consideration.
+ */
+ PathFragment getSubdir() {
+ return subdir;
+ }
+
+ /**
+ * Returns the glob pattern under consideration. May contain wildcards.
+ *
+ * <p>As the glob evaluator traverses deeper into the file tree, components are added at the
+ * beginning of {@code subdir} and removed from the beginning of {@code pattern}.
+ */
+ String getPattern() {
+ return pattern;
+ }
+
+ /**
+ * Returns true if directories should be excluded from results.
+ */
+ boolean excludeDirs() {
+ return excludeDirs;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(packageId, subdir, pattern, excludeDirs);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof GlobDescriptor)) {
+ return false;
+ }
+ GlobDescriptor other = (GlobDescriptor) obj;
+ return packageId.equals(other.packageId) && subdir.equals(other.subdir)
+ && pattern.equals(other.pattern) && excludeDirs == other.excludeDirs;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/GlobFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/GlobFunction.java
new file mode 100644
index 0000000..a1fdcb2
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/GlobFunction.java
@@ -0,0 +1,251 @@
+// 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.cache.CacheBuilder;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.vfs.Dirent;
+import com.google.devtools.build.lib.vfs.Dirent.Type;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.lib.vfs.UnixGlob;
+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 java.util.regex.Pattern;
+
+import javax.annotation.Nullable;
+
+/**
+ * A {@link SkyFunction} for {@link GlobValue}s.
+ *
+ * <p>This code drives the glob matching process.
+ */
+final class GlobFunction implements SkyFunction {
+
+ private final Cache<String, Pattern> regexPatternCache =
+ CacheBuilder.newBuilder().concurrencyLevel(4).build();
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) throws GlobFunctionException {
+ GlobDescriptor glob = (GlobDescriptor) skyKey.argument();
+
+ PackageLookupValue globPkgLookupValue = (PackageLookupValue)
+ env.getValue(PackageLookupValue.key(glob.getPackageId()));
+ if (globPkgLookupValue == null) {
+ return null;
+ }
+ Preconditions.checkState(globPkgLookupValue.packageExists(), "%s isn't an existing package",
+ glob.getPackageId());
+ // Note that this implies that the package's BUILD file exists which implies that the
+ // package's directory exists.
+
+ PathFragment globSubdir = glob.getSubdir();
+ if (!globSubdir.equals(PathFragment.EMPTY_FRAGMENT)) {
+ PackageLookupValue globSubdirPkgLookupValue = (PackageLookupValue) env.getValue(
+ PackageLookupValue.key(glob.getPackageId().getPackageFragment()
+ .getRelative(globSubdir)));
+ if (globSubdirPkgLookupValue == null) {
+ return null;
+ }
+ if (globSubdirPkgLookupValue.packageExists()) {
+ // We crossed the package boundary, that is, pkg/subdir contains a BUILD file and thus
+ // defines another package, so glob expansion should not descend into that subdir.
+ return GlobValue.EMPTY;
+ }
+ }
+
+ String pattern = glob.getPattern();
+ // Split off the first path component of the pattern.
+ int slashPos = pattern.indexOf("/");
+ String patternHead;
+ String patternTail;
+ if (slashPos == -1) {
+ patternHead = pattern;
+ patternTail = null;
+ } else {
+ // Substrings will share the backing array of the original glob string. That should be fine.
+ patternHead = pattern.substring(0, slashPos);
+ patternTail = pattern.substring(slashPos + 1);
+ }
+
+ NestedSetBuilder<PathFragment> matches = NestedSetBuilder.stableOrder();
+
+ // "**" also matches an empty segment, so try the case where it is not present.
+ if ("**".equals(patternHead)) {
+ if (patternTail == null) {
+ if (!glob.excludeDirs()) {
+ matches.add(globSubdir);
+ }
+ } else {
+ SkyKey globKey = GlobValue.internalKey(
+ glob.getPackageId(), globSubdir, patternTail, glob.excludeDirs());
+ GlobValue globValue = (GlobValue) env.getValue(globKey);
+ if (globValue == null) {
+ return null;
+ }
+ matches.addTransitive(globValue.getMatches());
+ }
+ }
+
+ PathFragment dirPathFragment = glob.getPackageId().getPackageFragment().getRelative(globSubdir);
+ RootedPath dirRootedPath = RootedPath.toRootedPath(globPkgLookupValue.getRoot(),
+ dirPathFragment);
+ if (containsGlobs(patternHead)) {
+ // Pattern contains globs, so a directory listing is required.
+ //
+ // Note that we have good reason to believe the directory exists: if this is the
+ // top-level directory of the package, the package's existence implies the directory's
+ // existence; if this is a lower-level directory in the package, then we got here from
+ // previous directory listings. Filesystem operations concurrent with build could mean the
+ // directory no longer exists, but DirectoryListingFunction handles that gracefully.
+ DirectoryListingValue listingValue = (DirectoryListingValue)
+ env.getValue(DirectoryListingValue.key(dirRootedPath));
+ if (listingValue == null) {
+ return null;
+ }
+
+ for (Dirent dirent : listingValue.getDirents()) {
+ Type direntType = dirent.getType();
+ String fileName = dirent.getName();
+
+ boolean isDirectory = (direntType == Dirent.Type.DIRECTORY);
+
+ if (!UnixGlob.matches(patternHead, fileName, regexPatternCache)) {
+ continue;
+ }
+
+ if (direntType == Dirent.Type.SYMLINK) {
+ // TODO(bazel-team): Consider extracting the symlink resolution logic.
+ // For symlinks, look up the corresponding FileValue. This ensures that if the symlink
+ // changes and "switches types" (say, from a file to a directory), this value will be
+ // invalidated.
+ RootedPath symlinkRootedPath = RootedPath.toRootedPath(globPkgLookupValue.getRoot(),
+ dirPathFragment.getRelative(fileName));
+ FileValue symlinkFileValue = (FileValue) env.getValue(FileValue.key(symlinkRootedPath));
+ if (symlinkFileValue == null) {
+ continue;
+ }
+ if (!symlinkFileValue.isSymlink()) {
+ throw new GlobFunctionException(new InconsistentFilesystemException(
+ "readdir and stat disagree about whether " + symlinkRootedPath.asPath()
+ + " is a symlink."), Transience.TRANSIENT);
+ }
+ isDirectory = symlinkFileValue.isDirectory();
+ }
+
+ String subdirPattern = "**".equals(patternHead) ? glob.getPattern() : patternTail;
+ addFile(fileName, glob, subdirPattern, patternTail == null, isDirectory,
+ matches, env);
+ }
+ } else {
+ // Pattern does not contain globs, so a direct stat is enough.
+ String fileName = patternHead;
+ RootedPath fileRootedPath = RootedPath.toRootedPath(globPkgLookupValue.getRoot(),
+ dirPathFragment.getRelative(fileName));
+ FileValue fileValue = (FileValue) env.getValue(FileValue.key(fileRootedPath));
+ if (fileValue == null) {
+ return null;
+ }
+ if (fileValue.exists()) {
+ addFile(fileName, glob, patternTail, patternTail == null,
+ fileValue.isDirectory(), matches, env);
+ }
+ }
+
+ if (env.valuesMissing()) {
+ return null;
+ }
+
+ NestedSet<PathFragment> matchesBuilt = matches.build();
+ // Use the same value to represent that we did not match anything.
+ if (matchesBuilt.isEmpty()) {
+ return GlobValue.EMPTY;
+ }
+ return new GlobValue(matchesBuilt);
+ }
+
+ /**
+ * Returns true if the given pattern contains globs.
+ */
+ private boolean containsGlobs(String pattern) {
+ return pattern.contains("*") || pattern.contains("?");
+ }
+
+ /**
+ * Includes the given file/directory in the glob.
+ *
+ * <p>{@code fileName} must exist.
+ *
+ * <p>{@code isDirectory} must be true iff the file is a directory.
+ *
+ * <p>{@code directResult} must be set if the file should be included in the result set
+ * directly rather than recursed into if it is a directory.
+ */
+ private void addFile(String fileName, GlobDescriptor glob, String subdirPattern,
+ boolean directResult, boolean isDirectory, NestedSetBuilder<PathFragment> matches,
+ Environment env) {
+ if (isDirectory && subdirPattern != null) {
+ // This is a directory, and the pattern covers files under that directory.
+ SkyKey subdirGlobKey = GlobValue.internalKey(glob.getPackageId(),
+ glob.getSubdir().getRelative(fileName), subdirPattern, glob.excludeDirs());
+ GlobValue subdirGlob = (GlobValue) env.getValue(subdirGlobKey);
+ if (subdirGlob == null) {
+ return;
+ }
+ matches.addTransitive(subdirGlob.getMatches());
+ }
+
+ if (directResult && !(isDirectory && glob.excludeDirs())) {
+ if (isDirectory) {
+ // TODO(bazel-team): Refactor. This is basically inlined code from the next recursion level.
+ // Ensure that subdirectories that contain other packages are not picked up.
+ PathFragment directory = glob.getPackageId().getPackageFragment()
+ .getRelative(glob.getSubdir()).getRelative(fileName);
+ PackageLookupValue pkgLookupValue = (PackageLookupValue)
+ env.getValue(PackageLookupValue.key(directory));
+ if (pkgLookupValue == null) {
+ return;
+ }
+ if (pkgLookupValue.packageExists()) {
+ // The file is a directory and contains another package.
+ return;
+ }
+ }
+ matches.add(glob.getSubdir().getRelative(fileName));
+ }
+ }
+
+ @Nullable
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+
+ /**
+ * Used to declare all the exception types that can be wrapped in the exception thrown by
+ * {@link GlobFunction#compute}.
+ */
+ private static final class GlobFunctionException extends SkyFunctionException {
+ public GlobFunctionException(InconsistentFilesystemException e, Transience transience) {
+ super(e, transience);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/GlobValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/GlobValue.java
new file mode 100644
index 0000000..6de0fbd
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/GlobValue.java
@@ -0,0 +1,132 @@
+// 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.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.UnixGlob;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+/**
+ * A value corresponding to a glob.
+ */
+@Immutable
+@ThreadSafe
+final class GlobValue implements SkyValue {
+
+ static final GlobValue EMPTY = new GlobValue(
+ NestedSetBuilder.<PathFragment>emptySet(Order.STABLE_ORDER));
+
+ private final NestedSet<PathFragment> matches;
+
+ GlobValue(NestedSet<PathFragment> matches) {
+ this.matches = Preconditions.checkNotNull(matches);
+ }
+
+ /**
+ * Returns glob matches.
+ */
+ NestedSet<PathFragment> getMatches() {
+ return matches;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == this) {
+ return true;
+ }
+ if (!(other instanceof GlobValue)) {
+ return false;
+ }
+ // shallowEquals() may fail to detect that two equivalent (according to toString())
+ // NestedSets are equal, but will always detect when two NestedSets are different.
+ // This makes this implementation of equals() overly strict, but we only call this
+ // method when doing change pruning, which can accept false negatives.
+ return getMatches().shallowEquals(((GlobValue) other).getMatches());
+ }
+
+ @Override
+ public int hashCode() {
+ return matches.shallowHashCode();
+ }
+
+ /**
+ * Constructs a {@link SkyKey} for a glob lookup. {@code packageName} is assumed to be an
+ * existing package. Trying to glob into a non-package is undefined behavior.
+ *
+ * @throws InvalidGlobPatternException if the pattern is not valid.
+ */
+ @ThreadSafe
+ static SkyKey key(PackageIdentifier packageId, String pattern, boolean excludeDirs)
+ throws InvalidGlobPatternException {
+ if (pattern.indexOf('?') != -1) {
+ throw new InvalidGlobPatternException(pattern, "wildcard ? forbidden");
+ }
+
+ String error = UnixGlob.checkPatternForError(pattern);
+ if (error != null) {
+ throw new InvalidGlobPatternException(pattern, error);
+ }
+
+ return internalKey(packageId, PathFragment.EMPTY_FRAGMENT, pattern, excludeDirs);
+ }
+
+ /**
+ * Constructs a {@link SkyKey} for a glob lookup.
+ *
+ * <p>Do not use outside {@code GlobFunction}.
+ */
+ @ThreadSafe
+ static SkyKey internalKey(PackageIdentifier packageId, PathFragment subdir, String pattern,
+ boolean excludeDirs) {
+ return new SkyKey(SkyFunctions.GLOB,
+ new GlobDescriptor(packageId, subdir, pattern, excludeDirs));
+ }
+
+ /**
+ * Constructs a {@link SkyKey} for a glob lookup.
+ *
+ * <p>Do not use outside {@code GlobFunction}.
+ */
+ @ThreadSafe
+ static SkyKey internalKey(GlobDescriptor glob, String subdirName) {
+ return internalKey(glob.packageId, glob.subdir.getRelative(subdirName),
+ glob.pattern, glob.excludeDirs);
+ }
+
+ /**
+ * An exception that indicates that a glob pattern is syntactically invalid.
+ */
+ @ThreadSafe
+ static final class InvalidGlobPatternException extends Exception {
+ private final String pattern;
+
+ InvalidGlobPatternException(String pattern, String error) {
+ super(error);
+ this.pattern = pattern;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("invalid glob pattern '%s': %s", pattern, getMessage());
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/IncompatibleViewException.java b/src/main/java/com/google/devtools/build/lib/skyframe/IncompatibleViewException.java
new file mode 100644
index 0000000..9d6f550
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/IncompatibleViewException.java
@@ -0,0 +1,26 @@
+// 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;
+
+/**
+ * Thrown on {@link DiffAwareness#getDiff} to indicate that the given {@link DiffAwareness.View}s
+ * are incompatible with the {@link DiffAwareness} instance.
+ */
+public class IncompatibleViewException extends Exception {
+ public IncompatibleViewException(String msg) {
+ super(Preconditions.checkNotNull(msg));
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/InconsistentFilesystemException.java b/src/main/java/com/google/devtools/build/lib/skyframe/InconsistentFilesystemException.java
new file mode 100644
index 0000000..26cb02f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/InconsistentFilesystemException.java
@@ -0,0 +1,27 @@
+// 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;
+
+/**
+ * Used to indicate a filesystem inconsistency, e.g. file 'a/b' exists but directory 'a' doesn't
+ * exist. This generally means the result of the build is undefined but we shouldn't crash hard.
+ */
+public class InconsistentFilesystemException extends Exception {
+ public InconsistentFilesystemException(String inconsistencyMessage) {
+ super("Inconsistent filesystem operations. " + inconsistencyMessage + " The results of the "
+ + "build are not guaranteed to be correct. You should probably run 'blaze clean' and "
+ + "investigate the filesystem inconsistency (likely due to filesytem updates concurrent "
+ + "with the build)");
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/LocalDiffAwareness.java b/src/main/java/com/google/devtools/build/lib/skyframe/LocalDiffAwareness.java
new file mode 100644
index 0000000..861f89ac
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/LocalDiffAwareness.java
@@ -0,0 +1,329 @@
+// 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.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.HashBiMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.Constants;
+import com.google.devtools.build.lib.vfs.ModifiedFileSet;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+import java.io.IOException;
+import java.nio.file.ClosedWatchServiceException;
+import java.nio.file.FileSystems;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.StandardWatchEventKinds;
+import java.nio.file.WatchEvent;
+import java.nio.file.WatchEvent.Kind;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchService;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * File system watcher for local filesystems. It's able to provide a list of changed
+ * files between two consecutive calls. Uses the standard Java WatchService, which uses
+ * 'inotify' on Linux.
+ */
+public class LocalDiffAwareness implements DiffAwareness {
+
+ /** Factory for creating {@link LocalDiffAwareness} instances. */
+ public static class Factory implements DiffAwareness.Factory {
+ @Override
+ public DiffAwareness maybeCreate(com.google.devtools.build.lib.vfs.Path pathEntry) {
+ com.google.devtools.build.lib.vfs.Path resolvedPathEntry;
+ try {
+ resolvedPathEntry = pathEntry.resolveSymbolicLinks();
+ } catch (IOException e) {
+ return null;
+ }
+ PathFragment resolvedPathEntryFragment = resolvedPathEntry.asFragment();
+ // There's no good way to automatically detect network file systems. We rely on a blacklist
+ // for now (and maybe add a command-line option in the future?).
+ for (String prefix : Constants.WATCHFS_BLACKLIST) {
+ if (resolvedPathEntryFragment.startsWith(new PathFragment(prefix))) {
+ return null;
+ }
+ }
+
+ WatchService watchService;
+ try {
+ watchService = FileSystems.getDefault().newWatchService();
+ } catch (IOException e) {
+ return null;
+ }
+ return new LocalDiffAwareness(resolvedPathEntryFragment.toString(),
+ watchService);
+ }
+ }
+
+ private int numGetCurrentViewCalls = 0;
+
+ /**
+ * Bijection from WatchKey to the (absolute) Path being watched. WatchKeys don't have this
+ * functionality built-in so we do it ourselves.
+ */
+ private final HashBiMap<WatchKey, Path> watchKeyToDirBiMap = HashBiMap.create();
+
+ /** Root directory to watch. This is an absolute path. */
+ private final Path watchRootPath;
+
+ /** Every directory is registered under this watch service. */
+ private WatchService watchService;
+
+ private LocalDiffAwareness(String watchRoot, WatchService watchService) {
+ this.watchRootPath = FileSystems.getDefault().getPath(watchRoot);
+ this.watchService = watchService;
+ }
+
+ /**
+ * The WatchService is inherently sequential and side-effectful, so we enforce this by only
+ * supporting {@link #getDiff} calls that happen to be sequential.
+ */
+ private static class SequentialView implements DiffAwareness.View {
+ private final LocalDiffAwareness owner;
+ private final int position;
+ private final Set<Path> modifiedAbsolutePaths;
+
+ public SequentialView(LocalDiffAwareness owner, int position, Set<Path> modifiedAbsolutePaths) {
+ this.owner = owner;
+ this.position = position;
+ this.modifiedAbsolutePaths = modifiedAbsolutePaths;
+ }
+
+ public static boolean areInSequence(SequentialView oldView, SequentialView newView) {
+ return oldView.owner == newView.owner && (oldView.position + 1) == newView.position;
+ }
+ }
+
+ @Override
+ public SequentialView getCurrentView() throws BrokenDiffAwarenessException {
+ Set<Path> modifiedAbsolutePaths;
+ if (numGetCurrentViewCalls++ == 0) {
+ try {
+ registerSubDirectoriesAndReturnContents(watchRootPath);
+ } catch (IOException e) {
+ close();
+ throw new BrokenDiffAwarenessException(
+ "Error encountered with local file system watcher " + e);
+ }
+ modifiedAbsolutePaths = ImmutableSet.of();
+ } else {
+ try {
+ modifiedAbsolutePaths = collectChanges();
+ } catch (BrokenDiffAwarenessException e) {
+ close();
+ throw e;
+ } catch (IOException e) {
+ close();
+ throw new BrokenDiffAwarenessException(
+ "Error encountered with local file system watcher " + e);
+ } catch (ClosedWatchServiceException e) {
+ throw new BrokenDiffAwarenessException(
+ "Internal error with the local file system watcher " + e);
+ }
+ }
+ return new SequentialView(this, numGetCurrentViewCalls, modifiedAbsolutePaths);
+ }
+
+ @Override
+ public ModifiedFileSet getDiff(View oldView, View newView)
+ throws IncompatibleViewException, BrokenDiffAwarenessException {
+ SequentialView oldSequentialView;
+ SequentialView newSequentialView;
+ try {
+ oldSequentialView = (SequentialView) oldView;
+ newSequentialView = (SequentialView) newView;
+ } catch (ClassCastException e) {
+ throw new IncompatibleViewException("Given views are not from LocalDiffAwareness");
+ }
+ if (!SequentialView.areInSequence(oldSequentialView, newSequentialView)) {
+ return ModifiedFileSet.EVERYTHING_MODIFIED;
+ }
+ return ModifiedFileSet.builder()
+ .modifyAll(Iterables.transform(newSequentialView.modifiedAbsolutePaths,
+ nioAbsolutePathToPathFragment))
+ .build();
+ }
+
+ @Override
+ public void close() {
+ try {
+ watchService.close();
+ } catch (IOException ignored) {
+ // Nothing we can do here.
+ }
+ }
+
+ /** Converts java.nio.file.Path objects to vfs.PathFragment. */
+ private final Function<Path, PathFragment> nioAbsolutePathToPathFragment =
+ new Function<Path, PathFragment>() {
+ @Override
+ public PathFragment apply(Path input) {
+ Preconditions.checkArgument(input.startsWith(watchRootPath), "%s %s", input,
+ watchRootPath);
+ return new PathFragment(watchRootPath.relativize(input).toString());
+ }
+ };
+
+ /** Returns the changed files caught by the watch service. */
+ private Set<Path> collectChanges() throws BrokenDiffAwarenessException, IOException {
+ Set<Path> createdFilesAndDirectories = new HashSet<Path>();
+ Set<Path> deletedOrModifiedFilesAndDirectories = new HashSet<Path>();
+ Set<Path> deletedTrackedDirectories = new HashSet<Path>();
+
+ WatchKey watchKey;
+ while ((watchKey = watchService.poll()) != null) {
+ Path dir = watchKeyToDirBiMap.get(watchKey);
+ Preconditions.checkArgument(dir != null);
+
+ // We replay all the events for this watched directory in chronological order and
+ // construct the diff of this directory since the last #collectChanges call.
+ for (WatchEvent<?> event : watchKey.pollEvents()) {
+ Kind<?> kind = event.kind();
+ if (kind == StandardWatchEventKinds.OVERFLOW) {
+ // TODO(bazel-team): find out when an overflow might happen, and maybe handle it more
+ // gently.
+ throw new BrokenDiffAwarenessException("Overflow when watching local filesystem for "
+ + "changes");
+ }
+ if (event.context() == null) {
+ // The WatchService documentation mentions that WatchEvent#context may return null, but
+ // doesn't explain how/why it would do so. Looking at the implementation, it only
+ // happens on an overflow event. But we make no assumptions about that implementation
+ // detail here.
+ throw new BrokenDiffAwarenessException("Insufficient information from local file system "
+ + "watcher");
+ }
+ // For the events we've registered, the context given is a relative path.
+ Path relativePath = (Path) event.context();
+ Path path = dir.resolve(relativePath);
+ Preconditions.checkState(path.isAbsolute(), path);
+ if (kind == StandardWatchEventKinds.ENTRY_CREATE) {
+ createdFilesAndDirectories.add(path);
+ deletedOrModifiedFilesAndDirectories.remove(path);
+ } else if (kind == StandardWatchEventKinds.ENTRY_DELETE) {
+ createdFilesAndDirectories.remove(path);
+ deletedOrModifiedFilesAndDirectories.add(path);
+ if (watchKeyToDirBiMap.containsValue(path)) {
+ // If the deleted directory has children, then there will also be events for the
+ // WatchKey of the directory itself. WatchService#poll doesn't specify the order in
+ // which WatchKeys are returned, so the key for the directory itself may be processed
+ // *after* the current key (the parent of the deleted directory), and so we don't want
+ // to remove the deleted directory from our bimap just yet.
+ //
+ // For example, suppose we have the file '/root/a/foo.txt' and are watching the
+ // directories '/root' and '/root/a'. If the directory '/root/a' gets deleted then the
+ // following is a valid sequence of events by key.
+ //
+ // WatchKey '/root/'
+ // WatchEvent EVENT_MODIFY 'a'
+ // WatchEvent EVENT_DELETE 'a'
+ // WatchKey '/root/a'
+ // WatchEvent EVENT_DELETE 'foo.txt'
+ deletedTrackedDirectories.add(path);
+ }
+ } else if (kind == StandardWatchEventKinds.ENTRY_MODIFY) {
+ // If a file was created and then modified, then the net diff is that it was
+ // created.
+ if (!createdFilesAndDirectories.contains(path)) {
+ deletedOrModifiedFilesAndDirectories.add(path);
+ }
+ }
+ }
+
+ if (!watchKey.reset()) {
+ // Watcher got deleted, directory no longer valid.
+ watchKeyToDirBiMap.remove(watchKey);
+ }
+ }
+
+ for (Path path : deletedTrackedDirectories) {
+ WatchKey staleKey = watchKeyToDirBiMap.inverse().get(path);
+ watchKeyToDirBiMap.remove(staleKey);
+ }
+ if (watchKeyToDirBiMap.isEmpty()) {
+ // No more directories to watch, something happened the root directory being watched.
+ throw new IOException("Root directory " + watchRootPath + " became inaccessible.");
+ }
+
+ Set<Path> changedPaths = new HashSet<Path>();
+ for (Path path : createdFilesAndDirectories) {
+ if (Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) {
+ // This is a new directory, so changes to it since its creation have not been watched.
+ // We manually traverse the directory tree to register all the new subdirectories and find
+ // all the new subdirectories and files.
+ changedPaths.addAll(registerSubDirectoriesAndReturnContents(path));
+ } else {
+ changedPaths.add(path);
+ }
+ }
+ changedPaths.addAll(deletedOrModifiedFilesAndDirectories);
+ return changedPaths;
+ }
+
+ /**
+ * Traverses directory tree to register subdirectories. Returns all paths traversed (as absolute
+ * paths).
+ */
+ private Set<Path> registerSubDirectoriesAndReturnContents(Path rootDir) throws IOException {
+ Set<Path> visitedAbsolutePaths = new HashSet<Path>();
+ // Note that this does not follow symlinks.
+ Files.walkFileTree(rootDir, new WatcherFileVisitor(visitedAbsolutePaths));
+ return visitedAbsolutePaths;
+ }
+
+ /** File visitor used by Files.walkFileTree() upon traversing subdirectories. */
+ private class WatcherFileVisitor extends SimpleFileVisitor<Path> {
+
+ private final Set<Path> visitedAbsolutePaths;
+
+ private WatcherFileVisitor(Set<Path> visitedPaths) {
+ this.visitedAbsolutePaths = visitedPaths;
+ }
+
+ @Override
+ public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) {
+ Preconditions.checkState(path.isAbsolute(), path);
+ visitedAbsolutePaths.add(path);
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult preVisitDirectory(Path path, BasicFileAttributes attrs)
+ throws IOException {
+ // It's important that we register the directory before we visit its children. This way we
+ // are guaranteed to see new files/directories either on this #getDiff or the next one.
+ // Otherwise, e.g., an intra-build creation of a child directory will be forever missed if it
+ // happens before the directory is listed as part of the visitation.
+ WatchKey key = path.register(watchService,
+ StandardWatchEventKinds.ENTRY_CREATE,
+ StandardWatchEventKinds.ENTRY_MODIFY,
+ StandardWatchEventKinds.ENTRY_DELETE);
+ Preconditions.checkState(path.isAbsolute(), path);
+ visitedAbsolutePaths.add(path);
+ watchKeyToDirBiMap.put(key, path);
+ return FileVisitResult.CONTINUE;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/MutableSupplier.java b/src/main/java/com/google/devtools/build/lib/skyframe/MutableSupplier.java
new file mode 100644
index 0000000..86de11d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/MutableSupplier.java
@@ -0,0 +1,46 @@
+// 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.Objects;
+import com.google.common.base.Supplier;
+
+/**
+ * Supplier whose value can be changed by clients who have a reference to it as a MutableSupplier.
+ * Unlike an {@code AtomicReference}, clients who are passed a MutableSupplier as a Supplier cannot
+ * change its value without a reckless cast.
+ */
+public class MutableSupplier<T> implements Supplier<T> {
+ private T val;
+
+ @Override
+ public T get() {
+ return val;
+ }
+
+ /**
+ * Sets the value of the object supplied. Do not cast a Supplier to a MutableSupplier in order to
+ * call this method!
+ */
+ public void set(T newVal) {
+ val = newVal;
+ }
+
+ @SuppressWarnings("deprecation") // MoreObjects.toStringHelper() is not in Guava
+ @Override
+ public String toString() {
+ return Objects.toStringHelper(getClass())
+ .add("val", val).toString();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/PackageFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/PackageFunction.java
new file mode 100644
index 0000000..2404b99
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/PackageFunction.java
@@ -0,0 +1,809 @@
+// 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.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.common.eventbus.EventBus;
+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.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.PackageIdentifier.RepositoryName;
+import com.google.devtools.build.lib.packages.PackageLoadedEvent;
+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.Clock;
+import com.google.devtools.build.lib.util.JavaClock;
+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.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+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 ConcurrentMap<PackageIdentifier, Package.LegacyBuilder> packageFunctionCache;
+ private final AtomicBoolean showLoadingProgress;
+ private final AtomicReference<EventBus> eventBus;
+ 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,
+ ConcurrentMap<PackageIdentifier, Package.LegacyBuilder> packageFunctionCache,
+ AtomicReference<EventBus> eventBus, AtomicInteger numPackagesLoaded) {
+ this.reporter = reporter;
+
+ this.packageFactory = packageFactory;
+ this.packageLocator = pkgLocator;
+ this.showLoadingProgress = showLoadingProgress;
+ this.packageFunctionCache = packageFunctionCache;
+ this.eventBus = eventBus;
+ this.numPackagesLoaded = numPackagesLoaded;
+ }
+
+ private static void maybeThrowFilesystemInconsistency(String packageName,
+ Exception skyframeException, boolean packageWasInError)
+ throws InternalInconsistentFilesystemException {
+ if (!packageWasInError) {
+ throw new InternalInconsistentFilesystemException(packageName, "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(String packageName,
+ 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, FileSymlinkCycleException>> 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(packageName, e, packageWasInError);
+ } catch (InconsistentFilesystemException e) {
+ throw new InternalInconsistentFilesystemException(packageName, e);
+ } catch (FileSymlinkCycleException e) {
+ // Legacy doesn't detect symlink cycles.
+ packageShouldBeInError = true;
+ }
+ }
+ return Pair.of(builder.build(), packageShouldBeInError);
+ }
+
+ private static boolean markFileDepsAndPropagateInconsistentFilesystemExceptions(
+ String packageName, 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, FileSymlinkCycleException,
+ InconsistentFilesystemException>> entry : env.getValuesOrThrow(depKeys, IOException.class,
+ FileSymlinkCycleException.class, InconsistentFilesystemException.class).entrySet()) {
+ try {
+ entry.getValue().get();
+ } catch (IOException e) {
+ maybeThrowFilesystemInconsistency(packageName, e, packageWasInError);
+ } catch (FileSymlinkCycleException e) {
+ // Legacy doesn't detect symlink cycles.
+ packageShouldBeInError = true;
+ } catch (InconsistentFilesystemException e) {
+ throw new InternalInconsistentFilesystemException(packageName, e);
+ }
+ }
+ return packageShouldBeInError;
+ }
+
+ private static boolean markGlobDepsAndPropagateInconsistentFilesystemExceptions(
+ String packageName, 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,
+ FileSymlinkCycleException, InconsistentFilesystemException>> entry :
+ env.getValuesOrThrow(depKeys, IOException.class, BuildFileNotFoundException.class,
+ FileSymlinkCycleException.class, InconsistentFilesystemException.class).entrySet()) {
+ try {
+ entry.getValue().get();
+ } catch (IOException | BuildFileNotFoundException e) {
+ maybeThrowFilesystemInconsistency(packageName, e, packageWasInError);
+ } catch (FileSymlinkCycleException e) {
+ // Legacy doesn't detect symlink cycles.
+ packageShouldBeInError = true;
+ } catch (InconsistentFilesystemException e) {
+ throw new InternalInconsistentFilesystemException(packageName, 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 packageShouldBeInError = pkg.containsErrors();
+
+ // 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.getPackageFragment()));
+ }
+ Pair<? extends Map<PathFragment, PackageLookupValue>, Boolean> subincludePackageLookupResult =
+ getPackageLookupDepsAndPropagateInconsistentFilesystemExceptions(pkg.getName(),
+ subincludePackageLookupDepKeys, env, pkg.containsErrors());
+ 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.getName(), 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.getName(), 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.getName(), subincludeFileDepKeys, env, pkg.containsErrors());
+ // Another concern is a subpackage cutting off the subinclude label, but this is already
+ // handled by the legacy package loading code which calls into our SkyframePackageLocator.
+
+ // 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);
+ } 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.getName(), globDepKeys, env, pkg.containsErrors());
+ 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 = WorkspaceFileValue.key(workspacePath);
+ WorkspaceFileValue workspace = null;
+ try {
+ workspace = (WorkspaceFileValue) 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("external",
+ "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(packageName, 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(packageName,
+ packageLookupValue.getErrorMsg()), Transience.PERSISTENT);
+ case INVALID_PACKAGE_NAME:
+ throw new PackageFunctionException(new InvalidPackageNameException(packageName,
+ packageLookupValue.getErrorMsg()), Transience.PERSISTENT);
+ default:
+ // We should never get here.
+ Preconditions.checkState(false);
+ }
+ }
+
+ if (packageName.equals(EXTERNAL_PACKAGE_NAME)) {
+ return getExternalPackage(env, packageLookupValue.getRoot());
+ }
+
+ RootedPath buildFileRootedPath = RootedPath.toRootedPath(packageLookupValue.getRoot(),
+ packageNameFragment.getChild("BUILD"));
+ 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(packageName, e.getMessage()),
+ Transience.PERSISTENT);
+ }
+ if (astLookupValue == null) {
+ return null;
+ }
+ List<Statement> preludeStatements = astLookupValue == ASTFileLookupValue.NO_FILE
+ ? 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.containsKey(packageId)) {
+ // 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(
+ packageName, e.getMessage()), Transience.TRANSIENT);
+ }
+ SkylarkImportResult importResult = fetchImportsFromBuildFile(
+ buildFilePath, packageId.getRepository(), preludeStatements, inputSource, packageName, env);
+ if (importResult == null) {
+ return null;
+ }
+
+ Package.LegacyBuilder legacyPkgBuilder = loadPackage(inputSource, replacementContents,
+ packageId, buildFilePath, defaultVisibility, preludeStatements, importResult);
+ legacyPkgBuilder.buildPartial();
+ try {
+ handleLabelsCrossingSubpackagesAndPropagateInconsistentFilesystemExceptions(
+ packageLookupValue.getRoot(), packageId, legacyPkgBuilder, env);
+ } catch (InternalInconsistentFilesystemException e) {
+ packageFunctionCache.remove(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 = pkg.containsErrors();
+ try {
+ packageShouldBeConsideredInError =
+ markDependenciesAndPropagateInconsistentFilesystemExceptions(pkg, env,
+ globPatterns, subincludes);
+ } catch (InternalInconsistentFilesystemException e) {
+ packageFunctionCache.remove(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.remove(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, RepositoryName repo,
+ List<Statement> preludeStatements, ParserInputSource inputSource,
+ String packageName, 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.<PathFragment, SkylarkEnvironment>of(),
+ ImmutableList.<Label>of());
+ }
+
+ ImmutableCollection<PathFragment> imports = buildFileAST.getImports();
+ Map<PathFragment, SkylarkEnvironment> importMap = new HashMap<>();
+ ImmutableList.Builder<SkylarkFileDependency> fileDependencies = ImmutableList.builder();
+ try {
+ for (PathFragment importFile : imports) {
+ SkyKey importsLookupKey = SkylarkImportLookupValue.key(repo, importFile);
+ SkylarkImportLookupValue importLookupValue = (SkylarkImportLookupValue)
+ env.getValueOrThrow(importsLookupKey, SkylarkImportFailedException.class,
+ InconsistentFilesystemException.class, ASTLookupInputException.class,
+ BuildFileNotFoundException.class);
+ if (importLookupValue != null) {
+ importMap.put(importFile, importLookupValue.getImportedEnvironment());
+ fileDependencies.add(importLookupValue.getDependency());
+ }
+ }
+ } catch (SkylarkImportFailedException e) {
+ env.getListener().handle(Event.error(Location.fromFile(buildFilePath), e.getMessage()));
+ throw new PackageFunctionException(new BuildFileContainsErrorsException(packageName,
+ e.getMessage()), Transience.PERSISTENT);
+ } catch (InconsistentFilesystemException e) {
+ throw new PackageFunctionException(new InternalInconsistentFilesystemException(packageName,
+ e), Transience.PERSISTENT);
+ } catch (ASTLookupInputException e) {
+ // The load syntax is bad in the BUILD file so BuildFileContainsErrorsException is OK.
+ throw new PackageFunctionException(new BuildFileContainsErrorsException(packageName,
+ 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.contains(dep.getLabel())) {
+ 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,
+ FileSymlinkCycleException>> containingPkgLookupValues = env.getValuesOrThrow(
+ containingPkgLookupKeys, BuildFileNotFoundException.class,
+ InconsistentFilesystemException.class, FileSymlinkCycleException.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.getPackageFragment().getPathString(), 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.getPackageFragment().getPathString(), containingPkgLookupValues.get(key), env);
+ if (maybeAddEventAboutLabelCrossingSubpackage(pkgBuilder, pkgRoot, subincludeLabel,
+ /*location=*/null, containingPackageLookupValue)) {
+ pkgBuilder.setContainsErrors();
+ }
+ }
+ }
+
+ @Nullable
+ private static ContainingPackageLookupValue
+ getContainingPkgLookupValueAndPropagateInconsistentFilesystemExceptions(String packageName,
+ ValueOrException3<BuildFileNotFoundException, InconsistentFilesystemException,
+ FileSymlinkCycleException> containingPkgLookupValueOrException, Environment env)
+ throws InternalInconsistentFilesystemException {
+ try {
+ return (ContainingPackageLookupValue) containingPkgLookupValueOrException.get();
+ } catch (BuildFileNotFoundException | FileSymlinkCycleException e) {
+ env.getListener().handle(Event.error(null, e.getMessage()));
+ return null;
+ } catch (InconsistentFilesystemException e) {
+ throw new InternalInconsistentFilesystemException(packageName, 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.getPackageFragment().startsWith(label.getPackageFragment())) {
+ // 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.
+ */
+ private Package.LegacyBuilder loadPackage(ParserInputSource inputSource,
+ @Nullable String replacementContents,
+ PackageIdentifier packageId, Path buildFilePath, RuleVisibility defaultVisibility,
+ List<Statement> preludeStatements, SkylarkImportResult importResult)
+ throws InterruptedException {
+ ParserInputSource replacementSource = replacementContents == null ? null
+ : ParserInputSource.create(replacementContents, buildFilePath);
+ Package.LegacyBuilder pkgBuilder = packageFunctionCache.get(packageId);
+ if (pkgBuilder == null) {
+ Clock clock = new JavaClock();
+ long startTime = clock.nanoTime();
+ profiler.startTask(ProfilerTask.CREATE_PACKAGE, packageId.toString());
+ try {
+ Globber globber = packageFactory.createLegacyGlobber(buildFilePath.getParentDirectory(),
+ packageId, packageLocator);
+ StoredEventHandler localReporter = new StoredEventHandler();
+ Preprocessor.Result preprocessingResult = replacementSource == null
+ ? packageFactory.preprocess(packageId, buildFilePath, inputSource, globber,
+ localReporter)
+ : Preprocessor.Result.noPreprocessing(replacementSource);
+ pkgBuilder = packageFactory.createPackageFromPreprocessingResult(packageId, buildFilePath,
+ preprocessingResult, localReporter.getEvents(), preludeStatements,
+ importResult.importMap, importResult.fileDependencies, packageLocator,
+ defaultVisibility, globber);
+ if (eventBus.get() != null) {
+ eventBus.get().post(new PackageLoadedEvent(packageId.toString(),
+ (clock.nanoTime() - startTime) / (1000 * 1000),
+ // It's impossible to tell if the package was loaded before, so we always pass false.
+ /*reloading=*/false,
+ // This isn't completely correct since we may encounter errors later (e.g. filesystem
+ // inconsistencies)
+ !pkgBuilder.containsErrors()));
+ }
+ 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(String packageName,
+ InconsistentFilesystemException e) {
+ super(packageName, 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(String packageName,
+ String inconsistencyMessage) {
+ this(packageName, new InconsistentFilesystemException(inconsistencyMessage));
+ this.isTransient = true;
+ }
+
+ public boolean isTransient() {
+ return isTransient;
+ }
+ }
+
+ private static class BadWorkspaceFileException extends NoSuchPackageException {
+ private BadWorkspaceFileException(String message) {
+ super("external", "Error encountered while dealing with the WORKSPACE file: " + message);
+ }
+ }
+
+ private static class BadPreludeFileException extends NoSuchPackageException {
+ private BadPreludeFileException(String packageName, String message) {
+ super(packageName, "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, SkylarkEnvironment> importMap;
+ private final ImmutableList<Label> fileDependencies;
+ private SkylarkImportResult(Map<PathFragment, SkylarkEnvironment> importMap,
+ ImmutableList<Label> fileDependencies) {
+ this.importMap = importMap;
+ this.fileDependencies = fileDependencies;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/PackageLookupFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/PackageLookupFunction.java
new file mode 100644
index 0000000..ae4ee55
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/PackageLookupFunction.java
@@ -0,0 +1,180 @@
+// 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.collect.ImmutableSet;
+import com.google.devtools.build.lib.cmdline.LabelValidator;
+import com.google.devtools.build.lib.packages.BuildFileContainsErrorsException;
+import com.google.devtools.build.lib.packages.BuildFileNotFoundException;
+import com.google.devtools.build.lib.packages.NoSuchPackageException;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
+import com.google.devtools.build.lib.syntax.EvalException;
+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 java.io.IOException;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.annotation.Nullable;
+
+/**
+ * SkyFunction for {@link PackageLookupValue}s.
+ */
+class PackageLookupFunction implements SkyFunction {
+
+ private final AtomicReference<ImmutableSet<String>> deletedPackages;
+
+ PackageLookupFunction(AtomicReference<ImmutableSet<String>> deletedPackages) {
+ this.deletedPackages = deletedPackages;
+ }
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) throws PackageLookupFunctionException {
+ PathPackageLocator pkgLocator = PrecomputedValue.PATH_PACKAGE_LOCATOR.get(env);
+ PackageIdentifier packageKey = (PackageIdentifier) skyKey.argument();
+ if (!packageKey.getRepository().isDefault()) {
+ return computeExternalPackageLookupValue(skyKey, env);
+ }
+ PathFragment pkg = packageKey.getPackageFragment();
+
+ // This represents a package lookup at the package root.
+ if (pkg.equals(PathFragment.EMPTY_FRAGMENT)) {
+ return PackageLookupValue.invalidPackageName("The empty package name is invalid");
+ }
+
+ String pkgName = pkg.getPathString();
+ String packageNameErrorMsg = LabelValidator.validatePackageName(pkgName);
+ if (packageNameErrorMsg != null) {
+ return PackageLookupValue.invalidPackageName("Invalid package name '" + pkgName + "': "
+ + packageNameErrorMsg);
+ }
+
+ if (deletedPackages.get().contains(pkg.getPathString())) {
+ return PackageLookupValue.deletedPackage();
+ }
+
+ // TODO(bazel-team): The following is O(n^2) on the number of elements on the package path due
+ // to having restart the SkyFunction after every new dependency. However, if we try to batch
+ // the missing value keys, more dependencies than necessary will be declared. This wart can be
+ // fixed once we have nicer continuation support [skyframe-loading]
+ for (Path packagePathEntry : pkgLocator.getPathEntries()) {
+ PackageLookupValue value = getPackageLookupValue(env, packagePathEntry, pkg);
+ if (value == null || value.packageExists()) {
+ return value;
+ }
+ }
+ return PackageLookupValue.noBuildFile();
+ }
+
+ @Nullable
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+
+ private PackageLookupValue getPackageLookupValue(Environment env, Path packagePathEntry,
+ PathFragment pkgFragment) throws PackageLookupFunctionException {
+ PathFragment buildFileFragment;
+ if (pkgFragment.getPathString().equals(PackageFunction.EXTERNAL_PACKAGE_NAME)) {
+ buildFileFragment = new PathFragment("WORKSPACE");
+ } else {
+ buildFileFragment = pkgFragment.getChild("BUILD");
+ }
+ RootedPath buildFileRootedPath = RootedPath.toRootedPath(packagePathEntry,
+ buildFileFragment);
+ String basename = buildFileRootedPath.asPath().getBaseName();
+ SkyKey fileSkyKey = FileValue.key(buildFileRootedPath);
+ FileValue fileValue = null;
+ try {
+ fileValue = (FileValue) env.getValueOrThrow(fileSkyKey, IOException.class,
+ FileSymlinkCycleException.class, InconsistentFilesystemException.class);
+ } catch (IOException e) {
+ String pkgName = pkgFragment.getPathString();
+ // TODO(bazel-team): throw an IOException here and let PackageFunction wrap that into a
+ // BuildFileNotFoundException.
+ throw new PackageLookupFunctionException(new BuildFileNotFoundException(pkgName,
+ "IO errors while looking for " + basename + " file reading "
+ + buildFileRootedPath.asPath() + ": " + e.getMessage(), e),
+ Transience.PERSISTENT);
+ } catch (FileSymlinkCycleException e) {
+ String pkgName = buildFileRootedPath.asPath().getPathString();
+ throw new PackageLookupFunctionException(new BuildFileNotFoundException(pkgName,
+ "Symlink cycle detected while trying to find " + basename + " file "
+ + buildFileRootedPath.asPath()),
+ Transience.PERSISTENT);
+ } catch (InconsistentFilesystemException e) {
+ // This error is not transient from the perspective of the PackageLookupFunction.
+ throw new PackageLookupFunctionException(e, Transience.PERSISTENT);
+ }
+ if (fileValue == null) {
+ return null;
+ }
+ if (fileValue.isFile()) {
+ return PackageLookupValue.success(buildFileRootedPath.getRoot());
+ }
+ return PackageLookupValue.noBuildFile();
+ }
+
+ /**
+ * Gets a PackageLookupValue from a different Bazel repository.
+ *
+ * To do this, it looks up the "external" package and finds a path mapping for the repository
+ * name.
+ */
+ private PackageLookupValue computeExternalPackageLookupValue(
+ SkyKey skyKey, Environment env) throws PackageLookupFunctionException {
+ PackageIdentifier id = (PackageIdentifier) skyKey.argument();
+ SkyKey repositoryKey = RepositoryValue.key(id.getRepository());
+ RepositoryValue repositoryValue = null;
+ try {
+ repositoryValue = (RepositoryValue) env.getValueOrThrow(
+ repositoryKey, NoSuchPackageException.class, IOException.class, EvalException.class);
+ if (repositoryValue == null) {
+ return null;
+ }
+ } catch (NoSuchPackageException e) {
+ throw new PackageLookupFunctionException(e, Transience.PERSISTENT);
+ } catch (IOException e) {
+ throw new PackageLookupFunctionException(new BuildFileContainsErrorsException(
+ PackageFunction.EXTERNAL_PACKAGE_NAME, e.getMessage()), Transience.PERSISTENT);
+ } catch (EvalException e) {
+ throw new PackageLookupFunctionException(new BuildFileContainsErrorsException(
+ PackageFunction.EXTERNAL_PACKAGE_NAME, e.getMessage()), Transience.PERSISTENT);
+ }
+
+ return getPackageLookupValue(env, repositoryValue.getPath(), id.getPackageFragment());
+ }
+
+ /**
+ * Used to declare all the exception types that can be wrapped in the exception thrown by
+ * {@link PackageLookupFunction#compute}.
+ */
+ private static final class PackageLookupFunctionException extends SkyFunctionException {
+ public PackageLookupFunctionException(NoSuchPackageException e, Transience transience) {
+ super(e, transience);
+ }
+
+ public PackageLookupFunctionException(InconsistentFilesystemException e,
+ Transience transience) {
+ super(e, transience);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/PackageLookupValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/PackageLookupValue.java
new file mode 100644
index 0000000..c877d38
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/PackageLookupValue.java
@@ -0,0 +1,249 @@
+// 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.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+/**
+ * A value that represents a package lookup result.
+ *
+ * <p>Package lookups will always produce a value. On success, the {@code #getRoot} returns the
+ * package path root under which the package resides and the package's BUILD file is guaranteed to
+ * exist; on failure, {@code #getErrorReason} and {@code #getErrorMsg} describe why the package
+ * doesn't exist.
+ *
+ * <p>Implementation detail: we use inheritance here to optimize for memory usage.
+ */
+abstract class PackageLookupValue implements SkyValue {
+
+ enum ErrorReason {
+ // There is no BUILD file.
+ NO_BUILD_FILE,
+
+ // The package name is invalid.
+ INVALID_PACKAGE_NAME,
+
+ // The package is considered deleted because of --deleted_packages.
+ DELETED_PACKAGE,
+
+ // The //external package could not be loaded, either because the WORKSPACE file could not be
+ // parsed or the packages it references cannot be loaded.
+ NO_EXTERNAL_PACKAGE
+ }
+
+ protected PackageLookupValue() {
+ }
+
+ public static PackageLookupValue success(Path root) {
+ return new SuccessfulPackageLookupValue(root);
+ }
+
+ public static PackageLookupValue noBuildFile() {
+ return NoBuildFilePackageLookupValue.INSTANCE;
+ }
+
+ public static PackageLookupValue noExternalPackage() {
+ return NoExternalPackageLookupValue.INSTANCE;
+ }
+
+ public static PackageLookupValue invalidPackageName(String errorMsg) {
+ return new InvalidNamePackageLookupValue(errorMsg);
+ }
+
+ public static PackageLookupValue deletedPackage() {
+ return DeletedPackageLookupValue.INSTANCE;
+ }
+
+ /**
+ * For a successful package lookup, returns the root (package path entry) that the package
+ * resides in.
+ */
+ public abstract Path getRoot();
+
+ /**
+ * Returns whether the package lookup was successful.
+ */
+ public abstract boolean packageExists();
+
+ /**
+ * For an unsuccessful package lookup, gets the reason why {@link #packageExists} returns
+ * {@code false}.
+ */
+ abstract ErrorReason getErrorReason();
+
+ /**
+ * For an unsuccessful package lookup, gets a detailed error message for {@link #getErrorReason}
+ * that is suitable for reporting to a user.
+ */
+ abstract String getErrorMsg();
+
+ static SkyKey key(PathFragment directory) {
+ Preconditions.checkArgument(!directory.isAbsolute(), directory);
+ return key(PackageIdentifier.createInDefaultRepo(directory));
+ }
+
+ static SkyKey key(PackageIdentifier pkgIdentifier) {
+ return new SkyKey(SkyFunctions.PACKAGE_LOOKUP, pkgIdentifier);
+ }
+
+ private static class SuccessfulPackageLookupValue extends PackageLookupValue {
+
+ private final Path root;
+
+ private SuccessfulPackageLookupValue(Path root) {
+ this.root = root;
+ }
+
+ @Override
+ public boolean packageExists() {
+ return true;
+ }
+
+ @Override
+ public Path getRoot() {
+ return root;
+ }
+
+ @Override
+ ErrorReason getErrorReason() {
+ throw new IllegalStateException();
+ }
+
+ @Override
+ String getErrorMsg() {
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof SuccessfulPackageLookupValue)) {
+ return false;
+ }
+ SuccessfulPackageLookupValue other = (SuccessfulPackageLookupValue) obj;
+ return root.equals(other.root);
+ }
+
+ @Override
+ public int hashCode() {
+ return root.hashCode();
+ }
+ }
+
+ private abstract static class UnsuccessfulPackageLookupValue extends PackageLookupValue {
+
+ @Override
+ public boolean packageExists() {
+ return false;
+ }
+
+ @Override
+ public Path getRoot() {
+ throw new IllegalStateException();
+ }
+ }
+
+ private static class NoBuildFilePackageLookupValue extends UnsuccessfulPackageLookupValue {
+
+ public static final NoBuildFilePackageLookupValue INSTANCE =
+ new NoBuildFilePackageLookupValue();
+
+ private NoBuildFilePackageLookupValue() {
+ }
+
+ @Override
+ ErrorReason getErrorReason() {
+ return ErrorReason.NO_BUILD_FILE;
+ }
+
+ @Override
+ String getErrorMsg() {
+ return "BUILD file not found on package path";
+ }
+ }
+
+ private static class NoExternalPackageLookupValue extends UnsuccessfulPackageLookupValue {
+
+ public static final NoExternalPackageLookupValue INSTANCE =
+ new NoExternalPackageLookupValue();
+
+ private NoExternalPackageLookupValue() {
+ }
+
+ @Override
+ ErrorReason getErrorReason() {
+ return ErrorReason.NO_EXTERNAL_PACKAGE;
+ }
+
+ @Override
+ String getErrorMsg() {
+ return "Error loading the //external package";
+ }
+ }
+
+ private static class InvalidNamePackageLookupValue extends UnsuccessfulPackageLookupValue {
+
+ private final String errorMsg;
+
+ private InvalidNamePackageLookupValue(String errorMsg) {
+ this.errorMsg = errorMsg;
+ }
+
+ @Override
+ ErrorReason getErrorReason() {
+ return ErrorReason.INVALID_PACKAGE_NAME;
+ }
+
+ @Override
+ String getErrorMsg() {
+ return errorMsg;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof InvalidNamePackageLookupValue)) {
+ return false;
+ }
+ InvalidNamePackageLookupValue other = (InvalidNamePackageLookupValue) obj;
+ return errorMsg.equals(other.errorMsg);
+ }
+
+ @Override
+ public int hashCode() {
+ return errorMsg.hashCode();
+ }
+ }
+
+ private static class DeletedPackageLookupValue extends UnsuccessfulPackageLookupValue {
+
+ public static final DeletedPackageLookupValue INSTANCE = new DeletedPackageLookupValue();
+
+ private DeletedPackageLookupValue() {
+ }
+
+ @Override
+ ErrorReason getErrorReason() {
+ return ErrorReason.DELETED_PACKAGE;
+ }
+
+ @Override
+ String getErrorMsg() {
+ return "Package is considered deleted due to --deleted_packages";
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/PackageValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/PackageValue.java
new file mode 100644
index 0000000..65fd2af
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/PackageValue.java
@@ -0,0 +1,55 @@
+// 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.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.packages.Package;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+/**
+ * A Skyframe value representing a package.
+ */
+@Immutable
+@ThreadSafe
+public class PackageValue implements SkyValue {
+
+ private final Package pkg;
+
+ PackageValue(Package pkg) {
+ this.pkg = Preconditions.checkNotNull(pkg);
+ }
+
+ public Package getPackage() {
+ return pkg;
+ }
+
+ @Override
+ public String toString() {
+ return "<PackageValue name=" + pkg.getName() + ">";
+ }
+
+ @ThreadSafe
+ public static SkyKey key(PathFragment pkgName) {
+ return key(PackageIdentifier.createInDefaultRepo(pkgName));
+ }
+
+ public static SkyKey key(PackageIdentifier pkgIdentifier) {
+ return new SkyKey(SkyFunctions.PACKAGE, pkgIdentifier);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/PerBuildSyscallCache.java b/src/main/java/com/google/devtools/build/lib/skyframe/PerBuildSyscallCache.java
new file mode 100644
index 0000000..5116a6f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/PerBuildSyscallCache.java
@@ -0,0 +1,131 @@
+// 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.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.devtools.build.lib.util.Pair;
+import com.google.devtools.build.lib.vfs.Dirent;
+import com.google.devtools.build.lib.vfs.FileStatus;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.Symlinks;
+import com.google.devtools.build.lib.vfs.UnixGlob;
+
+import java.io.IOException;
+import java.util.Collection;
+
+/**
+ * A per-build cache of filesystem operations for Skyframe invocations of legacy package loading.
+ */
+class PerBuildSyscallCache implements UnixGlob.FilesystemCalls {
+
+ private final LoadingCache<Pair<Path, Symlinks>, FileStatus> statCache =
+ newStatMap();
+ private final LoadingCache<Pair<Path, Symlinks>, Pair<Collection<Dirent>, IOException>>
+ readdirCache = newReaddirMap();
+
+ private static final FileStatus NO_STATUS = new FakeFileStatus();
+
+ @Override
+ public Collection<Dirent> readdir(Path path, Symlinks symlinks) throws IOException {
+ Pair<Collection<Dirent>, IOException> result =
+ readdirCache.getUnchecked(Pair.of(path, symlinks));
+ Collection<Dirent> entries = result.getFirst();
+ if (entries != null) {
+ return entries;
+ }
+ throw result.getSecond();
+ }
+
+ @Override
+ public FileStatus statNullable(Path path, Symlinks symlinks) {
+ FileStatus status = statCache.getUnchecked(Pair.of(path, symlinks));
+ return (status == NO_STATUS) ? null : status;
+ }
+
+ // This is used because the cache implementations don't allow null.
+ private static final class FakeFileStatus implements FileStatus {
+ @Override
+ public long getLastChangeTime() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public long getNodeId() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public long getLastModifiedTime() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public long getSize() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isDirectory() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isFile() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isSymbolicLink() {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ /**
+ * A cache of stat calls.
+ * Input: (path, following_symlinks)
+ * Output: FileStatus
+ */
+ private static LoadingCache<Pair<Path, Symlinks>, FileStatus> newStatMap() {
+ return CacheBuilder.newBuilder().build(
+ new CacheLoader<Pair<Path, Symlinks>, FileStatus>() {
+ @Override
+ public FileStatus load(Pair<Path, Symlinks> p) {
+ FileStatus f = p.first.statNullable(p.second);
+ return (f == null) ? NO_STATUS : f;
+ }
+ });
+ }
+
+ /**
+ * A cache of readdir calls.
+ * Input: (path, following_symlinks)
+ * Output: A union of (Dirents, IOException).
+ */
+ private static
+ LoadingCache<Pair<Path, Symlinks>, Pair<Collection<Dirent>, IOException>> newReaddirMap() {
+ return CacheBuilder.newBuilder().build(
+ new CacheLoader<Pair<Path, Symlinks>, Pair<Collection<Dirent>, IOException>>() {
+ @Override
+ public Pair<Collection<Dirent>, IOException> load(Pair<Path, Symlinks> p) {
+ try {
+ return Pair.of(p.first.readdir(p.second), null);
+ } catch (IOException e) {
+ return Pair.of(null, e);
+ }
+ }
+ });
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/PostConfiguredTargetFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/PostConfiguredTargetFunction.java
new file mode 100644
index 0000000..03920b7
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/PostConfiguredTargetFunction.java
@@ -0,0 +1,145 @@
+// 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.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.DependencyResolver.Dependency;
+import com.google.devtools.build.lib.analysis.LabelAndConfiguration;
+import com.google.devtools.build.lib.analysis.TargetAndConfiguration;
+import com.google.devtools.build.lib.analysis.config.ConfigMatchingProvider;
+import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.RawAttributeMapper;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.skyframe.SkyframeActionExecutor.ConflictException;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionException;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * Build a post-processed ConfiguredTarget, vetting it for action conflict issues.
+ */
+public class PostConfiguredTargetFunction implements SkyFunction {
+ private static final Function<Dependency, SkyKey> TO_KEYS =
+ new Function<Dependency, SkyKey>() {
+ @Override
+ public SkyKey apply(Dependency input) {
+ return PostConfiguredTargetValue.key(
+ new ConfiguredTargetKey(input.getLabel(), input.getConfiguration()));
+ }
+ };
+
+ private final SkyframeExecutor.BuildViewProvider buildViewProvider;
+
+ public PostConfiguredTargetFunction(
+ SkyframeExecutor.BuildViewProvider buildViewProvider) {
+ this.buildViewProvider = Preconditions.checkNotNull(buildViewProvider);
+ }
+
+ @Nullable
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) throws SkyFunctionException {
+ ImmutableMap<Action, ConflictException> badActions = PrecomputedValue.BAD_ACTIONS.get(env);
+ ConfiguredTargetValue ctValue = (ConfiguredTargetValue)
+ env.getValue(ConfiguredTargetValue.key((ConfiguredTargetKey) skyKey.argument()));
+ SkyframeDependencyResolver resolver =
+ buildViewProvider.getSkyframeBuildView().createDependencyResolver(env);
+ if (env.valuesMissing()) {
+ return null;
+ }
+
+ for (Action action : ctValue.getActions()) {
+ if (badActions.containsKey(action)) {
+ throw new ActionConflictFunctionException(badActions.get(action));
+ }
+ }
+
+ ConfiguredTarget ct = ctValue.getConfiguredTarget();
+ TargetAndConfiguration ctgValue =
+ new TargetAndConfiguration(ct.getTarget(), ct.getConfiguration());
+
+ Set<ConfigMatchingProvider> configConditions =
+ getConfigurableAttributeConditions(ctgValue, env);
+ if (configConditions == null) {
+ return null;
+ }
+
+ Collection<Dependency> deps = resolver.dependentNodes(ctgValue, configConditions);
+ env.getValues(Iterables.transform(deps, TO_KEYS));
+ if (env.valuesMissing()) {
+ return null;
+ }
+
+ return new PostConfiguredTargetValue(ct);
+ }
+
+ /**
+ * Returns the configurable attribute conditions necessary to evaluate the given configured
+ * target, or null if not all dependencies have yet been SkyFrame-evaluated.
+ */
+ @Nullable
+ private Set<ConfigMatchingProvider> getConfigurableAttributeConditions(
+ TargetAndConfiguration ctg, Environment env) {
+ if (!(ctg.getTarget() instanceof Rule)) {
+ return ImmutableSet.of();
+ }
+ Rule rule = (Rule) ctg.getTarget();
+ RawAttributeMapper mapper = RawAttributeMapper.of(rule);
+ Set<SkyKey> depKeys = new LinkedHashSet<>();
+ for (Attribute attribute : rule.getAttributes()) {
+ for (Label label : mapper.getConfigurabilityKeys(attribute.getName(), attribute.getType())) {
+ if (!Type.Selector.isReservedLabel(label)) {
+ depKeys.add(ConfiguredTargetValue.key(label, ctg.getConfiguration()));
+ }
+ }
+ }
+ Map<SkyKey, SkyValue> cts = env.getValues(depKeys);
+ if (env.valuesMissing()) {
+ return null;
+ }
+ ImmutableSet.Builder<ConfigMatchingProvider> conditions = ImmutableSet.builder();
+ for (SkyValue ctValue : cts.values()) {
+ ConfiguredTarget ct = ((ConfiguredTargetValue) ctValue).getConfiguredTarget();
+ conditions.add(Preconditions.checkNotNull(ct.getProvider(ConfigMatchingProvider.class)));
+ }
+ return conditions.build();
+ }
+
+ @Nullable
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return Label.print(((LabelAndConfiguration) skyKey.argument()).getLabel());
+ }
+
+ private static class ActionConflictFunctionException extends SkyFunctionException {
+ public ActionConflictFunctionException(ConflictException e) {
+ super(e, Transience.PERSISTENT);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/PostConfiguredTargetValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/PostConfiguredTargetValue.java
new file mode 100644
index 0000000..42d2b38
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/PostConfiguredTargetValue.java
@@ -0,0 +1,49 @@
+// 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.collect.ImmutableList;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+/**
+ * A post-processed ConfiguredTarget which is known to be transitively error-free from action
+ * conflict issues.
+ */
+class PostConfiguredTargetValue implements SkyValue {
+
+ private final ConfiguredTarget ct;
+
+ public PostConfiguredTargetValue(ConfiguredTarget ct) {
+ this.ct = Preconditions.checkNotNull(ct);
+ }
+
+ public static ImmutableList<SkyKey> keys(Iterable<ConfiguredTargetKey> lacs) {
+ ImmutableList.Builder<SkyKey> keys = ImmutableList.builder();
+ for (ConfiguredTargetKey lac : lacs) {
+ keys.add(key(lac));
+ }
+ return keys.build();
+ }
+
+ public static SkyKey key(ConfiguredTargetKey lac) {
+ return new SkyKey(SkyFunctions.POST_CONFIGURED_TARGET, lac);
+ }
+
+ public ConfiguredTarget getCt() {
+ return ct;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/PrecomputedFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/PrecomputedFunction.java
new file mode 100644
index 0000000..8254987
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/PrecomputedFunction.java
@@ -0,0 +1,38 @@
+// 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.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionException;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+/**
+ * Builder for {@link PrecomputedValue}s.
+ *
+ * <p>Always throws an error, because the values aren't computed inside the skyframe framework.
+ */
+public class PrecomputedFunction implements SkyFunction {
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) throws SkyFunctionException,
+ InterruptedException {
+ throw new IllegalStateException(skyKey + " not set");
+ }
+
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/PrecomputedValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/PrecomputedValue.java
new file mode 100644
index 0000000..bb2656d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/PrecomputedValue.java
@@ -0,0 +1,182 @@
+// 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.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Supplier;
+import com.google.common.base.Suppliers;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.analysis.TopLevelArtifactContext;
+import com.google.devtools.build.lib.analysis.WorkspaceStatusAction;
+import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoFactory;
+import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoFactory.BuildInfoKey;
+import com.google.devtools.build.lib.packages.RuleVisibility;
+import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
+import com.google.devtools.build.lib.skyframe.SkyframeActionExecutor.ConflictException;
+import com.google.devtools.build.skyframe.Injectable;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.util.Map;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+/**
+ * A value that represents something computed outside of the skyframe framework. These values are
+ * "precomputed" from skyframe's perspective and so the graph needs to be prepopulated with them
+ * (e.g. via injection).
+ */
+public class PrecomputedValue implements SkyValue {
+ /**
+ * An externally-injected precomputed value. Exists so that modules can inject precomputed values
+ * into Skyframe's graph.
+ *
+ * <p>{@see com.google.devtools.build.lib.blaze.BlazeModule#getPrecomputedValues}.
+ */
+ public static final class Injected {
+ private final Precomputed<?> precomputed;
+ private final Supplier<? extends Object> supplier;
+
+ private Injected(Precomputed<?> precomputed, Supplier<? extends Object> supplier) {
+ this.precomputed = precomputed;
+ this.supplier = supplier;
+ }
+
+ void inject(Injectable injectable) {
+ injectable.inject(ImmutableMap.of(precomputed.key, new PrecomputedValue(supplier.get())));
+ }
+ }
+
+ public static <T> Injected injected(Precomputed<T> precomputed, Supplier<T> value) {
+ return new Injected(precomputed, value);
+ }
+
+ public static <T> Injected injected(Precomputed<T> precomputed, T value) {
+ return new Injected(precomputed, Suppliers.ofInstance(value));
+ }
+
+ static final Precomputed<String> DEFAULTS_PACKAGE_CONTENTS =
+ new Precomputed<>(new SkyKey(SkyFunctions.PRECOMPUTED, "default_pkg"));
+
+ static final Precomputed<RuleVisibility> DEFAULT_VISIBILITY =
+ new Precomputed<>(new SkyKey(SkyFunctions.PRECOMPUTED, "default_visibility"));
+
+ static final Precomputed<UUID> BUILD_ID =
+ new Precomputed<>(new SkyKey(SkyFunctions.PRECOMPUTED, "build_id"));
+
+ static final Precomputed<WorkspaceStatusAction> WORKSPACE_STATUS_KEY =
+ new Precomputed<>(new SkyKey(SkyFunctions.PRECOMPUTED, "workspace_status_action"));
+
+ static final Precomputed<Action> COVERAGE_REPORT_KEY =
+ new Precomputed<>(new SkyKey(SkyFunctions.PRECOMPUTED, "coverage_report_action"));
+
+ static final Precomputed<TopLevelArtifactContext> TOP_LEVEL_CONTEXT =
+ new Precomputed<>(new SkyKey(SkyFunctions.PRECOMPUTED, "top_level_context"));
+
+ static final Precomputed<Map<BuildInfoKey, BuildInfoFactory>> BUILD_INFO_FACTORIES =
+ new Precomputed<>(new SkyKey(SkyFunctions.PRECOMPUTED, "build_info_factories"));
+
+ static final Precomputed<Map<String, String>> TEST_ENVIRONMENT_VARIABLES =
+ new Precomputed<>(new SkyKey(SkyFunctions.PRECOMPUTED, "test_environment"));
+
+ static final Precomputed<BlazeDirectories> BLAZE_DIRECTORIES =
+ new Precomputed<>(new SkyKey(SkyFunctions.PRECOMPUTED, "blaze_directories"));
+
+ static final Precomputed<ImmutableMap<Action, ConflictException>> BAD_ACTIONS =
+ new Precomputed<>(new SkyKey(SkyFunctions.PRECOMPUTED, "bad_actions"));
+
+ public static final Precomputed<PathPackageLocator> PATH_PACKAGE_LOCATOR =
+ new Precomputed<>(new SkyKey(SkyFunctions.PRECOMPUTED, "path_package_locator"));
+
+ private final Object value;
+
+ public PrecomputedValue(Object value) {
+ this.value = Preconditions.checkNotNull(value);
+ }
+
+ /**
+ * Returns the value of the variable.
+ */
+ public Object get() {
+ return value;
+ }
+
+ @Override
+ public int hashCode() {
+ return value.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof PrecomputedValue)) {
+ return false;
+ }
+ PrecomputedValue other = (PrecomputedValue) obj;
+ return value.equals(other.value);
+ }
+
+ @Override
+ public String toString() {
+ return "<BuildVariable " + value + ">";
+ }
+
+ public static final void dependOnBuildId(SkyFunction.Environment env) {
+ BUILD_ID.get(env);
+ }
+
+ /**
+ * A helper object corresponding to a variable in Skyframe.
+ *
+ * <p>Instances do not have internal state.
+ */
+ public static final class Precomputed<T> {
+ private final SkyKey key;
+
+ public Precomputed(SkyKey key) {
+ this.key = key;
+ }
+
+ @VisibleForTesting
+ SkyKey getKeyForTesting() {
+ return key;
+ }
+
+ /**
+ * Retrieves the value of this variable from Skyframe.
+ *
+ * <p>If the value was not set, an exception will be raised.
+ */
+ @Nullable
+ @SuppressWarnings("unchecked")
+ public T get(SkyFunction.Environment env) {
+ PrecomputedValue value = (PrecomputedValue) env.getValue(key);
+ if (value == null) {
+ return null;
+ }
+ return (T) value.get();
+ }
+
+ /**
+ * Injects a new variable value.
+ */
+ void set(Injectable injectable, T value) {
+ injectable.inject(ImmutableMap.of(key, new PrecomputedValue(value)));
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalFunction.java
new file mode 100644
index 0000000..d54e98f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalFunction.java
@@ -0,0 +1,454 @@
+// 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.Function;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Verify;
+import com.google.common.collect.Collections2;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventKind;
+import com.google.devtools.build.lib.skyframe.RecursiveFilesystemTraversalValue.ResolvedFile;
+import com.google.devtools.build.lib.skyframe.RecursiveFilesystemTraversalValue.TraversalRequest;
+import com.google.devtools.build.lib.vfs.Dirent;
+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.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+/** A {@link SkyFunction} to build {@link RecursiveFilesystemTraversalValue}s. */
+public final class RecursiveFilesystemTraversalFunction implements SkyFunction {
+
+ private static final class MissingDepException extends Exception {}
+
+ /** Base class for exceptions that {@link RecursiveFilesystemTraversalFunctionException} wraps. */
+ public abstract static class RecursiveFilesystemTraversalException extends Exception {
+ protected RecursiveFilesystemTraversalException(String message) {
+ super(message);
+ }
+ }
+
+ /** Thrown when a generated directory's root-relative path conflicts with a package's path. */
+ public static final class GeneratedPathConflictException extends
+ RecursiveFilesystemTraversalException {
+ GeneratedPathConflictException(TraversalRequest traversal) {
+ super(String.format(
+ "Generated directory %s conflicts with package under the same path. Additional info: %s",
+ traversal.path.getRelativePath().getPathString(),
+ traversal.errorInfo != null ? traversal.errorInfo : traversal.toString()));
+ }
+ }
+
+ /**
+ * Thrown when the traversal encounters a subdirectory with a BUILD file but is not allowed to
+ * recurse into it.
+ */
+ public static final class CannotCrossPackageBoundaryException extends
+ RecursiveFilesystemTraversalException {
+ CannotCrossPackageBoundaryException(String message) {
+ super(message);
+ }
+ }
+
+ /**
+ * Thrown when a dangling symlink is attempted to be dereferenced.
+ *
+ * <p>Note: this class is not identical to the one in com.google.devtools.build.lib.view.fileset
+ * and it's not easy to merge the two because of the dependency structure. The other one will
+ * probably be removed along with the rest of the legacy Fileset code.
+ */
+ public static final class DanglingSymlinkException extends RecursiveFilesystemTraversalException {
+ public final String path;
+ public final String unresolvedLink;
+
+ public DanglingSymlinkException(String path, String unresolvedLink) {
+ super("Found dangling symlink: " + path + ", unresolved path: ");
+ Preconditions.checkArgument(path != null && !path.isEmpty());
+ Preconditions.checkArgument(unresolvedLink != null && !unresolvedLink.isEmpty());
+ this.path = path;
+ this.unresolvedLink = unresolvedLink;
+ }
+
+ public String getPath() {
+ return path;
+ }
+ }
+
+ /** Exception type thrown by {@link RecursiveFilesystemTraversalFunction#compute}. */
+ private static final class RecursiveFilesystemTraversalFunctionException extends
+ SkyFunctionException {
+ RecursiveFilesystemTraversalFunctionException(RecursiveFilesystemTraversalException e) {
+ super(e, Transience.PERSISTENT);
+ }
+ }
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env)
+ throws RecursiveFilesystemTraversalFunctionException {
+ TraversalRequest traversal = (TraversalRequest) skyKey.argument();
+ try {
+ // Stat the traversal root.
+ FileInfo rootInfo = lookUpFileInfo(env, traversal);
+
+ if (!rootInfo.type.exists()) {
+ // May be a dangling symlink or a non-existent file. Handle gracefully.
+ if (rootInfo.type.isSymlink()) {
+ return resultForDanglingSymlink(traversal.path, rootInfo);
+ } else {
+ return RecursiveFilesystemTraversalValue.EMPTY;
+ }
+ }
+
+ if (rootInfo.type.isFile()) {
+ // The root is a file or a symlink to one.
+ return resultForFileRoot(traversal.path, rootInfo);
+ }
+
+ // Otherwise the root is a directory or a symlink to one.
+ PkgLookupResult pkgLookupResult = checkIfPackage(env, traversal, rootInfo);
+ traversal = pkgLookupResult.traversal;
+
+ if (pkgLookupResult.isConflicting()) {
+ // The traversal was requested for an output directory whose root-relative path conflicts
+ // with a source package. We can't handle that, bail out.
+ throw new RecursiveFilesystemTraversalFunctionException(
+ new GeneratedPathConflictException(traversal));
+ } else if (pkgLookupResult.isPackage() && !traversal.skipTestingForSubpackage) {
+ // The traversal was requested for a directory that defines a package.
+ if (traversal.crossPkgBoundaries) {
+ // We are free to traverse the subpackage but we need to display a warning.
+ String msg = traversal.errorInfo + " crosses package boundary into package rooted at "
+ + traversal.path.getRelativePath().getPathString();
+ env.getListener().handle(new Event(EventKind.WARNING, null, msg));
+ } else {
+ // We cannot traverse the subpackage and should skip it silently. Return empty results.
+ return RecursiveFilesystemTraversalValue.EMPTY;
+ }
+ }
+
+ // We are free to traverse this directory.
+ Collection<SkyKey> dependentKeys = createRecursiveTraversalKeys(env, traversal);
+ return resultForDirectory(traversal, rootInfo, traverseChildren(env, dependentKeys));
+ } catch (MissingDepException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+
+ private static final class FileInfo {
+ final FileType type;
+ final FileStateValue metadata;
+ @Nullable final RootedPath realPath;
+ @Nullable final PathFragment unresolvedSymlinkTarget;
+
+ FileInfo(FileType type, FileStateValue metadata, @Nullable RootedPath realPath,
+ @Nullable PathFragment unresolvedSymlinkTarget) {
+ this.type = Preconditions.checkNotNull(type);
+ this.metadata = Preconditions.checkNotNull(metadata);
+ this.realPath = realPath;
+ this.unresolvedSymlinkTarget = unresolvedSymlinkTarget;
+ }
+
+ @Override
+ public String toString() {
+ if (type.isSymlink()) {
+ return String.format("(%s: link_value=%s, real_path=%s)", type,
+ unresolvedSymlinkTarget.getPathString(), realPath);
+ } else {
+ return String.format("(%s: real_path=%s)", type, realPath);
+ }
+ }
+ }
+
+ private static FileInfo lookUpFileInfo(Environment env, TraversalRequest traversal)
+ throws MissingDepException {
+ // Stat the file.
+ FileValue fileValue = (FileValue) getDependentSkyValue(env, FileValue.key(traversal.path));
+ if (fileValue.exists()) {
+ // If it exists, it may either be a symlink or a file/directory.
+ PathFragment unresolvedLinkTarget = null;
+ FileType type = null;
+ if (fileValue.isSymlink()) {
+ unresolvedLinkTarget = fileValue.getUnresolvedLinkTarget();
+ type = fileValue.isDirectory() ? FileType.SYMLINK_TO_DIRECTORY : FileType.SYMLINK_TO_FILE;
+ } else {
+ type = fileValue.isDirectory() ? FileType.DIRECTORY : FileType.FILE;
+ }
+ return new FileInfo(type, fileValue.realFileStateValue(),
+ fileValue.realRootedPath(), unresolvedLinkTarget);
+ } else {
+ // If it doesn't exist, or it's a dangling symlink, we still want to handle that gracefully.
+ return new FileInfo(
+ fileValue.isSymlink() ? FileType.DANGLING_SYMLINK : FileType.NONEXISTENT,
+ fileValue.realFileStateValue(), null,
+ fileValue.isSymlink() ? fileValue.getUnresolvedLinkTarget() : null);
+ }
+ }
+
+ private static final class PkgLookupResult {
+ private enum Type {
+ CONFLICT, DIRECTORY, PKG
+ }
+
+ private final Type type;
+ final TraversalRequest traversal;
+ final FileInfo rootInfo;
+
+ /** Result for a generated directory that conflicts with a source package. */
+ static PkgLookupResult conflict(TraversalRequest traversal, FileInfo rootInfo) {
+ return new PkgLookupResult(Type.CONFLICT, traversal, rootInfo);
+ }
+
+ /** Result for a source or generated directory (not a package). */
+ static PkgLookupResult directory(TraversalRequest traversal, FileInfo rootInfo) {
+ return new PkgLookupResult(Type.DIRECTORY, traversal, rootInfo);
+ }
+
+ /** Result for a package, i.e. a directory with a BUILD file. */
+ static PkgLookupResult pkg(TraversalRequest traversal, FileInfo rootInfo) {
+ return new PkgLookupResult(Type.PKG, traversal, rootInfo);
+ }
+
+ private PkgLookupResult(Type type, TraversalRequest traversal, FileInfo rootInfo) {
+ this.type = Preconditions.checkNotNull(type);
+ this.traversal = Preconditions.checkNotNull(traversal);
+ this.rootInfo = Preconditions.checkNotNull(rootInfo);
+ }
+
+ boolean isPackage() {
+ return type == Type.PKG;
+ }
+
+ boolean isConflicting() {
+ return type == Type.CONFLICT;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("(%s: info=%s, traversal=%s)", type, rootInfo, traversal);
+ }
+ }
+
+ /**
+ * Checks whether the {@code traversal}'s path refers to a package directory.
+ *
+ * @return the result of the lookup; it contains potentially new {@link TraversalRequest} and
+ * {@link FileInfo} so the caller should use these instead of the old ones (this happens when
+ * a package is found, but under a different root than expected)
+ */
+ private static PkgLookupResult checkIfPackage(Environment env, TraversalRequest traversal,
+ FileInfo rootInfo) throws MissingDepException {
+ Preconditions.checkArgument(rootInfo.type.exists() && !rootInfo.type.isFile(),
+ "{%s} {%s}", traversal, rootInfo);
+ PackageLookupValue pkgLookup = (PackageLookupValue) getDependentSkyValue(env,
+ PackageLookupValue.key(traversal.path.getRelativePath()));
+
+ if (pkgLookup.packageExists()) {
+ if (traversal.isGenerated) {
+ // The traversal's root was a generated directory, but its root-relative path conflicts with
+ // an existing package.
+ return PkgLookupResult.conflict(traversal, rootInfo);
+ } else {
+ // The traversal's root was a source directory and it defines a package.
+ Path pkgRoot = pkgLookup.getRoot();
+ if (!pkgRoot.equals(traversal.path.getRoot())) {
+ // However the root of this package is different from what we expected. stat() the real
+ // BUILD file of that package.
+ traversal = traversal.forChangedRootPath(pkgRoot);
+ rootInfo = lookUpFileInfo(env, traversal);
+ Verify.verify(rootInfo.type.exists(), "{%s} {%s}", traversal, rootInfo);
+ }
+ return PkgLookupResult.pkg(traversal, rootInfo);
+ }
+ } else {
+ // The traversal's root was a directory (source or generated one), no package exists under the
+ // same root-relative path.
+ return PkgLookupResult.directory(traversal, rootInfo);
+ }
+ }
+
+ /**
+ * List the directory and create {@code SkyKey}s to request contents of its children recursively.
+ *
+ * <p>The returned keys are of type {@link SkyFunctions#RECURSIVE_FILESYSTEM_TRAVERSAL}.
+ */
+ private static Collection<SkyKey> createRecursiveTraversalKeys(Environment env,
+ TraversalRequest traversal) throws MissingDepException {
+ // Use the traversal's path, even if it's a symlink. The contents of the directory, as listed
+ // in the result, must be relative to it.
+ DirectoryListingValue dirListing = (DirectoryListingValue) getDependentSkyValue(env,
+ DirectoryListingValue.key(traversal.path));
+
+ List<SkyKey> result = new ArrayList<>();
+ for (Dirent dirent : dirListing.getDirents()) {
+ RootedPath childPath = RootedPath.toRootedPath(traversal.path.getRoot(),
+ traversal.path.getRelativePath().getRelative(dirent.getName()));
+ TraversalRequest childTraversal = traversal.forChildEntry(childPath);
+ result.add(RecursiveFilesystemTraversalValue.key(childTraversal));
+ }
+ return result;
+ }
+
+ /**
+ * Creates result for a dangling symlink.
+ *
+ * @param linkName path to the symbolic link
+ * @param info the {@link FileInfo} associated with the link file
+ */
+ private static RecursiveFilesystemTraversalValue resultForDanglingSymlink(RootedPath linkName,
+ FileInfo info) {
+ Preconditions.checkState(info.type.isSymlink() && !info.type.exists(), "{%s} {%s}", linkName,
+ info.type);
+ return RecursiveFilesystemTraversalValue.of(
+ ResolvedFile.danglingSymlink(linkName, info.unresolvedSymlinkTarget, info.metadata));
+ }
+
+ /**
+ * Creates results for a file or for a symlink that points to one.
+ *
+ * <p>A symlink may be direct (points to a file) or transitive (points at a direct or transitive
+ * symlink).
+ */
+ private static RecursiveFilesystemTraversalValue resultForFileRoot(RootedPath path,
+ FileInfo info) {
+ Preconditions.checkState(info.type.isFile() && info.type.exists(), "{%s} {%s}", path,
+ info.type);
+ if (info.type.isSymlink()) {
+ return RecursiveFilesystemTraversalValue.of(ResolvedFile.symlinkToFile(info.realPath, path,
+ info.unresolvedSymlinkTarget, info.metadata));
+ } else {
+ return RecursiveFilesystemTraversalValue.of(ResolvedFile.regularFile(path, info.metadata));
+ }
+ }
+
+ private static RecursiveFilesystemTraversalValue resultForDirectory(TraversalRequest traversal,
+ FileInfo rootInfo, Collection<RecursiveFilesystemTraversalValue> subdirTraversals) {
+ // Collect transitive closure of files in subdirectories.
+ NestedSetBuilder<ResolvedFile> paths = NestedSetBuilder.stableOrder();
+ for (RecursiveFilesystemTraversalValue child : subdirTraversals) {
+ paths.addTransitive(child.getTransitiveFiles());
+ }
+ ResolvedFile root;
+ if (rootInfo.type.isSymlink()) {
+ root = ResolvedFile.symlinkToDirectory(rootInfo.realPath, traversal.path,
+ rootInfo.unresolvedSymlinkTarget, rootInfo.metadata);
+ paths.add(root);
+ } else {
+ root = ResolvedFile.directory(rootInfo.realPath);
+ }
+ return RecursiveFilesystemTraversalValue.of(root, paths.build());
+ }
+
+ private static SkyValue getDependentSkyValue(Environment env, SkyKey key)
+ throws MissingDepException {
+ SkyValue value = env.getValue(key);
+ if (env.valuesMissing()) {
+ throw new MissingDepException();
+ }
+ return value;
+ }
+
+ /**
+ * Requests Skyframe to compute the dependent values and returns them.
+ *
+ * <p>The keys must all be {@link SkyFunctions#RECURSIVE_FILESYSTEM_TRAVERSAL} keys.
+ */
+ private static Collection<RecursiveFilesystemTraversalValue> traverseChildren(
+ Environment env, Iterable<SkyKey> keys)
+ throws MissingDepException {
+ Map<SkyKey, SkyValue> values = env.getValues(keys);
+ if (env.valuesMissing()) {
+ throw new MissingDepException();
+ }
+ return Collections2.transform(values.values(),
+ new Function<SkyValue, RecursiveFilesystemTraversalValue>() {
+ @Override
+ public RecursiveFilesystemTraversalValue apply(SkyValue input) {
+ return (RecursiveFilesystemTraversalValue) input;
+ }
+ });
+ }
+
+ /** Type information about the filesystem entry residing at a path. */
+ enum FileType {
+ /** A regular file. */
+ FILE {
+ @Override boolean isFile() { return true; }
+ @Override boolean exists() { return true; }
+ @Override public String toString() { return "<f>"; }
+ },
+ /**
+ * A symlink to a regular file.
+ *
+ * <p>The symlink may be direct (points to a non-symlink (here a file)) or it may be transitive
+ * (points to a direct or transitive symlink).
+ */
+ SYMLINK_TO_FILE {
+ @Override boolean isFile() { return true; }
+ @Override boolean isSymlink() { return true; }
+ @Override boolean exists() { return true; }
+ @Override public String toString() { return "<lf>"; }
+ },
+ /** A directory. */
+ DIRECTORY {
+ @Override boolean isDirectory() { return true; }
+ @Override boolean exists() { return true; }
+ @Override public String toString() { return "<d>"; }
+ },
+ /**
+ * A symlink to a directory.
+ *
+ * <p>The symlink may be direct (points to a non-symlink (here a directory)) or it may be
+ * transitive (points to a direct or transitive symlink).
+ */
+ SYMLINK_TO_DIRECTORY {
+ @Override boolean isDirectory() { return true; }
+ @Override boolean isSymlink() { return true; }
+ @Override boolean exists() { return true; }
+ @Override public String toString() { return "<ld>"; }
+ },
+ /** A dangling symlink, i.e. one whose target is known not to exist. */
+ DANGLING_SYMLINK {
+ @Override boolean isFile() { throw new UnsupportedOperationException(); }
+ @Override boolean isDirectory() { throw new UnsupportedOperationException(); }
+ @Override boolean isSymlink() { return true; }
+ @Override public String toString() { return "<l?>"; }
+ },
+ /** A path that does not exist or should be ignored. */
+ NONEXISTENT {
+ @Override public String toString() { return "<?>"; }
+ };
+
+ boolean isFile() { return false; }
+ boolean isDirectory() { return false; }
+ boolean isSymlink() { return false; }
+ boolean exists() { return false; }
+ @Override public abstract String toString();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalValue.java
new file mode 100644
index 0000000..023b1cf
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalValue.java
@@ -0,0 +1,597 @@
+// 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.annotations.VisibleForTesting;
+import com.google.common.base.Objects;
+import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.skyframe.RecursiveFilesystemTraversalFunction.DanglingSymlinkException;
+import com.google.devtools.build.lib.skyframe.RecursiveFilesystemTraversalFunction.FileType;
+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.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import javax.annotation.Nullable;
+
+/**
+ * Collection of files found while recursively traversing a path.
+ *
+ * <p>The path may refer to files, symlinks or directories that may or may not exist.
+ *
+ * <p>Traversing a file or a symlink results in a single {@link ResolvedFile} corresponding to the
+ * file or symlink.
+ *
+ * <p>Traversing a directory results in a collection of {@link ResolvedFile}s for all files and
+ * symlinks under it, and in all of its subdirectories. The {@link TraversalRequest} can specify
+ * whether to traverse source subdirectories that are packages (have BUILD files in them).
+ *
+ * <p>Traversing a symlink that points to a directory is the same as traversing a normal directory.
+ * The paths in the result will not be resolved; the files will be listed under the symlink, as if
+ * it was the actual directory they reside in.
+ *
+ * <p>Editing a file that is part of this traversal, or adding or removing a file in a directory
+ * that is part of this traversal, will invalidate this {@link SkyValue}. This also applies to
+ * directories that are symlinked to.
+ */
+public final class RecursiveFilesystemTraversalValue implements SkyValue {
+ static final RecursiveFilesystemTraversalValue EMPTY = new RecursiveFilesystemTraversalValue(
+ Optional.<ResolvedFile>absent(),
+ NestedSetBuilder.<ResolvedFile>emptySet(Order.STABLE_ORDER));
+
+ /** The root of the traversal. May only be absent for the {@link #EMPTY} instance. */
+ private final Optional<ResolvedFile> resolvedRoot;
+
+ /** The transitive closure of {@link ResolvedFile}s. */
+ private final NestedSet<ResolvedFile> resolvedPaths;
+
+ private RecursiveFilesystemTraversalValue(Optional<ResolvedFile> resolvedRoot,
+ NestedSet<ResolvedFile> resolvedPaths) {
+ this.resolvedRoot = Preconditions.checkNotNull(resolvedRoot);
+ this.resolvedPaths = Preconditions.checkNotNull(resolvedPaths);
+ }
+
+ static RecursiveFilesystemTraversalValue of(ResolvedFile resolvedRoot,
+ NestedSet<ResolvedFile> resolvedPaths) {
+ if (resolvedPaths.isEmpty()) {
+ return EMPTY;
+ } else {
+ return new RecursiveFilesystemTraversalValue(Optional.of(resolvedRoot), resolvedPaths);
+ }
+ }
+
+ static RecursiveFilesystemTraversalValue of(ResolvedFile singleMember) {
+ return new RecursiveFilesystemTraversalValue(Optional.of(singleMember),
+ NestedSetBuilder.<ResolvedFile>create(Order.STABLE_ORDER, singleMember));
+ }
+
+ /** Returns the root of the traversal; absent only for the {@link #EMPTY} instance. */
+ public Optional<ResolvedFile> getResolvedRoot() {
+ return resolvedRoot;
+ }
+
+ /**
+ * Retrieves the set of {@link ResolvedFile}s that were found by this traversal.
+ *
+ * <p>The returned set may be empty if no files were found, or the ones found were to be
+ * considered non-existent. Unless it's empty, the returned set always includes the
+ * {@link #getResolvedRoot() resolved root}.
+ *
+ * <p>The returned set also includes symlinks. If a symlink points to a directory, its contents
+ * are also included in this set, and their path will start with the symlink's path, just like on
+ * a usual Unix file system.
+ */
+ public NestedSet<ResolvedFile> getTransitiveFiles() {
+ return resolvedPaths;
+ }
+
+ public static SkyKey key(TraversalRequest traversal) {
+ return new SkyKey(SkyFunctions.RECURSIVE_FILESYSTEM_TRAVERSAL, traversal);
+ }
+
+ /** The parameters of a file or directory traversal. */
+ public static final class TraversalRequest {
+
+ /** The path to start the traversal from; may be a file, a directory or a symlink. */
+ final RootedPath path;
+
+ /**
+ * Whether the path is in the output tree.
+ *
+ * <p>Such paths and all their subdirectories are assumed not to define packages, so package
+ * lookup for them is skipped.
+ */
+ final boolean isGenerated;
+
+ /** Whether traversal should descend into directories that are roots of subpackages. */
+ final boolean crossPkgBoundaries;
+
+ /**
+ * Whether to skip checking if the root (if it's a directory) contains a BUILD file.
+ *
+ * <p>Such directories are not considered to be packages when this flag is true. This needs to
+ * be true in order to traverse directories of packages, but should be false for <i>their</i>
+ * subdirectories.
+ */
+ final boolean skipTestingForSubpackage;
+
+ /** Information to be attached to any error messages that may be reported. */
+ @Nullable final String errorInfo;
+
+ public TraversalRequest(RootedPath path, boolean isRootGenerated,
+ boolean crossPkgBoundaries, boolean skipTestingForSubpackage,
+ @Nullable String errorInfo) {
+ this.path = path;
+ this.isGenerated = isRootGenerated;
+ this.crossPkgBoundaries = crossPkgBoundaries;
+ this.skipTestingForSubpackage = skipTestingForSubpackage;
+ this.errorInfo = errorInfo;
+ }
+
+ private TraversalRequest duplicate(RootedPath newRoot, boolean newSkipTestingForSubpackage) {
+ return new TraversalRequest(newRoot, isGenerated, crossPkgBoundaries,
+ newSkipTestingForSubpackage, errorInfo);
+ }
+
+ /** Creates a new request to traverse a child element in the current directory (the root). */
+ TraversalRequest forChildEntry(RootedPath newPath) {
+ return duplicate(newPath, false);
+ }
+
+ /**
+ * Creates a new request for a changed root.
+ *
+ * <p>This method can be used when a package is found out to be under a different root path than
+ * originally assumed.
+ */
+ TraversalRequest forChangedRootPath(Path newRoot) {
+ return duplicate(RootedPath.toRootedPath(newRoot, path.getRelativePath()),
+ skipTestingForSubpackage);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof TraversalRequest)) {
+ return false;
+ }
+ TraversalRequest o = (TraversalRequest) obj;
+ return path.equals(o.path) && isGenerated == o.isGenerated
+ && crossPkgBoundaries == o.crossPkgBoundaries
+ && skipTestingForSubpackage == o.skipTestingForSubpackage;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(path, isGenerated, crossPkgBoundaries, skipTestingForSubpackage);
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "TraversalParams(root=%s, is_generated=%d, skip_testing_for_subpkg=%d,"
+ + " pkg_boundaries=%d)", path, isGenerated ? 1 : 0, skipTestingForSubpackage ? 1 : 0,
+ crossPkgBoundaries ? 1 : 0);
+ }
+ }
+
+ /**
+ * Path and type information about a single file or symlink.
+ *
+ * <p>The object stores things such as the absolute path of the file or symlink, its exact type
+ * and, if it's a symlink, the resolved and unresolved link target paths.
+ */
+ public abstract static class ResolvedFile {
+ private static final class Symlink {
+ private final RootedPath linkName;
+ private final PathFragment unresolvedLinkTarget;
+ // The resolved link target is stored in ResolvedFile.path
+
+ private Symlink(RootedPath linkName, PathFragment unresolvedLinkTarget) {
+ this.linkName = Preconditions.checkNotNull(linkName);
+ this.unresolvedLinkTarget = Preconditions.checkNotNull(unresolvedLinkTarget);
+ }
+
+ PathFragment getNameInSymlinkTree() {
+ return linkName.getRelativePath();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof Symlink)) {
+ return false;
+ }
+ Symlink o = (Symlink) obj;
+ return linkName.equals(o.linkName) && unresolvedLinkTarget.equals(o.unresolvedLinkTarget);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(linkName, unresolvedLinkTarget);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("Symlink(link_name=%s, unresolved_target=%s)",
+ linkName, unresolvedLinkTarget);
+ }
+ }
+
+ private static final class RegularFile extends ResolvedFile {
+ private RegularFile(RootedPath path) {
+ super(FileType.FILE, Optional.of(path), Optional.<FileStateValue>absent());
+ }
+
+ RegularFile(RootedPath path, FileStateValue metadata) {
+ super(FileType.FILE, Optional.of(path), Optional.of(metadata));
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof RegularFile)) {
+ return false;
+ }
+ return super.isEqualTo((RegularFile) obj);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("RegularFile(%s)", super.toString());
+ }
+
+ @Override
+ ResolvedFile stripMetadataForTesting() {
+ return new RegularFile(path.get());
+ }
+
+ @Override
+ public PathFragment getNameInSymlinkTree() {
+ return path.get().getRelativePath();
+ }
+
+ @Override
+ public PathFragment getTargetInSymlinkTree(boolean followSymlinks) {
+ return path.get().asPath().asFragment();
+ }
+ }
+
+ private static final class Directory extends ResolvedFile {
+ Directory(RootedPath path) {
+ super(FileType.DIRECTORY, Optional.of(path), Optional.of(
+ FileStateValue.DIRECTORY_FILE_STATE_NODE));
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof Directory)) {
+ return false;
+ }
+ return super.isEqualTo((Directory) obj);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("Directory(%s)", super.toString());
+ }
+
+ @Override
+ ResolvedFile stripMetadataForTesting() {
+ return this;
+ }
+
+ @Override
+ public PathFragment getNameInSymlinkTree() {
+ return path.get().getRelativePath();
+ }
+
+ @Override
+ public PathFragment getTargetInSymlinkTree(boolean followSymlinks) {
+ return path.get().asPath().asFragment();
+ }
+ }
+
+ private static final class DanglingSymlink extends ResolvedFile {
+ private final Symlink symlink;
+
+ private DanglingSymlink(Symlink symlink) {
+ super(FileType.DANGLING_SYMLINK, Optional.<RootedPath>absent(),
+ Optional.<FileStateValue>absent());
+ this.symlink = symlink;
+ }
+
+ DanglingSymlink(RootedPath linkNamePath, PathFragment linkTargetPath,
+ FileStateValue metadata) {
+ super(FileType.DANGLING_SYMLINK, Optional.<RootedPath>absent(), Optional.of(metadata));
+ this.symlink = new Symlink(linkNamePath, linkTargetPath);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof DanglingSymlink)) {
+ return false;
+ }
+ DanglingSymlink o = (DanglingSymlink) obj;
+ return super.isEqualTo(o) && symlink.equals(o.symlink);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(super.hashCode(), symlink);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("DanglingSymlink(%s, %s)", super.toString(), symlink);
+ }
+
+ @Override
+ ResolvedFile stripMetadataForTesting() {
+ return new DanglingSymlink(symlink);
+ }
+
+ @Override
+ public PathFragment getNameInSymlinkTree() {
+ return symlink.getNameInSymlinkTree();
+ }
+
+ @Override
+ public PathFragment getTargetInSymlinkTree(boolean followSymlinks)
+ throws DanglingSymlinkException {
+ if (followSymlinks) {
+ throw new DanglingSymlinkException(symlink.linkName.asPath().getPathString(),
+ symlink.unresolvedLinkTarget.getPathString());
+ } else {
+ return symlink.unresolvedLinkTarget;
+ }
+ }
+ }
+
+ private static final class SymlinkToFile extends ResolvedFile {
+ private final Symlink symlink;
+
+ private SymlinkToFile(RootedPath targetPath, Symlink symlink) {
+ super(FileType.SYMLINK_TO_FILE, Optional.of(targetPath), Optional.<FileStateValue>absent());
+ this.symlink = symlink;
+ }
+
+ SymlinkToFile(RootedPath targetPath, RootedPath linkNamePath,
+ PathFragment linkTargetPath, FileStateValue metadata) {
+ super(FileType.SYMLINK_TO_FILE, Optional.of(targetPath), Optional.of(metadata));
+ this.symlink = new Symlink(linkNamePath, linkTargetPath);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof SymlinkToFile)) {
+ return false;
+ }
+ SymlinkToFile o = (SymlinkToFile) obj;
+ return super.isEqualTo(o) && symlink.equals(o.symlink);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(super.hashCode(), symlink);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("SymlinkToFile(%s, %s)", super.toString(), symlink);
+ }
+
+ @Override
+ ResolvedFile stripMetadataForTesting() {
+ return new SymlinkToFile(path.get(), symlink);
+ }
+
+ @Override
+ public PathFragment getNameInSymlinkTree() {
+ return symlink.getNameInSymlinkTree();
+ }
+
+ @Override
+ public PathFragment getTargetInSymlinkTree(boolean followSymlinks) {
+ return followSymlinks ? path.get().asPath().asFragment() : symlink.unresolvedLinkTarget;
+ }
+ }
+
+ private static final class SymlinkToDirectory extends ResolvedFile {
+ private final Symlink symlink;
+
+ private SymlinkToDirectory(RootedPath targetPath, Symlink symlink) {
+ super(FileType.SYMLINK_TO_DIRECTORY, Optional.of(targetPath),
+ Optional.<FileStateValue>absent());
+ this.symlink = symlink;
+ }
+
+ SymlinkToDirectory(RootedPath targetPath, RootedPath linkNamePath,
+ PathFragment linkValue, FileStateValue metadata) {
+ super(FileType.SYMLINK_TO_DIRECTORY, Optional.of(targetPath), Optional.of(metadata));
+ this.symlink = new Symlink(linkNamePath, linkValue);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof SymlinkToDirectory)) {
+ return false;
+ }
+ SymlinkToDirectory o = (SymlinkToDirectory) obj;
+ return super.isEqualTo(o) && symlink.equals(o.symlink);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(super.hashCode(), symlink);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("SymlinkToDirectory(%s, %s)", super.toString(), symlink);
+ }
+
+ @Override
+ ResolvedFile stripMetadataForTesting() {
+ return new SymlinkToDirectory(path.get(), symlink);
+ }
+
+ @Override
+ public PathFragment getNameInSymlinkTree() {
+ return symlink.getNameInSymlinkTree();
+ }
+
+ @Override
+ public PathFragment getTargetInSymlinkTree(boolean followSymlinks) {
+ return followSymlinks ? path.get().asPath().asFragment() : symlink.unresolvedLinkTarget;
+ }
+ }
+
+ /** Type of the entity under {@link #path}. */
+ final FileType type;
+
+ /**
+ * Path of the file, directory or resolved target of the symlink.
+ *
+ * <p>May only be absent for dangling symlinks.
+ */
+ protected final Optional<RootedPath> path;
+
+ /**
+ * Associated metadata.
+ *
+ * <p>This field must be stored so that this {@link ResolvedFile} is (also) the function of the
+ * stat() of the file, but otherwise it is likely not something the consumer of the
+ * {@link ResolvedFile} is directly interested in.
+ *
+ * <p>May only be absent if stripped for tests.
+ */
+ final Optional<FileStateValue> metadata;
+
+ private ResolvedFile(FileType type, Optional<RootedPath> path,
+ Optional<FileStateValue> metadata) {
+ this.type = Preconditions.checkNotNull(type);
+ this.path = Preconditions.checkNotNull(path);
+ this.metadata = Preconditions.checkNotNull(metadata);
+ }
+
+ static ResolvedFile regularFile(RootedPath path, FileStateValue metadata) {
+ return new RegularFile(path, metadata);
+ }
+
+ static ResolvedFile directory(RootedPath path) {
+ return new Directory(path);
+ }
+
+ static ResolvedFile symlinkToFile(RootedPath targetPath, RootedPath linkNamePath,
+ PathFragment linkTargetPath, FileStateValue metadata) {
+ return new SymlinkToFile(targetPath, linkNamePath, linkTargetPath, metadata);
+ }
+
+ static ResolvedFile symlinkToDirectory(RootedPath targetPath,
+ RootedPath linkNamePath, PathFragment linkValue, FileStateValue metadata) {
+ return new SymlinkToDirectory(targetPath, linkNamePath, linkValue, metadata);
+ }
+
+ static ResolvedFile danglingSymlink(RootedPath linkNamePath, PathFragment linkValue,
+ FileStateValue metadata) {
+ return new DanglingSymlink(linkNamePath, linkValue, metadata);
+ }
+
+ private boolean isEqualTo(ResolvedFile o) {
+ return type.equals(o.type) && path.equals(o.path) && metadata.equals(o.metadata);
+ }
+
+ @Override
+ public abstract boolean equals(Object obj);
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(type, path, metadata);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("type=%s, path=%s, metadata=%s", type, path,
+ metadata.isPresent() ? Integer.toHexString(metadata.get().hashCode()) : "(stripped)");
+ }
+
+ /**
+ * Returns the path of the Fileset-output symlink relative to the output directory.
+ *
+ * <p>The path should contain the FilesetEntry-specific destination directory (if any) and
+ * should have necessary prefixes stripped (if any).
+ */
+ public abstract PathFragment getNameInSymlinkTree();
+
+ /**
+ * Returns the path of the symlink target.
+ *
+ * @throws DanglingSymlinkException if the target cannot be resolved because the symlink is
+ * dangling
+ */
+ public abstract PathFragment getTargetInSymlinkTree(boolean followSymlinks)
+ throws DanglingSymlinkException;
+
+ /**
+ * Returns a copy of this object with the metadata stripped away.
+ *
+ * <p>This method should only be used by tests that wish to assert that this
+ * {@link ResolvedFile} refers to the expected absolute path and has the expected type, without
+ * asserting its actual contents (which the metadata is a function of).
+ */
+ @VisibleForTesting
+ abstract ResolvedFile stripMetadataForTesting();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof RecursiveFilesystemTraversalValue)) {
+ return false;
+ }
+ RecursiveFilesystemTraversalValue o = (RecursiveFilesystemTraversalValue) obj;
+ return resolvedRoot.equals(o.resolvedRoot) && resolvedPaths.equals(o.resolvedPaths);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(resolvedRoot, resolvedPaths);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/RecursivePkgFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/RecursivePkgFunction.java
new file mode 100644
index 0000000..11ed3be
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/RecursivePkgFunction.java
@@ -0,0 +1,151 @@
+// 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.collect.Lists;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.packages.NoSuchPackageException;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
+import com.google.devtools.build.lib.vfs.Dirent;
+import com.google.devtools.build.lib.vfs.Dirent.Type;
+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.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+/**
+ * RecursivePkgFunction builds up the set of packages underneath a given directory
+ * transitively.
+ *
+ * <p>Example: foo/BUILD, foo/sub/x, foo/subpkg/BUILD would yield transitive packages "foo" and
+ * "foo/subpkg".
+ */
+public class RecursivePkgFunction implements SkyFunction {
+
+ private static final Order ORDER = Order.STABLE_ORDER;
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) {
+ RootedPath rootedPath = (RootedPath) skyKey.argument();
+ Path root = rootedPath.getRoot();
+ PathFragment rootRelativePath = rootedPath.getRelativePath();
+
+ SkyKey fileKey = FileValue.key(rootedPath);
+ FileValue fileValue = (FileValue) env.getValue(fileKey);
+ if (fileValue == null) {
+ return null;
+ }
+
+ if (!fileValue.isDirectory()) {
+ return new RecursivePkgValue(NestedSetBuilder.<String>emptySet(ORDER));
+ }
+
+ if (fileValue.isSymlink()) {
+ // We do not follow directory symlinks when we look recursively for packages. It also
+ // prevents symlink loops.
+ return new RecursivePkgValue(NestedSetBuilder.<String>emptySet(ORDER));
+ }
+
+ PackageIdentifier packageId = PackageIdentifier.createInDefaultRepo(
+ rootRelativePath.getPathString());
+ PackageLookupValue pkgLookupValue =
+ (PackageLookupValue) env.getValue(PackageLookupValue.key(packageId));
+ if (pkgLookupValue == null) {
+ return null;
+ }
+
+ NestedSetBuilder<String> packages = new NestedSetBuilder<>(ORDER);
+
+ if (pkgLookupValue.packageExists()) {
+ if (pkgLookupValue.getRoot().equals(root)) {
+ try {
+ PackageValue pkgValue = (PackageValue)
+ env.getValueOrThrow(PackageValue.key(packageId),
+ NoSuchPackageException.class);
+ if (pkgValue == null) {
+ return null;
+ }
+ packages.add(pkgValue.getPackage().getName());
+ } catch (NoSuchPackageException e) {
+ // The package had errors, but don't fail-fast as there might subpackages below the
+ // current directory.
+ env.getListener().handle(Event.error(
+ "package contains errors: " + rootRelativePath.getPathString()));
+ if (e.getPackage() != null) {
+ packages.add(e.getPackage().getName());
+ }
+ }
+ }
+ // The package lookup succeeded, but was under a different root. We still, however, need to
+ // recursively consider subdirectories. For example:
+ //
+ // Pretend --package_path=rootA/workspace:rootB/workspace and these are the only files:
+ // rootA/workspace/foo/
+ // rootA/workspace/foo/bar/BUILD
+ // rootB/workspace/foo/BUILD
+ // If we're doing a recursive package lookup under 'rootA/workspace' starting at 'foo', note
+ // that even though the package 'foo' is under 'rootB/workspace', there is still a package
+ // 'foo/bar' under 'rootA/workspace'.
+ }
+
+ DirectoryListingValue dirValue = (DirectoryListingValue)
+ env.getValue(DirectoryListingValue.key(rootedPath));
+ if (dirValue == null) {
+ return null;
+ }
+
+ List<SkyKey> childDeps = Lists.newArrayList();
+ for (Dirent dirent : dirValue.getDirents()) {
+ if (dirent.getType() != Type.DIRECTORY) {
+ // Non-directories can never host packages, and we do not follow symlinks (see above).
+ continue;
+ }
+ String basename = dirent.getName();
+ if (rootRelativePath.equals(PathFragment.EMPTY_FRAGMENT)
+ && PathPackageLocator.DEFAULT_TOP_LEVEL_EXCLUDES.contains(basename)) {
+ continue;
+ }
+ SkyKey req = RecursivePkgValue.key(RootedPath.toRootedPath(root,
+ rootRelativePath.getRelative(basename)));
+ childDeps.add(req);
+ }
+ Map<SkyKey, SkyValue> childValueMap = env.getValues(childDeps);
+ if (env.valuesMissing()) {
+ return null;
+ }
+ // Aggregate the transitive subpackages.
+ for (SkyValue childValue : childValueMap.values()) {
+ if (childValue != null) {
+ packages.addTransitive(((RecursivePkgValue) childValue).getPackages());
+ }
+ }
+ return new RecursivePkgValue(packages.build());
+ }
+
+ @Nullable
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/RecursivePkgValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/RecursivePkgValue.java
new file mode 100644
index 0000000..4013b90
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/RecursivePkgValue.java
@@ -0,0 +1,48 @@
+// 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.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+/**
+ * This value represents the result of looking up all the packages under a given package path root,
+ * starting at a given directory.
+ */
+@Immutable
+@ThreadSafe
+public class RecursivePkgValue implements SkyValue {
+
+ private final NestedSet<String> packages;
+
+ public RecursivePkgValue(NestedSet<String> packages) {
+ this.packages = packages;
+ }
+
+ /**
+ * Create a transitive package lookup request.
+ */
+ @ThreadSafe
+ public static SkyKey key(RootedPath rootedPath) {
+ return new SkyKey(SkyFunctions.RECURSIVE_PKG, rootedPath);
+ }
+
+ public NestedSet<String> getPackages() {
+ return packages;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/RepositoryValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/RepositoryValue.java
new file mode 100644
index 0000000..3183953
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/RepositoryValue.java
@@ -0,0 +1,75 @@
+// 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.Objects;
+import com.google.devtools.build.lib.packages.PackageIdentifier.RepositoryName;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+/**
+ * A local view of an external repository.
+ */
+public class RepositoryValue implements SkyValue {
+ private final Path path;
+
+ /**
+ * If path is a symlink, this will keep track of what the symlink actually points to (for
+ * checking equality).
+ */
+ private final FileValue details;
+
+ public RepositoryValue(Path path, FileValue repositoryDirectory) {
+ this.path = path;
+ this.details = repositoryDirectory;
+ }
+
+ /**
+ * Returns the path to the directory containing the repository's contents. This directory is
+ * guaranteed to exist. It may contain a full Bazel repository (with a WORKSPACE file,
+ * directories, and BUILD files) or simply contain a file (or set of files) for, say, a jar from
+ * Maven.
+ */
+ public Path getPath() {
+ return path;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+
+ if (other instanceof RepositoryValue) {
+ RepositoryValue otherValue = (RepositoryValue) other;
+ return path.equals(otherValue.path) && details.equals(otherValue.details);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(path, details);
+ }
+
+ /**
+ * Creates a key from the given repository name.
+ */
+ public static SkyKey key(RepositoryName repository) {
+ return new SkyKey(SkyFunctions.REPOSITORY, repository);
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SequencedSkyframeExecutor.java b/src/main/java/com/google/devtools/build/lib/skyframe/SequencedSkyframeExecutor.java
new file mode 100644
index 0000000..e547166
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SequencedSkyframeExecutor.java
@@ -0,0 +1,580 @@
+// 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.annotations.VisibleForTesting;
+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.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSetMultimap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.analysis.BuildView;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.WorkspaceStatusAction;
+import com.google.devtools.build.lib.analysis.WorkspaceStatusAction.Factory;
+import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoFactory;
+import com.google.devtools.build.lib.events.Reporter;
+import com.google.devtools.build.lib.packages.Package;
+import com.google.devtools.build.lib.packages.PackageFactory;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.packages.Preprocessor;
+import com.google.devtools.build.lib.pkgcache.PackageCacheOptions;
+import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
+import com.google.devtools.build.lib.util.AbruptExitException;
+import com.google.devtools.build.lib.util.Pair;
+import com.google.devtools.build.lib.util.ResourceUsage;
+import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor;
+import com.google.devtools.build.lib.vfs.ModifiedFileSet;
+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.BuildDriver;
+import com.google.devtools.build.skyframe.Differencer;
+import com.google.devtools.build.skyframe.ImmutableDiff;
+import com.google.devtools.build.skyframe.InMemoryMemoizingEvaluator;
+import com.google.devtools.build.skyframe.Injectable;
+import com.google.devtools.build.skyframe.MemoizingEvaluator.EvaluatorSupplier;
+import com.google.devtools.build.skyframe.RecordingDifferencer;
+import com.google.devtools.build.skyframe.SequentialBuildDriver;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionName;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.Callable;
+
+/**
+ * A SkyframeExecutor that implicitly assumes that builds can be done incrementally from the most
+ * recent build. In other words, builds are "sequenced".
+ */
+public final class SequencedSkyframeExecutor extends SkyframeExecutor {
+ /** Lower limit for number of loaded packages to consider clearing CT values. */
+ private int valueCacheEvictionLimit = -1;
+
+ /** Union of labels of loaded packages since the last eviction of CT values. */
+ private Set<PackageIdentifier> allLoadedPackages = ImmutableSet.of();
+ private boolean lastAnalysisDiscarded = false;
+
+ // Can only be set once (to false) over the lifetime of this object. If false, the graph will not
+ // store edges, saving memory but making incremental builds impossible.
+ private boolean keepGraphEdges = true;
+
+ private RecordingDifferencer recordingDiffer;
+ private final DiffAwarenessManager diffAwarenessManager;
+
+ private SequencedSkyframeExecutor(Reporter reporter, EvaluatorSupplier evaluatorSupplier,
+ PackageFactory pkgFactory, TimestampGranularityMonitor tsgm,
+ BlazeDirectories directories, Factory workspaceStatusActionFactory,
+ ImmutableList<BuildInfoFactory> buildInfoFactories,
+ Set<Path> immutableDirectories,
+ Iterable<? extends DiffAwareness.Factory> diffAwarenessFactories,
+ Predicate<PathFragment> allowedMissingInputs,
+ Preprocessor.Factory.Supplier preprocessorFactorySupplier,
+ ImmutableMap<SkyFunctionName, SkyFunction> extraSkyFunctions,
+ ImmutableList<PrecomputedValue.Injected> extraPrecomputedValues) {
+ super(reporter, evaluatorSupplier, pkgFactory, tsgm, directories,
+ workspaceStatusActionFactory, buildInfoFactories, immutableDirectories,
+ allowedMissingInputs, preprocessorFactorySupplier,
+ extraSkyFunctions, extraPrecomputedValues);
+ this.diffAwarenessManager = new DiffAwarenessManager(diffAwarenessFactories, reporter);
+ }
+
+ private SequencedSkyframeExecutor(Reporter reporter, PackageFactory pkgFactory,
+ TimestampGranularityMonitor tsgm, BlazeDirectories directories,
+ Factory workspaceStatusActionFactory,
+ ImmutableList<BuildInfoFactory> buildInfoFactories,
+ Set<Path> immutableDirectories,
+ Iterable<? extends DiffAwareness.Factory> diffAwarenessFactories,
+ Predicate<PathFragment> allowedMissingInputs,
+ Preprocessor.Factory.Supplier preprocessorFactorySupplier,
+ ImmutableMap<SkyFunctionName, SkyFunction> extraSkyFunctions,
+ ImmutableList<PrecomputedValue.Injected> extraPrecomputedValues) {
+ this(reporter, InMemoryMemoizingEvaluator.SUPPLIER, pkgFactory, tsgm,
+ directories, workspaceStatusActionFactory, buildInfoFactories, immutableDirectories,
+ diffAwarenessFactories, allowedMissingInputs, preprocessorFactorySupplier,
+ extraSkyFunctions, extraPrecomputedValues);
+ }
+
+ private static SequencedSkyframeExecutor create(Reporter reporter,
+ EvaluatorSupplier evaluatorSupplier, PackageFactory pkgFactory,
+ TimestampGranularityMonitor tsgm, BlazeDirectories directories,
+ Factory workspaceStatusActionFactory, ImmutableList<BuildInfoFactory> buildInfoFactories,
+ Set<Path> immutableDirectories,
+ Iterable<? extends DiffAwareness.Factory> diffAwarenessFactories,
+ Predicate<PathFragment> allowedMissingInputs,
+ Preprocessor.Factory.Supplier preprocessorFactorySupplier,
+ ImmutableMap<SkyFunctionName, SkyFunction> extraSkyFunctions,
+ ImmutableList<PrecomputedValue.Injected> extraPrecomputedValues) {
+ SequencedSkyframeExecutor skyframeExecutor = new SequencedSkyframeExecutor(reporter,
+ evaluatorSupplier, pkgFactory, tsgm, directories, workspaceStatusActionFactory,
+ buildInfoFactories, immutableDirectories, diffAwarenessFactories, allowedMissingInputs,
+ preprocessorFactorySupplier,
+ extraSkyFunctions, extraPrecomputedValues);
+ skyframeExecutor.init();
+ return skyframeExecutor;
+ }
+
+ public static SequencedSkyframeExecutor create(Reporter reporter, PackageFactory pkgFactory,
+ TimestampGranularityMonitor tsgm, BlazeDirectories directories,
+ Factory workspaceStatusActionFactory,
+ ImmutableList<BuildInfoFactory> buildInfoFactories,
+ Set<Path> immutableDirectories,
+ Iterable<? extends DiffAwareness.Factory> diffAwarenessFactories,
+ Predicate<PathFragment> allowedMissingInputs,
+ Preprocessor.Factory.Supplier preprocessorFactorySupplier,
+ ImmutableMap<SkyFunctionName, SkyFunction> extraSkyFunctions,
+ ImmutableList<PrecomputedValue.Injected> extraPrecomputedValues) {
+ return create(reporter, InMemoryMemoizingEvaluator.SUPPLIER, pkgFactory, tsgm,
+ directories, workspaceStatusActionFactory, buildInfoFactories, immutableDirectories,
+ diffAwarenessFactories, allowedMissingInputs, preprocessorFactorySupplier,
+ extraSkyFunctions, extraPrecomputedValues);
+ }
+
+ @VisibleForTesting
+ public static SequencedSkyframeExecutor create(Reporter reporter, PackageFactory pkgFactory,
+ TimestampGranularityMonitor tsgm, BlazeDirectories directories,
+ WorkspaceStatusAction.Factory workspaceStatusActionFactory,
+ ImmutableList<BuildInfoFactory> buildInfoFactories,
+ Set<Path> immutableDirectories,
+ Iterable<? extends DiffAwareness.Factory> diffAwarenessFactories) {
+ return create(reporter, pkgFactory, tsgm, directories, workspaceStatusActionFactory,
+ buildInfoFactories, immutableDirectories, diffAwarenessFactories,
+ Predicates.<PathFragment>alwaysFalse(),
+ Preprocessor.Factory.Supplier.NullSupplier.INSTANCE,
+ ImmutableMap.<SkyFunctionName, SkyFunction>of(),
+ ImmutableList.<PrecomputedValue.Injected>of());
+ }
+
+ @Override
+ protected BuildDriver newBuildDriver() {
+ return new SequentialBuildDriver(memoizingEvaluator);
+ }
+
+ @Override
+ protected void init() {
+ // Note that we need to set recordingDiffer first since SkyframeExecutor#init calls
+ // SkyframeExecutor#evaluatorDiffer.
+ recordingDiffer = new RecordingDifferencer();
+ super.init();
+ }
+
+ @Override
+ public void resetEvaluator() {
+ super.resetEvaluator();
+ diffAwarenessManager.reset();
+ }
+
+ @Override
+ protected Differencer evaluatorDiffer() {
+ return recordingDiffer;
+ }
+
+ @Override
+ protected Injectable injectable() {
+ return recordingDiffer;
+ }
+
+ @VisibleForTesting
+ public RecordingDifferencer getDifferencerForTesting() {
+ return recordingDiffer;
+ }
+
+ @Override
+ public void sync(PackageCacheOptions packageCacheOptions, Path workingDirectory,
+ String defaultsPackageContents, UUID commandId)
+ throws InterruptedException, AbruptExitException {
+ this.valueCacheEvictionLimit = packageCacheOptions.minLoadedPkgCountForCtNodeEviction;
+ super.sync(packageCacheOptions, workingDirectory, defaultsPackageContents, commandId);
+ handleDiffs();
+ }
+
+ /**
+ * The value types whose builders have direct access to the package locator, rather than accessing
+ * it via an explicit Skyframe dependency. They need to be invalidated if the package locator
+ * changes.
+ */
+ private static final Set<SkyFunctionName> PACKAGE_LOCATOR_DEPENDENT_VALUES = ImmutableSet.of(
+ SkyFunctions.FILE_STATE,
+ SkyFunctions.FILE,
+ SkyFunctions.DIRECTORY_LISTING_STATE,
+ SkyFunctions.TARGET_PATTERN,
+ SkyFunctions.WORKSPACE_FILE);
+
+ @Override
+ protected void onNewPackageLocator(PathPackageLocator oldLocator, PathPackageLocator pkgLocator) {
+ invalidate(SkyFunctionName.functionIsIn(PACKAGE_LOCATOR_DEPENDENT_VALUES));
+ }
+
+ @Override
+ protected void invalidate(Predicate<SkyKey> pred) {
+ recordingDiffer.invalidate(Iterables.filter(memoizingEvaluator.getValues().keySet(), pred));
+ }
+
+ private void invalidateDeletedPackages(Iterable<String> deletedPackages) {
+ ArrayList<SkyKey> packagesToInvalidate = Lists.newArrayList();
+ for (String deletedPackage : deletedPackages) {
+ PathFragment pathFragment = new PathFragment(deletedPackage);
+ packagesToInvalidate.add(PackageLookupValue.key(pathFragment));
+ }
+ recordingDiffer.invalidate(packagesToInvalidate);
+ }
+
+ /**
+ * Sets the packages that should be treated as deleted and ignored.
+ */
+ @Override
+ @VisibleForTesting // productionVisibility = Visibility.PRIVATE
+ public void setDeletedPackages(Iterable<String> pkgs) {
+ // Invalidate the old deletedPackages as they may exist now.
+ invalidateDeletedPackages(deletedPackages.get());
+ deletedPackages.set(ImmutableSet.copyOf(pkgs));
+ // Invalidate the new deletedPackages as we need to pretend that they don't exist now.
+ invalidateDeletedPackages(deletedPackages.get());
+ }
+
+ /**
+ * Uses diff awareness on all the package paths to invalidate changed files.
+ */
+ @VisibleForTesting
+ public void handleDiffs() throws InterruptedException {
+ if (lastAnalysisDiscarded) {
+ // Values were cleared last build, but they couldn't be deleted because they were needed for
+ // the execution phase. We can delete them now.
+ dropConfiguredTargetsNow();
+ lastAnalysisDiscarded = false;
+ }
+ modifiedFiles = 0;
+ Map<Path, DiffAwarenessManager.ProcessableModifiedFileSet> modifiedFilesByPathEntry =
+ Maps.newHashMap();
+ Set<Pair<Path, DiffAwarenessManager.ProcessableModifiedFileSet>>
+ pathEntriesWithoutDiffInformation = Sets.newHashSet();
+ for (Path pathEntry : pkgLocator.get().getPathEntries()) {
+ DiffAwarenessManager.ProcessableModifiedFileSet modifiedFileSet =
+ diffAwarenessManager.getDiff(pathEntry);
+ if (modifiedFileSet.getModifiedFileSet().treatEverythingAsModified()) {
+ pathEntriesWithoutDiffInformation.add(Pair.of(pathEntry, modifiedFileSet));
+ } else {
+ modifiedFilesByPathEntry.put(pathEntry, modifiedFileSet);
+ }
+ }
+ handleDiffsWithCompleteDiffInformation(modifiedFilesByPathEntry);
+ handleDiffsWithMissingDiffInformation(pathEntriesWithoutDiffInformation);
+ }
+
+ /**
+ * Invalidates files under path entries whose corresponding {@link DiffAwareness} gave an exact
+ * diff. Removes entries from the given map as they are processed. All of the files need to be
+ * invalidated, so the map should be empty upon completion of this function.
+ */
+ private void handleDiffsWithCompleteDiffInformation(
+ Map<Path, DiffAwarenessManager.ProcessableModifiedFileSet> modifiedFilesByPathEntry) {
+ // It's important that the below code be uninterruptible, since we already promised to
+ // invalidate these files.
+ for (Path pathEntry : ImmutableSet.copyOf(modifiedFilesByPathEntry.keySet())) {
+ DiffAwarenessManager.ProcessableModifiedFileSet processableModifiedFileSet =
+ modifiedFilesByPathEntry.get(pathEntry);
+ ModifiedFileSet modifiedFileSet = processableModifiedFileSet.getModifiedFileSet();
+ Preconditions.checkState(!modifiedFileSet.treatEverythingAsModified(), pathEntry);
+ Iterable<SkyKey> dirtyValues = getSkyKeysPotentiallyAffected(
+ modifiedFileSet.modifiedSourceFiles(), pathEntry);
+ handleChangedFiles(new ImmutableDiff(dirtyValues, ImmutableMap.<SkyKey, SkyValue>of()));
+ processableModifiedFileSet.markProcessed();
+ }
+ }
+
+ /**
+ * Finds and invalidates changed files under path entries whose corresponding
+ * {@link DiffAwareness} said all files may have been modified.
+ */
+ private void handleDiffsWithMissingDiffInformation(
+ Set<Pair<Path, DiffAwarenessManager.ProcessableModifiedFileSet>>
+ pathEntriesWithoutDiffInformation) throws InterruptedException {
+ if (pathEntriesWithoutDiffInformation.isEmpty()) {
+ return;
+ }
+ // Before running the FilesystemValueChecker, ensure that all values marked for invalidation
+ // have actually been invalidated (recall that invalidation happens at the beginning of the
+ // next evaluate() call), because checking those is a waste of time.
+ buildDriver.evaluate(ImmutableList.<SkyKey>of(), false,
+ DEFAULT_THREAD_COUNT, reporter);
+ FilesystemValueChecker fsnc = new FilesystemValueChecker(memoizingEvaluator, tsgm);
+ // We need to manually check for changes to known files. This entails finding all dirty file
+ // system values under package roots for which we don't have diff information. If at least
+ // one path entry doesn't have diff information, then we're going to have to iterate over
+ // the skyframe values at least once no matter what so we might as well do so now and avoid
+ // doing so more than once.
+ Iterable<SkyKey> filesystemSkyKeys = fsnc.getFilesystemSkyKeys();
+ // Partition by package path entry.
+ Multimap<Path, SkyKey> skyKeysByPathEntry = partitionSkyKeysByPackagePathEntry(
+ ImmutableSet.copyOf(pkgLocator.get().getPathEntries()), filesystemSkyKeys);
+ // Contains all file system values that we need to check for dirtiness.
+ List<Iterable<SkyKey>> valuesToCheckManually = Lists.newArrayList();
+ for (Pair<Path, DiffAwarenessManager.ProcessableModifiedFileSet> pair :
+ pathEntriesWithoutDiffInformation) {
+ Path pathEntry = pair.getFirst();
+ valuesToCheckManually.add(skyKeysByPathEntry.get(pathEntry));
+ }
+ Differencer.Diff diff = fsnc.getDirtyFilesystemValues(Iterables.concat(valuesToCheckManually));
+ handleChangedFiles(diff);
+ for (Pair<Path, DiffAwarenessManager.ProcessableModifiedFileSet> pair :
+ pathEntriesWithoutDiffInformation) {
+ DiffAwarenessManager.ProcessableModifiedFileSet processableModifiedFileSet = pair.getSecond();
+ processableModifiedFileSet.markProcessed();
+ }
+ }
+
+ /**
+ * Partitions the given filesystem values based on which package path root they are under.
+ * Returns a {@link Multimap} {@code m} such that {@code m.containsEntry(k, pe)} is true for
+ * each filesystem valuekey {@code k} under a package path root {@code pe}. Note that values not
+ * under a package path root are not present in the returned {@link Multimap}; these values are
+ * unconditionally checked for changes on each incremental build.
+ */
+ private static Multimap<Path, SkyKey> partitionSkyKeysByPackagePathEntry(
+ Set<Path> pkgRoots, Iterable<SkyKey> filesystemSkyKeys) {
+ ImmutableSetMultimap.Builder<Path, SkyKey> multimapBuilder =
+ ImmutableSetMultimap.builder();
+ for (SkyKey key : filesystemSkyKeys) {
+ Preconditions.checkState(key.functionName() == SkyFunctions.FILE_STATE
+ || key.functionName() == SkyFunctions.DIRECTORY_LISTING_STATE, key);
+ Path root = ((RootedPath) key.argument()).getRoot();
+ if (pkgRoots.contains(root)) {
+ multimapBuilder.put(root, key);
+ }
+ // We don't need to worry about FileStateValues for external files because they have a
+ // dependency on the build_id and so they get invalidated each build.
+ }
+ return multimapBuilder.build();
+ }
+
+ private void handleChangedFiles(Differencer.Diff diff) {
+ recordingDiffer.invalidate(diff.changedKeysWithoutNewValues());
+ recordingDiffer.inject(diff.changedKeysWithNewValues());
+ modifiedFiles += getNumberOfModifiedFiles(diff.changedKeysWithoutNewValues());
+ modifiedFiles += getNumberOfModifiedFiles(diff.changedKeysWithNewValues().keySet());
+ incrementalBuildMonitor.accrue(diff.changedKeysWithoutNewValues());
+ incrementalBuildMonitor.accrue(diff.changedKeysWithNewValues().keySet());
+ }
+
+ private static int getNumberOfModifiedFiles(Iterable<SkyKey> modifiedValues) {
+ // We are searching only for changed files, DirectoryListingValues don't depend on
+ // child values, that's why they are invalidated separately
+ return Iterables.size(Iterables.filter(modifiedValues,
+ SkyFunctionName.functionIs(SkyFunctions.FILE_STATE)));
+ }
+
+ @Override
+ public void decideKeepIncrementalState(boolean batch, BuildView.Options viewOptions) {
+ Preconditions.checkState(!active);
+ if (viewOptions == null) {
+ // Some blaze commands don't include the view options. Don't bother with them.
+ return;
+ }
+ if (batch && viewOptions.keepGoing && viewOptions.discardAnalysisCache) {
+ Preconditions.checkState(keepGraphEdges, "May only be called once if successful");
+ keepGraphEdges = false;
+ // Graph will be recreated on next sync.
+ }
+ }
+
+ @Override
+ public boolean hasIncrementalState() {
+ // TODO(bazel-team): Combine this method with clearSkyframeRelevantCaches() once legacy
+ // execution is removed [skyframe-execution].
+ return keepGraphEdges;
+ }
+
+ @Override
+ public void invalidateFilesUnderPathForTesting(ModifiedFileSet modifiedFileSet, Path pathEntry)
+ throws InterruptedException {
+ if (lastAnalysisDiscarded) {
+ // Values were cleared last build, but they couldn't be deleted because they were needed for
+ // the execution phase. We can delete them now.
+ dropConfiguredTargetsNow();
+ lastAnalysisDiscarded = false;
+ }
+ Iterable<SkyKey> keys;
+ if (modifiedFileSet.treatEverythingAsModified()) {
+ Differencer.Diff diff =
+ new FilesystemValueChecker(memoizingEvaluator, tsgm).getDirtyFilesystemSkyKeys();
+ keys = diff.changedKeysWithoutNewValues();
+ recordingDiffer.inject(diff.changedKeysWithNewValues());
+ } else {
+ keys = getSkyKeysPotentiallyAffected(modifiedFileSet.modifiedSourceFiles(), pathEntry);
+ }
+ syscalls.set(new PerBuildSyscallCache());
+ recordingDiffer.invalidate(keys);
+ // Blaze invalidates transient errors on every build.
+ invalidateTransientErrors();
+ }
+
+ @Override
+ public void invalidateTransientErrors() {
+ checkActive();
+ recordingDiffer.invalidateTransientErrors();
+ }
+
+ @Override
+ protected void invalidateDirtyActions(Iterable<SkyKey> dirtyActionValues) {
+ recordingDiffer.invalidate(dirtyActionValues);
+ }
+
+ /**
+ * Save memory by removing references to configured targets and actions in Skyframe.
+ *
+ * <p>These values must be recreated on subsequent builds. We do not clear the top-level target
+ * values, since their configured targets are needed for the target completion middleman values.
+ *
+ * <p>The values are not deleted during this method call, because they are needed for the
+ * execution phase. Instead, their data is cleared. The next build will delete the values (and
+ * recreate them if necessary).
+ */
+ private void discardAnalysisCache(Collection<ConfiguredTarget> topLevelTargets) {
+ lastAnalysisDiscarded = true;
+ for (Map.Entry<SkyKey, SkyValue> entry : memoizingEvaluator.getValues().entrySet()) {
+ if (!entry.getKey().functionName().equals(SkyFunctions.CONFIGURED_TARGET)) {
+ continue;
+ }
+ ConfiguredTargetValue ctValue = (ConfiguredTargetValue) entry.getValue();
+ // ctValue may be null if target was not successfully analyzed.
+ if (ctValue != null && !topLevelTargets.contains(ctValue.getConfiguredTarget())) {
+ ctValue.clear();
+ }
+ }
+ }
+
+ @Override
+ public void clearAnalysisCache(Collection<ConfiguredTarget> topLevelTargets) {
+ discardAnalysisCache(topLevelTargets);
+ }
+
+ @Override
+ public void dropConfiguredTargets() {
+ if (skyframeBuildView != null) {
+ skyframeBuildView.clearInvalidatedConfiguredTargets();
+ }
+ memoizingEvaluator.delete(
+ // We delete any value that can hold an action -- all subclasses of ActionLookupValue -- as
+ // well as ActionExecutionValues, since they do not depend on ActionLookupValues.
+ SkyFunctionName.functionIsIn(ImmutableSet.of(
+ SkyFunctions.CONFIGURED_TARGET,
+ SkyFunctions.ACTION_LOOKUP,
+ SkyFunctions.BUILD_INFO,
+ SkyFunctions.TARGET_COMPLETION,
+ SkyFunctions.BUILD_INFO_COLLECTION,
+ SkyFunctions.ACTION_EXECUTION))
+ );
+ }
+
+ /**
+ * Deletes all ConfiguredTarget values from the Skyframe cache.
+ *
+ * <p>After the execution of this method all invalidated and marked for deletion values
+ * (and the values depending on them) will be deleted from the cache.
+ *
+ * <p>WARNING: Note that a call to this method leaves legacy data inconsistent with Skyframe.
+ * The next build should clear the legacy caches.
+ */
+ private void dropConfiguredTargetsNow() {
+ dropConfiguredTargets();
+ // Run the invalidator to actually delete the values.
+ try {
+ progressReceiver.ignoreInvalidations = true;
+ callUninterruptibly(new Callable<Void>() {
+ @Override
+ public Void call() throws InterruptedException {
+ buildDriver.evaluate(ImmutableList.<SkyKey>of(), false,
+ ResourceUsage.getAvailableProcessors(), reporter);
+ return null;
+ }
+ });
+ } catch (Exception e) {
+ throw new IllegalStateException(e);
+ } finally {
+ progressReceiver.ignoreInvalidations = false;
+ }
+ }
+
+ /**
+ * Returns true if the old set of Packages is a subset or superset of the new one.
+ *
+ * <p>Compares the names of packages instead of the Package objects themselves (Package doesn't
+ * yet override #equals). Since packages store their names as a String rather than a Label, it's
+ * easier to use strings here.
+ */
+ @VisibleForTesting
+ static boolean isBuildSubsetOrSupersetOfPreviousBuild(Set<PackageIdentifier> oldPackages,
+ Set<PackageIdentifier> newPackages) {
+ if (newPackages.size() <= oldPackages.size()) {
+ return Sets.difference(newPackages, oldPackages).isEmpty();
+ } else if (oldPackages.size() < newPackages.size()) {
+ // No need to check for <= here, since the first branch does that already.
+ // If size(A) = size(B), then then A\B = 0 iff B\A = 0
+ return Sets.difference(oldPackages, newPackages).isEmpty();
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public void updateLoadedPackageSet(Set<PackageIdentifier> loadedPackages) {
+ Preconditions.checkState(valueCacheEvictionLimit >= 0,
+ "should have called setMinLoadedPkgCountForCtValueEviction earlier");
+
+ // Make a copy to avoid nesting SetView objects. It also computes size(), which we need below.
+ Set<PackageIdentifier> union = ImmutableSet.copyOf(
+ Sets.union(allLoadedPackages, loadedPackages));
+
+ if (union.size() < valueCacheEvictionLimit
+ || isBuildSubsetOrSupersetOfPreviousBuild(allLoadedPackages, loadedPackages)) {
+ allLoadedPackages = union;
+ } else {
+ dropConfiguredTargets();
+ allLoadedPackages = loadedPackages;
+ }
+ }
+
+ @Override
+ public void deleteOldNodes(long versionWindowForDirtyGc) {
+ // TODO(bazel-team): perhaps we should come up with a separate GC class dedicated to maintaining
+ // value garbage. If we ever do so, this logic should be moved there.
+ memoizingEvaluator.deleteDirty(versionWindowForDirtyGc);
+ }
+
+ @Override
+ public void dumpPackages(PrintStream out) {
+ Iterable<SkyKey> packageSkyKeys = Iterables.filter(memoizingEvaluator.getValues().keySet(),
+ SkyFunctions.isSkyFunction(SkyFunctions.PACKAGE));
+ out.println(Iterables.size(packageSkyKeys) + " packages");
+ for (SkyKey packageSkyKey : packageSkyKeys) {
+ Package pkg = ((PackageValue) memoizingEvaluator.getValues().get(packageSkyKey)).getPackage();
+ pkg.dump(out);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SequencedSkyframeExecutorFactory.java b/src/main/java/com/google/devtools/build/lib/skyframe/SequencedSkyframeExecutorFactory.java
new file mode 100644
index 0000000..7e277a7
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SequencedSkyframeExecutorFactory.java
@@ -0,0 +1,53 @@
+// 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.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.analysis.WorkspaceStatusAction.Factory;
+import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoFactory;
+import com.google.devtools.build.lib.events.Reporter;
+import com.google.devtools.build.lib.packages.PackageFactory;
+import com.google.devtools.build.lib.packages.Preprocessor;
+import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionName;
+
+import java.util.Set;
+
+/**
+ * A factory of SkyframeExecutors that returns SequencedSkyframeExecutor.
+ */
+public class SequencedSkyframeExecutorFactory implements SkyframeExecutorFactory {
+
+ @Override
+ public SkyframeExecutor create(Reporter reporter, PackageFactory pkgFactory,
+ TimestampGranularityMonitor tsgm, BlazeDirectories directories,
+ Factory workspaceStatusActionFactory, ImmutableList<BuildInfoFactory> buildInfoFactories,
+ Set<Path> immutableDirectories,
+ Iterable<? extends DiffAwareness.Factory> diffAwarenessFactories,
+ Predicate<PathFragment> allowedMissingInputs,
+ Preprocessor.Factory.Supplier preprocessorFactorySupplier,
+ ImmutableMap<SkyFunctionName, SkyFunction> extraSkyFunctions,
+ ImmutableList<PrecomputedValue.Injected> extraPrecomputedValues) {
+ return SequencedSkyframeExecutor.create(reporter, pkgFactory, tsgm, directories,
+ workspaceStatusActionFactory, buildInfoFactories, immutableDirectories,
+ diffAwarenessFactories, allowedMissingInputs, preprocessorFactorySupplier,
+ extraSkyFunctions, extraPrecomputedValues);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyFunctions.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyFunctions.java
new file mode 100644
index 0000000..316d27d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyFunctions.java
@@ -0,0 +1,81 @@
+// 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.Predicate;
+import com.google.devtools.build.skyframe.SkyFunctionName;
+import com.google.devtools.build.skyframe.SkyKey;
+
+/**
+ * Value types in Skyframe.
+ */
+public final class SkyFunctions {
+ public static final SkyFunctionName PRECOMPUTED = new SkyFunctionName("PRECOMPUTED", false);
+ public static final SkyFunctionName FILE_STATE = new SkyFunctionName("FILE_STATE", false);
+ public static final SkyFunctionName DIRECTORY_LISTING_STATE =
+ new SkyFunctionName("DIRECTORY_LISTING_STATE", false);
+ public static final SkyFunctionName FILE_SYMLINK_CYCLE_UNIQUENESS =
+ SkyFunctionName.computed("FILE_SYMLINK_CYCLE_UNIQUENESS_NODE");
+ public static final SkyFunctionName FILE = SkyFunctionName.computed("FILE");
+ public static final SkyFunctionName DIRECTORY_LISTING =
+ SkyFunctionName.computed("DIRECTORY_LISTING");
+ public static final SkyFunctionName PACKAGE_LOOKUP = SkyFunctionName.computed("PACKAGE_LOOKUP");
+ public static final SkyFunctionName CONTAINING_PACKAGE_LOOKUP =
+ SkyFunctionName.computed("CONTAINING_PACKAGE_LOOKUP");
+ public static final SkyFunctionName AST_FILE_LOOKUP = SkyFunctionName.computed("AST_FILE_LOOKUP");
+ public static final SkyFunctionName SKYLARK_IMPORTS_LOOKUP =
+ SkyFunctionName.computed("SKYLARK_IMPORTS_LOOKUP");
+ public static final SkyFunctionName GLOB = SkyFunctionName.computed("GLOB");
+ public static final SkyFunctionName PACKAGE = SkyFunctionName.computed("PACKAGE");
+ public static final SkyFunctionName TARGET_MARKER = SkyFunctionName.computed("TARGET_MARKER");
+ public static final SkyFunctionName TARGET_PATTERN = SkyFunctionName.computed("TARGET_PATTERN");
+ public static final SkyFunctionName RECURSIVE_PKG = SkyFunctionName.computed("RECURSIVE_PKG");
+ public static final SkyFunctionName TRANSITIVE_TARGET =
+ SkyFunctionName.computed("TRANSITIVE_TARGET");
+ public static final SkyFunctionName CONFIGURED_TARGET =
+ SkyFunctionName.computed("CONFIGURED_TARGET");
+ public static final SkyFunctionName ASPECT = SkyFunctionName.computed("ASPECT");
+ public static final SkyFunctionName POST_CONFIGURED_TARGET =
+ SkyFunctionName.computed("POST_CONFIGURED_TARGET");
+ public static final SkyFunctionName TARGET_COMPLETION =
+ SkyFunctionName.computed("TARGET_COMPLETION");
+ public static final SkyFunctionName TEST_COMPLETION =
+ SkyFunctionName.computed("TEST_COMPLETION");
+ public static final SkyFunctionName CONFIGURATION_FRAGMENT =
+ SkyFunctionName.computed("CONFIGURATION_FRAGMENT");
+ public static final SkyFunctionName CONFIGURATION_COLLECTION =
+ SkyFunctionName.computed("CONFIGURATION_COLLECTION");
+ public static final SkyFunctionName ARTIFACT = SkyFunctionName.computed("ARTIFACT");
+ public static final SkyFunctionName ACTION_EXECUTION =
+ SkyFunctionName.computed("ACTION_EXECUTION");
+ public static final SkyFunctionName ACTION_LOOKUP = SkyFunctionName.computed("ACTION_LOOKUP");
+ public static final SkyFunctionName RECURSIVE_FILESYSTEM_TRAVERSAL =
+ SkyFunctionName.computed("RECURSIVE_DIRECTORY_TRAVERSAL");
+ public static final SkyFunctionName FILESET_ENTRY = SkyFunctionName.computed("FILESET_ENTRY");
+ public static final SkyFunctionName BUILD_INFO_COLLECTION =
+ SkyFunctionName.computed("BUILD_INFO_COLLECTION");
+ public static final SkyFunctionName BUILD_INFO = SkyFunctionName.computed("BUILD_INFO");
+ public static final SkyFunctionName WORKSPACE_FILE = SkyFunctionName.computed("WORKSPACE_FILE");
+ public static final SkyFunctionName COVERAGE_REPORT = SkyFunctionName.computed("COVERAGE_REPORT");
+ public static final SkyFunctionName REPOSITORY = SkyFunctionName.computed("REPOSITORY");
+
+ public static Predicate<SkyKey> isSkyFunction(final SkyFunctionName functionName) {
+ return new Predicate<SkyKey>() {
+ @Override
+ public boolean apply(SkyKey key) {
+ return key.functionName() == functionName;
+ }
+ };
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeActionExecutor.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeActionExecutor.java
new file mode 100644
index 0000000..bd591e1
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeActionExecutor.java
@@ -0,0 +1,1152 @@
+// 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 static com.google.devtools.build.lib.vfs.FileSystemUtils.createDirectoryAndParents;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import com.google.common.eventbus.EventBus;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.ActionCacheChecker;
+import com.google.devtools.build.lib.actions.ActionCacheChecker.Token;
+import com.google.devtools.build.lib.actions.ActionCompletionEvent;
+import com.google.devtools.build.lib.actions.ActionExecutedEvent;
+import com.google.devtools.build.lib.actions.ActionExecutionContext;
+import com.google.devtools.build.lib.actions.ActionExecutionException;
+import com.google.devtools.build.lib.actions.ActionExecutionStatusReporter;
+import com.google.devtools.build.lib.actions.ActionGraph;
+import com.google.devtools.build.lib.actions.ActionInput;
+import com.google.devtools.build.lib.actions.ActionInputFileCache;
+import com.google.devtools.build.lib.actions.ActionLogBufferPathGenerator;
+import com.google.devtools.build.lib.actions.ActionMiddlemanEvent;
+import com.google.devtools.build.lib.actions.ActionStartedEvent;
+import com.google.devtools.build.lib.actions.Actions;
+import com.google.devtools.build.lib.actions.AlreadyReportedActionExecutionException;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.Artifact.MiddlemanExpander;
+import com.google.devtools.build.lib.actions.ArtifactPrefixConflictException;
+import com.google.devtools.build.lib.actions.CachedActionEvent;
+import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.actions.MapBasedActionGraph;
+import com.google.devtools.build.lib.actions.MutableActionGraph;
+import com.google.devtools.build.lib.actions.MutableActionGraph.ActionConflictException;
+import com.google.devtools.build.lib.actions.NotifyOnActionCacheHit;
+import com.google.devtools.build.lib.actions.ResourceManager;
+import com.google.devtools.build.lib.actions.ResourceSet;
+import com.google.devtools.build.lib.actions.TargetOutOfDateException;
+import com.google.devtools.build.lib.actions.cache.Digest;
+import com.google.devtools.build.lib.actions.cache.DigestUtils;
+import com.google.devtools.build.lib.actions.cache.Metadata;
+import com.google.devtools.build.lib.actions.cache.MetadataHandler;
+import com.google.devtools.build.lib.concurrent.ExecutorShutdownUtil;
+import com.google.devtools.build.lib.concurrent.Sharder;
+import com.google.devtools.build.lib.concurrent.ThrowableRecordingRunnableWrapper;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.Reporter;
+import com.google.devtools.build.lib.profiler.Profiler;
+import com.google.devtools.build.lib.profiler.ProfilerTask;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.Pair;
+import com.google.devtools.build.lib.util.io.FileOutErr;
+import com.google.devtools.build.lib.util.io.OutErr;
+import com.google.devtools.build.lib.vfs.FileStatus;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.Symlinks;
+import com.google.protobuf.ByteString;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ConcurrentNavigableMap;
+import java.util.concurrent.ConcurrentSkipListMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.annotation.Nullable;
+
+/**
+ * Action executor: takes care of preparing an action for execution, executing it, validating that
+ * all output artifacts were created, error reporting, etc.
+ */
+public final class SkyframeActionExecutor {
+ private final Reporter reporter;
+ private final AtomicReference<EventBus> eventBus;
+ private final ResourceManager resourceManager;
+ private Executor executorEngine;
+ private ActionLogBufferPathGenerator actionLogBufferPathGenerator;
+ private ActionCacheChecker actionCacheChecker;
+ private ConcurrentMap<Artifact, Metadata> undeclaredInputsMetadata = new ConcurrentHashMap<>();
+ private final Profiler profiler = Profiler.instance();
+ private boolean explain;
+
+ // We keep track of actions already executed this build in order to avoid executing a shared
+ // action twice. Note that we may still unnecessarily re-execute the action on a subsequent
+ // build: say actions A and B are shared. If A is requested on the first build and then B is
+ // requested on the second build, we will execute B even though its output files are up to date.
+ // However, we will not re-execute A on a subsequent build.
+ // We do not allow the shared action to re-execute in the same build, even after the first
+ // action has finished execution, because a downstream action might be reading the output file
+ // at the same time as the shared action was writing to it.
+ // This map is also used for Actions that try to execute twice because they have discovered
+ // headers -- the SkyFunction tries to declare a dep on the missing headers and has to restart.
+ // We don't want to execute the action again on the second entry to the SkyFunction.
+ // In both cases, we store the already-computed ActionExecutionValue to avoid having to compute it
+ // again.
+ private ConcurrentMap<Artifact, Pair<Action, FutureTask<ActionExecutionValue>>> buildActionMap;
+
+ // Errors found when examining all actions in the graph are stored here, so that they can be
+ // thrown when execution of the action is requested. This field is set during each call to
+ // findAndStoreArtifactConflicts, and is preserved across builds otherwise.
+ private ImmutableMap<Action, ConflictException> badActionMap = ImmutableMap.of();
+ private boolean keepGoing;
+ private boolean hadExecutionError;
+ private ActionInputFileCache perBuildFileCache;
+ private ProgressSupplier progressSupplier;
+ private ActionCompletedReceiver completionReceiver;
+ private final AtomicReference<ActionExecutionStatusReporter> statusReporterRef;
+
+ SkyframeActionExecutor(Reporter reporter, ResourceManager resourceManager,
+ AtomicReference<EventBus> eventBus,
+ AtomicReference<ActionExecutionStatusReporter> statusReporterRef) {
+ this.reporter = reporter;
+ this.resourceManager = resourceManager;
+ this.eventBus = eventBus;
+ this.statusReporterRef = statusReporterRef;
+ }
+
+ /**
+ * A typed union of {@link ActionConflictException}, which indicates two actions that generate
+ * the same {@link Artifact}, and {@link ArtifactPrefixConflictException}, which indicates that
+ * the path of one {@link Artifact} is a prefix of another.
+ */
+ public static class ConflictException extends Exception {
+ @Nullable private final ActionConflictException ace;
+ @Nullable private final ArtifactPrefixConflictException apce;
+
+ public ConflictException(ActionConflictException e) {
+ super(e);
+ this.ace = e;
+ this.apce = null;
+ }
+
+ public ConflictException(ArtifactPrefixConflictException e) {
+ super(e);
+ this.ace = null;
+ this.apce = e;
+ }
+
+ void rethrowTyped() throws ActionConflictException, ArtifactPrefixConflictException {
+ if (ace == null) {
+ throw Preconditions.checkNotNull(apce);
+ }
+ if (apce == null) {
+ throw Preconditions.checkNotNull(ace);
+ }
+ throw new IllegalStateException();
+ }
+ }
+
+ /**
+ * Return the map of mostly recently executed bad actions to their corresponding exception.
+ * See {#findAndStoreArtifactConflicts()}.
+ */
+ public ImmutableMap<Action, ConflictException> badActions() {
+ // TODO(bazel-team): Move badActions() and findAndStoreArtifactConflicts() to SkyframeBuildView
+ // now that it's done in the analysis phase.
+ return badActionMap;
+ }
+
+ /**
+ * Basic implementation of {@link MetadataHandler} that delegates to Skyframe for metadata and
+ * caches missing source artifacts (which must be undeclared inputs: discovered headers) to avoid
+ * excessive filesystem access. The discovered-header cache is available across actions.
+ */
+ // TODO(bazel-team): remove when include scanning is skyframe-native.
+ private static class UndeclaredInputHandler implements MetadataHandler {
+ private final ConcurrentMap<Artifact, Metadata> undeclaredInputsMetadata;
+ private final MetadataHandler perActionHandler;
+
+ UndeclaredInputHandler(MetadataHandler perActionHandler,
+ ConcurrentMap<Artifact, Metadata> undeclaredInputsMetadata) {
+ // Shared across all UndeclaredInputHandlers in this build.
+ this.undeclaredInputsMetadata = undeclaredInputsMetadata;
+ this.perActionHandler = perActionHandler;
+ }
+
+ @Override
+ public Metadata getMetadataMaybe(Artifact artifact) {
+ try {
+ return getMetadata(artifact);
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public Metadata getMetadata(Artifact artifact) throws IOException {
+ Metadata metadata = perActionHandler.getMetadata(artifact);
+ if (metadata != null) {
+ return metadata;
+ }
+ // Skyframe stats all generated artifacts, because either they are outputs of the action being
+ // executed or they are generated files already present in the graph.
+ Preconditions.checkState(artifact.isSourceArtifact(), artifact);
+ metadata = undeclaredInputsMetadata.get(artifact);
+ if (metadata != null) {
+ return metadata;
+ }
+ FileStatus stat = artifact.getPath().stat();
+ if (DigestUtils.useFileDigest(artifact, stat.isFile(), stat.getSize())) {
+ metadata = new Metadata(Preconditions.checkNotNull(
+ DigestUtils.getDigestOrFail(artifact.getPath(), stat.getSize()), artifact));
+ } else {
+ metadata = new Metadata(stat.getLastModifiedTime());
+ }
+ // Cache for other actions that may also include without declaring.
+ Metadata oldMetadata = undeclaredInputsMetadata.put(artifact, metadata);
+ FileAndMetadataCache.checkInconsistentData(artifact, oldMetadata, metadata);
+ return metadata;
+ }
+
+ @Override
+ public void setDigestForVirtualArtifact(Artifact artifact, Digest digest) {
+ perActionHandler.setDigestForVirtualArtifact(artifact, digest);
+ }
+
+ @Override
+ public void injectDigest(ActionInput output, FileStatus statNoFollow, byte[] digest) {
+ perActionHandler.injectDigest(output, statNoFollow, digest);
+ }
+
+ @Override
+ public boolean artifactExists(Artifact artifact) {
+ return perActionHandler.artifactExists(artifact);
+ }
+
+ @Override
+ public boolean isRegularFile(Artifact artifact) {
+ return perActionHandler.isRegularFile(artifact);
+ }
+
+ @Override
+ public boolean isInjected(Artifact artifact) throws IOException {
+ return perActionHandler.isInjected(artifact);
+ }
+
+ @Override
+ public void discardMetadata(Collection<Artifact> artifactList) {
+ // This input handler only caches undeclared inputs, which never need to be discarded
+ // intra-build.
+ perActionHandler.discardMetadata(artifactList);
+ }
+ }
+
+ /**
+ * Find conflicts between generated artifacts. There are two ways to have conflicts. First, if
+ * two (unshareable) actions generate the same output artifact, this will result in an {@link
+ * ActionConflictException}. Second, if one action generates an artifact whose path is a prefix of
+ * another artifact's path, those two artifacts cannot exist simultaneously in the output tree.
+ * This causes an {@link ArtifactPrefixConflictException}. The relevant exceptions are stored in
+ * the executor in {@code badActionMap}, and will be thrown immediately when that action is
+ * executed. Those exceptions persist, so that even if the action is not executed this build, the
+ * first time it is executed, the correct exception will be thrown.
+ *
+ * <p>This method must be called if a new action was added to the graph this build, so
+ * whenever a new configured target was analyzed this build. It is somewhat expensive (~1s
+ * range for a medium build as of 2014), so it should only be called when necessary.
+ *
+ * <p>Conflicts found may not be requested this build, and so we may overzealously throw an error.
+ * For instance, if actions A and B generate the same artifact foo, and the user first requests
+ * A' depending on A, and then in a subsequent build B' depending on B, we will fail the second
+ * build, even though it would have succeeded if it had been the only build. However, since
+ * Skyframe does not know the transitive dependencies of the request, we err on the conservative
+ * side.
+ *
+ * <p>If the user first runs one action on the first build, and on the second build adds a
+ * conflicting action, only the second action's error may be reported (because the first action
+ * will be cached), whereas if both actions were requested for the first time, both errors would
+ * be reported. However, the first time an action is added to the build, we are guaranteed to find
+ * any conflicts it has, since this method will compare it against all other actions. So there is
+ * no sequence of builds that can evade the error.
+ */
+ void findAndStoreArtifactConflicts(Iterable<ActionLookupValue> actionLookupValues)
+ throws InterruptedException {
+ ConcurrentMap<Action, ConflictException> temporaryBadActionMap = new ConcurrentHashMap<>();
+ Pair<ActionGraph, SortedMap<PathFragment, Artifact>> result;
+ result = constructActionGraphAndPathMap(actionLookupValues, temporaryBadActionMap);
+ ActionGraph actionGraph = result.first;
+ SortedMap<PathFragment, Artifact> artifactPathMap = result.second;
+
+ // Report an error for every derived artifact which is a prefix of another.
+ // If x << y << z (where x << y means "y starts with x"), then we only report (x,y), (x,z), but
+ // not (y,z).
+ Iterator<PathFragment> iter = artifactPathMap.keySet().iterator();
+ if (!iter.hasNext()) {
+ // No actions in graph -- currently happens only in tests. Special-cased because .next() call
+ // below is unconditional.
+ this.badActionMap = ImmutableMap.of();
+ return;
+ }
+ for (PathFragment pathJ = iter.next(); iter.hasNext(); ) {
+ // For each comparison, we have a prefix candidate (pathI) and a suffix candidate (pathJ).
+ // At the beginning of the loop, we set pathI to the last suffix candidate, since it has not
+ // yet been tested as a prefix candidate, and then set pathJ to the paths coming after pathI,
+ // until we come to one that does not contain pathI as a prefix. pathI is then verified not to
+ // be the prefix of any path, so we start the next run of the loop.
+ PathFragment pathI = pathJ;
+ // Compare pathI to the paths coming after it.
+ while (iter.hasNext()) {
+ pathJ = iter.next();
+ if (pathJ.startsWith(pathI)) { // prefix conflict.
+ Artifact artifactI = Preconditions.checkNotNull(artifactPathMap.get(pathI), pathI);
+ Artifact artifactJ = Preconditions.checkNotNull(artifactPathMap.get(pathJ), pathJ);
+ Action actionI =
+ Preconditions.checkNotNull(actionGraph.getGeneratingAction(artifactI), artifactI);
+ Action actionJ =
+ Preconditions.checkNotNull(actionGraph.getGeneratingAction(artifactJ), artifactJ);
+ if (actionI.shouldReportPathPrefixConflict(actionJ)) {
+ ArtifactPrefixConflictException exception = new ArtifactPrefixConflictException(pathI,
+ pathJ, actionI.getOwner().getLabel(), actionJ.getOwner().getLabel());
+ temporaryBadActionMap.put(actionI, new ConflictException(exception));
+ temporaryBadActionMap.put(actionJ, new ConflictException(exception));
+ }
+ } else { // pathJ didn't have prefix pathI, so no conflict possible for pathI.
+ break;
+ }
+ }
+ }
+ this.badActionMap = ImmutableMap.copyOf(temporaryBadActionMap);
+ }
+
+ /**
+ * Simultaneously construct an action graph for all the actions in Skyframe and a map from
+ * {@link PathFragment}s to their respective {@link Artifact}s. We do this in a threadpool to save
+ * around 1.5 seconds on a mid-sized build versus a single-threaded operation.
+ */
+ private static Pair<ActionGraph, SortedMap<PathFragment, Artifact>>
+ constructActionGraphAndPathMap(
+ Iterable<ActionLookupValue> values,
+ ConcurrentMap<Action, ConflictException> badActionMap) throws InterruptedException {
+ MutableActionGraph actionGraph = new MapBasedActionGraph();
+ ConcurrentNavigableMap<PathFragment, Artifact> artifactPathMap = new ConcurrentSkipListMap<>();
+ // Action graph construction is CPU-bound.
+ int numJobs = Runtime.getRuntime().availableProcessors();
+ // No great reason for expecting 5000 action lookup values, but not worth counting size of
+ // values.
+ Sharder<ActionLookupValue> actionShards = new Sharder<>(numJobs, 5000);
+ for (ActionLookupValue value : values) {
+ actionShards.add(value);
+ }
+
+ ThrowableRecordingRunnableWrapper wrapper = new ThrowableRecordingRunnableWrapper(
+ "SkyframeActionExecutor#constructActionGraphAndPathMap");
+
+ ExecutorService executor = Executors.newFixedThreadPool(
+ numJobs,
+ new ThreadFactoryBuilder().setNameFormat("ActionLookupValue Processor %d").build());
+ for (List<ActionLookupValue> shard : actionShards) {
+ executor.execute(
+ wrapper.wrap(actionRegistration(shard, actionGraph, artifactPathMap, badActionMap)));
+ }
+ boolean interrupted = ExecutorShutdownUtil.interruptibleShutdown(executor);
+ Throwables.propagateIfPossible(wrapper.getFirstThrownError());
+ if (interrupted) {
+ throw new InterruptedException();
+ }
+ return Pair.<ActionGraph, SortedMap<PathFragment, Artifact>>of(actionGraph, artifactPathMap);
+ }
+
+ private static Runnable actionRegistration(
+ final List<ActionLookupValue> values,
+ final MutableActionGraph actionGraph,
+ final ConcurrentMap<PathFragment, Artifact> artifactPathMap,
+ final ConcurrentMap<Action, ConflictException> badActionMap) {
+ return new Runnable() {
+ @Override
+ public void run() {
+ for (ActionLookupValue value : values) {
+ Set<Action> registeredActions = new HashSet<>();
+ for (Map.Entry<Artifact, Action> entry : value.getMapForConsistencyCheck().entrySet()) {
+ Action action = entry.getValue();
+ // We have an entry for each <action, artifact> pair. Only try to register each action
+ // once.
+ if (registeredActions.add(action)) {
+ try {
+ actionGraph.registerAction(action);
+ } catch (ActionConflictException e) {
+ Exception oldException = badActionMap.put(action, new ConflictException(e));
+ Preconditions.checkState(oldException == null,
+ "%s | %s | %s", action, e, oldException);
+ // We skip the rest of the loop, and do not add the path->artifact mapping for this
+ // artifact below -- we don't need to check it since this action is already in
+ // error.
+ continue;
+ }
+ }
+ artifactPathMap.put(entry.getKey().getExecPath(), entry.getKey());
+ }
+ }
+ }
+ };
+ }
+
+ void prepareForExecution(Executor executor, boolean keepGoing,
+ boolean explain, ActionCacheChecker actionCacheChecker) {
+ this.executorEngine = Preconditions.checkNotNull(executor);
+
+ // Start with a new map each build so there's no issue with internal resizing.
+ this.buildActionMap = Maps.newConcurrentMap();
+ this.keepGoing = keepGoing;
+ this.hadExecutionError = false;
+ this.actionCacheChecker = Preconditions.checkNotNull(actionCacheChecker);
+ // Don't cache possibly stale data from the last build.
+ undeclaredInputsMetadata = new ConcurrentHashMap<>();
+ this.explain = explain;
+ }
+
+ public void setActionLogBufferPathGenerator(
+ ActionLogBufferPathGenerator actionLogBufferPathGenerator) {
+ this.actionLogBufferPathGenerator = actionLogBufferPathGenerator;
+ }
+
+ void executionOver() {
+ // This transitively holds a bunch of heavy objects, so it's important to clear it at the
+ // end of a build.
+ this.executorEngine = null;
+ }
+
+ File getExecRoot() {
+ return executorEngine.getExecRoot().getPathFile();
+ }
+
+ boolean probeActionExecution(Action action) {
+ return buildActionMap.containsKey(action.getPrimaryOutput());
+ }
+
+ /**
+ * Executes the provided action on the current thread. Returns the ActionExecutionValue with the
+ * result, either computed here or already computed on another thread.
+ *
+ * <p>For use from {@link ArtifactFunction} only.
+ */
+ ActionExecutionValue executeAction(Action action, FileAndMetadataCache graphFileCache,
+ Token token, long actionStartTime,
+ ActionExecutionContext actionExecutionContext)
+ throws ActionExecutionException, InterruptedException {
+ Exception exception = badActionMap.get(action);
+ if (exception != null) {
+ // If action had a conflict with some other action in the graph, report it now.
+ reportError(exception.getMessage(), exception, action, null);
+ }
+ Artifact primaryOutput = action.getPrimaryOutput();
+ FutureTask<ActionExecutionValue> actionTask =
+ new FutureTask<>(new ActionRunner(action, graphFileCache, token,
+ actionStartTime, actionExecutionContext));
+ // Check to see if another action is already executing/has executed this value.
+ Pair<Action, FutureTask<ActionExecutionValue>> oldAction =
+ buildActionMap.putIfAbsent(primaryOutput, Pair.of(action, actionTask));
+
+ if (oldAction == null) {
+ actionTask.run();
+ } else if (action == oldAction.first) {
+ // We only allow the same action to be executed twice if it discovers inputs. We allow that
+ // because we need to declare additional dependencies on those new inputs.
+ Preconditions.checkState(action.discoversInputs(),
+ "Same action shouldn't execute twice in build: %s", action);
+ actionTask = oldAction.second;
+ } else {
+ Preconditions.checkState(Actions.canBeShared(oldAction.first, action),
+ "Actions cannot be shared: %s %s", oldAction.first, action);
+ // Wait for other action to finish, so any actions that depend on its outputs can execute.
+ actionTask = oldAction.second;
+ }
+ try {
+ return actionTask.get();
+ } catch (ExecutionException e) {
+ Throwables.propagateIfPossible(e.getCause(),
+ ActionExecutionException.class, InterruptedException.class);
+ throw new IllegalStateException(e);
+ } finally {
+ String message = action.getProgressMessage();
+ if (message != null) {
+ // Tell the receiver that the action has completed *before* telling the reporter.
+ // This way the latter will correctly show the number of completed actions when task
+ // completion messages are enabled (--show_task_finish).
+ if (completionReceiver != null) {
+ completionReceiver.actionCompleted(action);
+ }
+ reporter.finishTask(null, prependExecPhaseStats(message));
+ }
+ }
+ }
+
+ /**
+ * Returns an ActionExecutionContext suitable for executing a particular action. The caller should
+ * pass the returned context to {@link #executeAction}, and any other method that needs to execute
+ * tasks related to that action.
+ */
+ ActionExecutionContext constructActionExecutionContext(final FileAndMetadataCache graphFileCache,
+ MetadataHandler metadataHandler) {
+ FileOutErr fileOutErr = actionLogBufferPathGenerator.generate();
+ return new ActionExecutionContext(
+ executorEngine,
+ new DelegatingPairFileCache(graphFileCache, perBuildFileCache),
+ metadataHandler,
+ fileOutErr,
+ new MiddlemanExpander() {
+ @Override
+ public void expand(Artifact middlemanArtifact,
+ Collection<? super Artifact> output) {
+ // Legacy code is more permissive regarding "mm" in that it expands any middleman,
+ // not just inputs of this action. Skyframe doesn't have access to a global action
+ // graph, therefore this implementation can't expand any middleman, only the
+ // inputs of this action.
+ // This is fine though: actions should only hold references to their input
+ // artifacts, otherwise hermeticity would be violated.
+ output.addAll(graphFileCache.expandInputMiddleman(middlemanArtifact));
+ }
+ });
+ }
+
+ /**
+ * Returns a MetadataHandler for use when executing a particular action. The caller can pass the
+ * returned handler in whenever a MetadataHandler is needed in the course of executing the action.
+ */
+ MetadataHandler constructMetadataHandler(MetadataHandler graphFileCache) {
+ return new UndeclaredInputHandler(graphFileCache, undeclaredInputsMetadata);
+ }
+
+ /**
+ * Checks the action cache to see if {@code action} needs to be executed, or is up to date.
+ * Returns a token with the semantics of {@link ActionCacheChecker#getTokenIfNeedToExecute}: null
+ * if the action is up to date, and non-null if it needs to be executed, in which case that token
+ * should be provided to the ActionCacheChecker after execution.
+ */
+ Token checkActionCache(Action action, MetadataHandler metadataHandler, long actionStartTime) {
+ profiler.startTask(ProfilerTask.ACTION_CHECK, action);
+ Token token = actionCacheChecker.getTokenIfNeedToExecute(
+ action, explain ? reporter : null, metadataHandler);
+ profiler.completeTask(ProfilerTask.ACTION_CHECK);
+ if (token == null) {
+ boolean eventPosted = false;
+ // Notify BlazeRuntimeStatistics about the action middleman 'execution'.
+ if (action.getActionType().isMiddleman()) {
+ postEvent(new ActionMiddlemanEvent(action, actionStartTime));
+ eventPosted = true;
+ }
+
+ if (action instanceof NotifyOnActionCacheHit) {
+ NotifyOnActionCacheHit notify = (NotifyOnActionCacheHit) action;
+ notify.actionCacheHit(executorEngine);
+ }
+
+ // We still need to check the outputs so that output file data is available to the value.
+ checkOutputs(action, metadataHandler);
+ if (!eventPosted) {
+ postEvent(new CachedActionEvent(action, actionStartTime));
+ }
+ }
+ return token;
+ }
+
+ /**
+ * Perform dependency discovery for action, which must discover its inputs.
+ *
+ * <p>This method is just a wrapper around {@link Action#discoverInputs} that properly processes
+ * any ActionExecutionException thrown before rethrowing it to the caller.
+ */
+ void discoverInputs(Action action, ActionExecutionContext actionExecutionContext)
+ throws ActionExecutionException, InterruptedException {
+ try {
+ action.discoverInputs(actionExecutionContext);
+ } catch (ActionExecutionException e) {
+ processAndThrow(e, action, actionExecutionContext.getFileOutErr());
+ }
+ }
+
+ /**
+ * This method should be called if the builder encounters an error during
+ * execution. This allows the builder to record that it encountered at
+ * least one error, and may make it swallow its output to prevent
+ * spamming the user any further.
+ */
+ private void recordExecutionError() {
+ hadExecutionError = true;
+ }
+
+ /**
+ * Returns true if the Builder is winding down (i.e. cancelling outstanding
+ * actions and preparing to abort.)
+ * The builder is winding down iff:
+ * <ul>
+ * <li>we had an execution error
+ * <li>we are not running with --keep_going
+ * </ul>
+ */
+ private boolean isBuilderAborting() {
+ return hadExecutionError && !keepGoing;
+ }
+
+ void setFileCache(ActionInputFileCache fileCache) {
+ this.perBuildFileCache = fileCache;
+ }
+
+ private class ActionRunner implements Callable<ActionExecutionValue> {
+ private final Action action;
+ private final FileAndMetadataCache graphFileCache;
+ private Token token;
+ private long actionStartTime;
+ private ActionExecutionContext actionExecutionContext;
+
+ ActionRunner(Action action, FileAndMetadataCache graphFileCache, Token token,
+ long actionStartTime,
+ ActionExecutionContext actionExecutionContext) {
+ this.action = action;
+ this.graphFileCache = graphFileCache;
+ this.token = token;
+ this.actionStartTime = actionStartTime;
+ this.actionExecutionContext = actionExecutionContext;
+ }
+
+ @Override
+ public ActionExecutionValue call() throws ActionExecutionException, InterruptedException {
+ profiler.startTask(ProfilerTask.ACTION, action);
+ try {
+ if (actionCacheChecker.isActionExecutionProhibited(action)) {
+ // We can't execute an action (e.g. because --check_???_up_to_date option was used). Fail
+ // the build instead.
+ synchronized (reporter) {
+ TargetOutOfDateException e = new TargetOutOfDateException(action);
+ reporter.handle(Event.error(e.getMessage()));
+ recordExecutionError();
+ throw e;
+ }
+ }
+
+ String message = action.getProgressMessage();
+ if (message != null) {
+ reporter.startTask(null, prependExecPhaseStats(message));
+ }
+ statusReporterRef.get().setPreparing(action);
+
+ createOutputDirectories(action);
+
+ prepareScheduleExecuteAndCompleteAction(action, token,
+ actionExecutionContext, actionStartTime);
+ return new ActionExecutionValue(
+ graphFileCache.getOutputData(), graphFileCache.getAdditionalOutputData());
+ } finally {
+ profiler.completeTask(ProfilerTask.ACTION);
+ }
+ }
+ }
+
+ private void createOutputDirectories(Action action) throws ActionExecutionException {
+ try {
+ Set<Path> done = new HashSet<>(); // avoid redundant calls for the same directory.
+ for (Artifact outputFile : action.getOutputs()) {
+ Path outputDir = outputFile.getPath().getParentDirectory();
+ if (done.add(outputDir)) {
+ try {
+ createDirectoryAndParents(outputDir);
+ continue;
+ } catch (IOException e) {
+ /* Fall through to plan B. */
+ }
+
+ // Possibly some direct ancestors are not directories. In that case, we unlink all the
+ // ancestors until we reach a directory, then try again. This handles the case where a
+ // file becomes a directory, either from one build to another, or within a single build.
+ //
+ // Symlinks should not be followed so in order to clean up symlinks pointing to Fileset
+ // outputs from previous builds. See bug [incremental build of Fileset fails if
+ // Fileset.out was changed to be a subdirectory of the old value].
+ try {
+ for (Path p = outputDir; !p.isDirectory(Symlinks.NOFOLLOW);
+ p = p.getParentDirectory()) {
+ // p may be a file or dangling symlink, or a symlink to an old Fileset output
+ p.delete(); // throws IOException
+ }
+ createDirectoryAndParents(outputDir);
+ } catch (IOException e) {
+ throw new ActionExecutionException(
+ "failed to create output directory '" + outputDir + "'", e, action, false);
+ }
+ }
+ }
+ } catch (ActionExecutionException ex) {
+ printError(ex.getMessage(), action, null);
+ throw ex;
+ }
+ }
+
+ private String prependExecPhaseStats(String message) {
+ if (progressSupplier != null) {
+ // Prints a progress message like:
+ // [2608/6445] Compiling foo/bar.cc [host]
+ return progressSupplier.getProgressString() + " " + message;
+ } else {
+ // progressSupplier may be null in tests
+ return message;
+ }
+ }
+
+ /**
+ * Prepare, schedule, execute, and then complete the action.
+ * When this function is called, we know that this action needs to be executed.
+ * This function will prepare for the action's execution (i.e. delete the outputs);
+ * schedule its execution; execute the action;
+ * and then do some post-execution processing to complete the action:
+ * set the outputs readonly and executable, and insert the action results in the
+ * action cache.
+ *
+ * @param action The action to execute
+ * @param token The non-null token returned by dependencyChecker.getTokenIfNeedToExecute()
+ * @param context services in the scope of the action
+ * @param actionStartTime time when we started the first phase of the action execution.
+ * @throws ActionExecutionException if the execution of the specified action
+ * failed for any reason.
+ * @throws InterruptedException if the thread was interrupted.
+ */
+ private void prepareScheduleExecuteAndCompleteAction(Action action, Token token,
+ ActionExecutionContext context, long actionStartTime)
+ throws ActionExecutionException, InterruptedException {
+ Preconditions.checkNotNull(token, action);
+ // Delete the metadataHandler's cache of the action's outputs, since they are being deleted.
+ context.getMetadataHandler().discardMetadata(action.getOutputs());
+ // Delete the outputs before executing the action, just to ensure that
+ // the action really does produce the outputs.
+ try {
+ action.prepare(context.getExecutor().getExecRoot());
+ } catch (IOException e) {
+ reportError("failed to delete output files before executing action", e, action, null);
+ }
+
+ postEvent(new ActionStartedEvent(action, actionStartTime));
+ ResourceSet estimate = action.estimateResourceConsumption(executorEngine);
+ ActionExecutionStatusReporter statusReporter = statusReporterRef.get();
+ try {
+ if (estimate == null || estimate == ResourceSet.ZERO) {
+ statusReporter.setRunningFromBuildData(action);
+ } else {
+ // If estimated resource consumption is null, action will manually call
+ // resource manager when it knows what resources are needed.
+ resourceManager.acquireResources(action, estimate);
+ }
+ boolean outputDumped = executeActionTask(action, context);
+ completeAction(action, token, context.getMetadataHandler(),
+ context.getFileOutErr(), outputDumped);
+ } finally {
+ if (estimate != null) {
+ resourceManager.releaseResources(action, estimate);
+ }
+ statusReporter.remove(action);
+ postEvent(new ActionCompletionEvent(action));
+ }
+ }
+
+ private ActionExecutionException processAndThrow(
+ ActionExecutionException e, Action action, FileOutErr outErrBuffer)
+ throws ActionExecutionException {
+ reportActionExecution(action, e, outErrBuffer);
+ boolean reported = reportErrorIfNotAbortingMode(e, outErrBuffer);
+
+ ActionExecutionException toThrow = e;
+ if (reported){
+ // If we already printed the error for the exception we mark it as already reported
+ // so that we do not print it again in upper levels.
+ // Note that we need to report it here since we want immediate feedback of the errors
+ // and in some cases the upper-level printing mechanism only prints one of the errors.
+ toThrow = new AlreadyReportedActionExecutionException(e);
+ }
+
+ // Now, rethrow the exception.
+ // This can have two effects:
+ // If we're still building, the exception will get retrieved by the
+ // completor and rethrown.
+ // If we're aborting, the exception will never be retrieved from the
+ // completor, since the completor is waiting for all outstanding jobs
+ // to finish. After they have finished, it will only rethrow the
+ // exception that initially caused it to abort will and not check the
+ // exit status of any actions that had finished in the meantime.
+ throw toThrow;
+ }
+
+ /**
+ * Execute the specified action, in a profiler task.
+ * The caller is responsible for having already checked that we need to
+ * execute it and for acquiring/releasing any scheduling locks needed.
+ *
+ * <p>This is thread-safe so long as you don't try to execute the same action
+ * twice at the same time (or overlapping times).
+ * May execute in a worker thread.
+ *
+ * @throws ActionExecutionException if the execution of the specified action
+ * failed for any reason.
+ * @throws InterruptedException if the thread was interrupted.
+ * @return true if the action output was dumped, false otherwise.
+ */
+ private boolean executeActionTask(Action action, ActionExecutionContext actionExecutionContext)
+ throws ActionExecutionException, InterruptedException {
+ profiler.startTask(ProfilerTask.ACTION_EXECUTE, action);
+ // ActionExecutionExceptions that occur as the thread is interrupted are
+ // assumed to be a result of that, so we throw InterruptedException
+ // instead.
+ FileOutErr outErrBuffer = actionExecutionContext.getFileOutErr();
+ try {
+ action.execute(actionExecutionContext);
+
+ // Action terminated fine, now report the output.
+ // The .showOutput() method is not necessarily a quick check: in its
+ // current implementation it uses regular expression matching.
+ if (outErrBuffer.hasRecordedOutput()
+ && (action.showsOutputUnconditionally()
+ || reporter.showOutput(Label.print(action.getOwner().getLabel())))) {
+ dumpRecordedOutErr(action, outErrBuffer);
+ return true;
+ }
+ // Defer reporting action success until outputs are checked
+ } catch (ActionExecutionException e) {
+ processAndThrow(e, action, outErrBuffer);
+ } finally {
+ profiler.completeTask(ProfilerTask.ACTION_EXECUTE);
+ }
+ return false;
+ }
+
+ private void completeAction(Action action, Token token, MetadataHandler metadataHandler,
+ FileOutErr fileOutErr, boolean outputAlreadyDumped) throws ActionExecutionException {
+ try {
+ Preconditions.checkState(action.inputsKnown(),
+ "Action %s successfully executed, but inputs still not known", action);
+
+ profiler.startTask(ProfilerTask.ACTION_COMPLETE, action);
+ try {
+ if (!checkOutputs(action, metadataHandler)) {
+ reportError("not all outputs were created", null, action,
+ outputAlreadyDumped ? null : fileOutErr);
+ }
+ // Prevent accidental stomping on files.
+ // This will also throw a FileNotFoundException
+ // if any of the output files doesn't exist.
+ try {
+ setOutputsReadOnlyAndExecutable(action, metadataHandler);
+ } catch (IOException e) {
+ reportError("failed to set outputs read-only", e, action, null);
+ }
+ try {
+ actionCacheChecker.afterExecution(action, token, metadataHandler);
+ } catch (IOException e) {
+ // Skyframe does all the filesystem access needed during the previous calls, and if those
+ // calls failed, we should already have thrown. So an IOException is impossible here.
+ throw new IllegalStateException(
+ "failed to update action cache for " + action.prettyPrint()
+ + ", but all outputs should already have been checked", e);
+ }
+ } finally {
+ profiler.completeTask(ProfilerTask.ACTION_COMPLETE);
+ }
+ reportActionExecution(action, null, fileOutErr);
+ } catch (ActionExecutionException actionException) {
+ // Success in execution but failure in completion.
+ reportActionExecution(action, actionException, fileOutErr);
+ throw actionException;
+ } catch (IllegalStateException exception) {
+ // More serious internal error, but failure still reported.
+ reportActionExecution(action,
+ new ActionExecutionException(exception, action, true), fileOutErr);
+ throw exception;
+ }
+ }
+
+ /**
+ * For each of the action's outputs that is a regular file (not a symbolic
+ * link or directory), make it read-only and executable.
+ *
+ * <p>Making the outputs read-only helps preventing accidental editing of
+ * them (e.g. in case of generated source code), while making them executable
+ * helps running generated files (such as generated shell scripts) on the
+ * command line.
+ *
+ * <p>May execute in a worker thread.
+ *
+ * <p>Note: setting these bits maintains transparency regarding the locality of the build;
+ * because the remote execution engine sets them, they should be set for local builds too.
+ *
+ * @throws IOException if an I/O error occurred.
+ */
+ private final void setOutputsReadOnlyAndExecutable(Action action, MetadataHandler metadataHandler)
+ throws IOException {
+ Preconditions.checkState(!action.getActionType().isMiddleman());
+
+ for (Artifact output : action.getOutputs()) {
+ Path path = output.getPath();
+ if (metadataHandler.isInjected(output)) {
+ // We trust the files created by the execution-engine to be non symlinks with expected
+ // chmod() settings already applied. The follow stanza implies a total of 6 system calls,
+ // since the UnixFileSystem implementation of setWritable() and setExecutable() both
+ // do a stat() internally.
+ continue;
+ }
+ if (path.isFile(Symlinks.NOFOLLOW)) { // i.e. regular files only.
+ path.setWritable(false);
+ path.setExecutable(true);
+ }
+ }
+ }
+
+ private void reportMissingOutputFile(Action action, Artifact output, Reporter reporter,
+ boolean isSymlink) {
+ boolean genrule = action.getMnemonic().equals("Genrule");
+ String prefix = (genrule ? "declared output '" : "output '") + output.prettyPrint() + "' ";
+ if (isSymlink) {
+ reporter.handle(Event.error(
+ action.getOwner().getLocation(), prefix + "is a dangling symbolic link"));
+ } else {
+ String suffix = genrule ? " by genrule. This is probably "
+ + "because the genrule actually didn't create this output, or because the output was a "
+ + "directory and the genrule was run remotely (note that only the contents of "
+ + "declared file outputs are copied from genrules run remotely)" : "";
+ reporter.handle(Event.error(
+ action.getOwner().getLocation(), prefix + "was not created" + suffix));
+ }
+ }
+
+ /**
+ * Validates that all action outputs were created.
+ *
+ * @return false if some outputs are missing, true - otherwise.
+ */
+ private boolean checkOutputs(Action action, MetadataHandler metadataHandler) {
+ boolean success = true;
+ for (Artifact output : action.getOutputs()) {
+ if (!metadataHandler.artifactExists(output)) {
+ reportMissingOutputFile(action, output, reporter, output.getPath().isSymbolicLink());
+ success = false;
+ }
+ }
+ return success;
+ }
+
+ private void postEvent(Object event) {
+ EventBus bus = eventBus.get();
+ if (bus != null) {
+ bus.post(event);
+ }
+ }
+
+ /**
+ * Convenience function for reporting that the action failed due to a
+ * the exception cause, if there is an additional explanatory message that
+ * clarifies the message of the exception. Combines the user-provided message
+ * and the exceptions' message and reports the combination as error.
+ * Then, throws an ActionExecutionException with the reported error as
+ * message and the provided exception as the cause.
+ *
+ * @param message A small text that explains why the action failed
+ * @param cause The exception that caused the action to fail
+ * @param action The action that failed
+ * @param actionOutput The output of the failed Action.
+ * May be null, if there is no output to display
+ */
+ private void reportError(String message, Throwable cause, Action action, FileOutErr actionOutput)
+ throws ActionExecutionException {
+ ActionExecutionException ex;
+ if (cause == null) {
+ ex = new ActionExecutionException(message, action, false);
+ } else {
+ ex = new ActionExecutionException(message, cause, action, false);
+ }
+ printError(ex.getMessage(), action, actionOutput);
+ throw ex;
+ }
+
+ /**
+ * For the action 'action' that failed due to 'ex' with the output
+ * 'actionOutput', notify the user about the error. To notify the user, the
+ * method first displays the output of the action and then reports an error
+ * via the reporter. The method ensures that the two messages appear next to
+ * each other by locking the outErr object where the output is displayed.
+ *
+ * @param message The reason why the action failed
+ * @param action The action that failed, must not be null.
+ * @param actionOutput The output of the failed Action.
+ * May be null, if there is no output to display
+ */
+ private void printError(String message, Action action, FileOutErr actionOutput) {
+ synchronized (reporter) {
+ if (actionOutput != null && actionOutput.hasRecordedOutput()) {
+ dumpRecordedOutErr(action, actionOutput);
+ }
+ if (keepGoing) {
+ message = "Couldn't " + describeAction(action) + ": " + message;
+ }
+ reporter.handle(Event.error(action.getOwner().getLocation(), message));
+ recordExecutionError();
+ }
+ }
+
+ /** Describe an action, for use in error messages. */
+ private static String describeAction(Action action) {
+ if (action.getOutputs().isEmpty()) {
+ return "run " + action.prettyPrint();
+ } else if (action.getActionType().isMiddleman()) {
+ return "build " + action.prettyPrint();
+ } else {
+ return "build file " + action.getPrimaryOutput().prettyPrint();
+ }
+ }
+
+ /**
+ * Dump the output from the action.
+ *
+ * @param action The action whose output is being dumped
+ * @param outErrBuffer The OutErr that recorded the actions output
+ */
+ private void dumpRecordedOutErr(Action action, FileOutErr outErrBuffer) {
+ StringBuilder message = new StringBuilder("");
+ message.append("From ");
+ message.append(action.describe());
+ message.append(":");
+
+ // Synchronize this on the reporter, so that the output from multiple
+ // actions will not be interleaved.
+ synchronized (reporter) {
+ // Only print the output if we're not winding down.
+ if (isBuilderAborting()) {
+ return;
+ }
+ reporter.handle(Event.info(message.toString()));
+
+ OutErr outErr = this.reporter.getOutErr();
+ outErrBuffer.dumpOutAsLatin1(outErr.getOutputStream());
+ outErrBuffer.dumpErrAsLatin1(outErr.getErrorStream());
+ }
+ }
+
+ private void reportActionExecution(Action action,
+ ActionExecutionException exception, FileOutErr outErr) {
+ String stdout = null;
+ String stderr = null;
+
+ if (outErr.hasRecordedStdout()) {
+ stdout = outErr.getOutputFile().toString();
+ }
+ if (outErr.hasRecordedStderr()) {
+ stderr = outErr.getErrorFile().toString();
+ }
+ postEvent(new ActionExecutedEvent(action, exception, stdout, stderr));
+ }
+
+ /**
+ * Returns true if the exception was reported. False otherwise. Currently this is a copy of what
+ * we did in pre-Skyframe execution. The main implication is that we are printing the error to the
+ * top level reporter instead of the action reporter. Because of that Skyframe values do not know
+ * about the errors happening in the execution phase. Even if we change in the future to log to
+ * the action reporter (that would be done in ActionExecutionFunction.compute() when we get an
+ * ActionExecutionException), we probably do not want to also store the StdErr output, so
+ * dumpRecordedOutErr() should still be called here.
+ */
+ private boolean reportErrorIfNotAbortingMode(ActionExecutionException ex,
+ FileOutErr outErrBuffer) {
+ // For some actions (e.g. many local actions) the pollInterruptedStatus()
+ // won't notice that we had an interrupted job. It will continue.
+ // For that reason we must take care to NOT report errors if we're
+ // in the 'aborting' mode: Any cancelled action would show up here.
+ // For some actions (e.g. many local actions) the pollInterruptedStatus()
+ // won't notice that we had an interrupted job. It will continue.
+ // For that reason we must take care to NOT report errors if we're
+ // in the 'aborting' mode: Any cancelled action would show up here.
+ synchronized (this.reporter) {
+ if (!isBuilderAborting()) {
+ // Oops. The action aborted. Report the problem.
+ printError(ex.getMessage(), ex.getAction(), outErrBuffer);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** An object supplying data for action execution progress reporting. */
+ public interface ProgressSupplier {
+ /** Returns the progress string to prefix action execution messages with. */
+ String getProgressString();
+ }
+
+ /** An object that can be notified about action completion. */
+ public interface ActionCompletedReceiver {
+ /** Receives a completed action. */
+ void actionCompleted(Action action);
+ }
+
+ public void setActionExecutionProgressReportingObjects(
+ @Nullable ProgressSupplier progressSupplier,
+ @Nullable ActionCompletedReceiver completionReceiver) {
+ this.progressSupplier = progressSupplier;
+ this.completionReceiver = completionReceiver;
+ }
+
+ private static class DelegatingPairFileCache implements ActionInputFileCache {
+ private final ActionInputFileCache perActionCache;
+ private final ActionInputFileCache perBuildFileCache;
+
+ private DelegatingPairFileCache(ActionInputFileCache mainCache,
+ ActionInputFileCache perBuildFileCache) {
+ this.perActionCache = mainCache;
+ this.perBuildFileCache = perBuildFileCache;
+ }
+
+ @Override
+ public ByteString getDigest(ActionInput actionInput) throws IOException {
+ ByteString digest = perActionCache.getDigest(actionInput);
+ return digest != null ? digest : perBuildFileCache.getDigest(actionInput);
+ }
+
+ @Override
+ public long getSizeInBytes(ActionInput actionInput) throws IOException {
+ long size = perActionCache.getSizeInBytes(actionInput);
+ return size > -1 ? size : perBuildFileCache.getSizeInBytes(actionInput);
+ }
+
+ @Override
+ public boolean contentsAvailableLocally(ByteString digest) {
+ return perActionCache.contentsAvailableLocally(digest)
+ || perBuildFileCache.contentsAvailableLocally(digest);
+ }
+
+ @Nullable
+ @Override
+ public File getFileFromDigest(ByteString digest) throws IOException {
+ File file = perActionCache.getFileFromDigest(digest);
+ return file != null ? file : perBuildFileCache.getFileFromDigest(digest);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeBuildView.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeBuildView.java
new file mode 100644
index 0000000..857c231
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeBuildView.java
@@ -0,0 +1,510 @@
+// 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.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.common.eventbus.EventBus;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.ArtifactFactory;
+import com.google.devtools.build.lib.actions.ArtifactOwner;
+import com.google.devtools.build.lib.actions.ArtifactPrefixConflictException;
+import com.google.devtools.build.lib.actions.MutableActionGraph;
+import com.google.devtools.build.lib.analysis.AnalysisEnvironment;
+import com.google.devtools.build.lib.analysis.AnalysisFailureEvent;
+import com.google.devtools.build.lib.analysis.Aspect;
+import com.google.devtools.build.lib.analysis.CachingAnalysisEnvironment;
+import com.google.devtools.build.lib.analysis.ConfiguredAspectFactory;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.ConfiguredTargetFactory;
+import com.google.devtools.build.lib.analysis.LabelAndConfiguration;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget;
+import com.google.devtools.build.lib.analysis.ViewCreationFailedException;
+import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoFactory;
+import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoFactory.BuildInfoKey;
+import com.google.devtools.build.lib.analysis.config.BinTools;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.BuildConfigurationCollection;
+import com.google.devtools.build.lib.analysis.config.ConfigMatchingProvider;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.skyframe.ActionLookupValue.ActionLookupKey;
+import com.google.devtools.build.lib.skyframe.BuildInfoCollectionValue.BuildInfoKeyAndConfig;
+import com.google.devtools.build.lib.skyframe.ConfiguredTargetFunction.ConfiguredValueCreationException;
+import com.google.devtools.build.lib.skyframe.SkyframeActionExecutor.ConflictException;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.skyframe.CycleInfo;
+import com.google.devtools.build.skyframe.ErrorInfo;
+import com.google.devtools.build.skyframe.EvaluationProgressReceiver;
+import com.google.devtools.build.skyframe.EvaluationResult;
+import com.google.devtools.build.skyframe.SkyFunction.Environment;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * Skyframe-based driver of analysis.
+ *
+ * <p>Covers enough functionality to work as a substitute for {@code BuildView#configureTargets}.
+ */
+public final class SkyframeBuildView {
+
+ private final ConfiguredTargetFactory factory;
+ private final ArtifactFactory artifactFactory;
+ @Nullable private EventHandler warningListener;
+ private final SkyframeExecutor skyframeExecutor;
+ private final Runnable legacyDataCleaner;
+ private final BinTools binTools;
+ private boolean enableAnalysis = false;
+
+ // This hack allows us to see when a configured target has been invalidated, and thus when the set
+ // of artifact conflicts needs to be recomputed (whenever a configured target has been invalidated
+ // or newly evaluated).
+ private final EvaluationProgressReceiver invalidationReceiver =
+ new ConfiguredTargetValueInvalidationReceiver();
+ private final Set<SkyKey> evaluatedConfiguredTargets = Sets.newConcurrentHashSet();
+ // Used to see if checks of graph consistency need to be done after analysis.
+ private volatile boolean someConfiguredTargetEvaluated = false;
+
+ // We keep the set of invalidated configuration targets so that we can know if something
+ // has been invalidated after graph pruning has been executed.
+ private Set<ConfiguredTargetValue> dirtyConfiguredTargets = Sets.newConcurrentHashSet();
+ private volatile boolean anyConfiguredTargetDeleted = false;
+ private SkyKey configurationKey = null;
+
+ public SkyframeBuildView(ConfiguredTargetFactory factory,
+ ArtifactFactory artifactFactory,
+ SkyframeExecutor skyframeExecutor, Runnable legacyDataCleaner, BinTools binTools) {
+ this.factory = factory;
+ this.artifactFactory = artifactFactory;
+ this.skyframeExecutor = skyframeExecutor;
+ this.legacyDataCleaner = legacyDataCleaner;
+ this.binTools = binTools;
+ skyframeExecutor.setArtifactFactoryAndBinTools(artifactFactory, binTools);
+ }
+
+ public void setWarningListener(@Nullable EventHandler warningListener) {
+ this.warningListener = warningListener;
+ }
+
+ public void setConfigurationSkyKey(SkyKey skyKey) {
+ this.configurationKey = skyKey;
+ }
+
+ public void resetEvaluatedConfiguredTargetKeysSet() {
+ evaluatedConfiguredTargets.clear();
+ }
+
+ public Set<SkyKey> getEvaluatedTargetKeys() {
+ return ImmutableSet.copyOf(evaluatedConfiguredTargets);
+ }
+
+ private void setDeserializedArtifactOwners() throws ViewCreationFailedException {
+ Map<PathFragment, Artifact> deserializedArtifactMap =
+ artifactFactory.getDeserializedArtifacts();
+ Set<Artifact> deserializedArtifacts = new HashSet<>();
+ for (Artifact artifact : deserializedArtifactMap.values()) {
+ if (!artifact.getExecPath().getBaseName().endsWith(".gcda")) {
+ // gcda files are classified as generated artifacts, but are not actually generated. All
+ // others need owners.
+ deserializedArtifacts.add(artifact);
+ }
+ }
+ if (deserializedArtifacts.isEmpty()) {
+ // If there are no deserialized artifacts to process, don't pay the price of iterating over
+ // the graph.
+ return;
+ }
+ for (Map.Entry<SkyKey, ActionLookupValue> entry :
+ skyframeExecutor.getActionLookupValueMap().entrySet()) {
+ for (Action action : entry.getValue().getActionsForFindingArtifactOwners()) {
+ for (Artifact output : action.getOutputs()) {
+ Artifact deserializedArtifact = deserializedArtifactMap.get(output.getExecPath());
+ if (deserializedArtifact != null) {
+ deserializedArtifact.setArtifactOwner((ActionLookupKey) entry.getKey().argument());
+ deserializedArtifacts.remove(deserializedArtifact);
+ }
+ }
+ }
+ }
+ if (!deserializedArtifacts.isEmpty()) {
+ throw new ViewCreationFailedException("These artifacts were read in from the FDO profile but"
+ + " have no generating action that could be found. If you are confident that your profile was"
+ + " collected from the same source state at which you're building, please report this:\n"
+ + Artifact.asExecPaths(deserializedArtifacts));
+ }
+ artifactFactory.clearDeserializedArtifacts();
+ }
+
+ /**
+ * Analyzes the specified targets using Skyframe as the driving framework.
+ *
+ * @return the configured targets that should be built
+ */
+ public Collection<ConfiguredTarget> configureTargets(List<ConfiguredTargetKey> values,
+ EventBus eventBus, boolean keepGoing)
+ throws InterruptedException, ViewCreationFailedException {
+ enableAnalysis(true);
+ EvaluationResult<ConfiguredTargetValue> result;
+ try {
+ result = skyframeExecutor.configureTargets(values, keepGoing);
+ } finally {
+ enableAnalysis(false);
+ }
+ // For Skyframe m1, note that we already reported action conflicts during action registration
+ // in the legacy action graph.
+ ImmutableMap<Action, ConflictException> badActions = skyframeExecutor.findArtifactConflicts();
+
+ // Filter out all CTs that have a bad action and convert to a list of configured targets. This
+ // code ensures that the resulting list of configured targets has the same order as the incoming
+ // list of values, i.e., that the order is deterministic.
+ Collection<ConfiguredTarget> goodCts = Lists.newArrayListWithCapacity(values.size());
+ for (ConfiguredTargetKey value : values) {
+ ConfiguredTargetValue ctValue = result.get(ConfiguredTargetValue.key(value));
+ if (ctValue == null) {
+ continue;
+ }
+ goodCts.add(ctValue.getConfiguredTarget());
+ }
+
+ if (!result.hasError() && badActions.isEmpty()) {
+ setDeserializedArtifactOwners();
+ return goodCts;
+ }
+
+ // --nokeep_going so we fail with an exception for the first error.
+ // TODO(bazel-team): We might want to report the other errors through the event bus but
+ // for keeping this code in parity with legacy we just report the first error for now.
+ if (!keepGoing) {
+ for (Map.Entry<Action, ConflictException> bad : badActions.entrySet()) {
+ ConflictException ex = bad.getValue();
+ try {
+ ex.rethrowTyped();
+ } catch (MutableActionGraph.ActionConflictException ace) {
+ ace.reportTo(skyframeExecutor.getReporter());
+ String errorMsg = "Analysis of target '" + bad.getKey().getOwner().getLabel()
+ + "' failed; build aborted";
+ throw new ViewCreationFailedException(errorMsg);
+ } catch (ArtifactPrefixConflictException apce) {
+ skyframeExecutor.getReporter().handle(Event.error(apce.getMessage()));
+ }
+ throw new ViewCreationFailedException(ex.getMessage());
+ }
+
+ Map.Entry<SkyKey, ErrorInfo> error = result.errorMap().entrySet().iterator().next();
+ SkyKey topLevel = error.getKey();
+ ErrorInfo errorInfo = error.getValue();
+ assertSaneAnalysisError(errorInfo, topLevel);
+ skyframeExecutor.getCyclesReporter().reportCycles(errorInfo.getCycleInfo(), topLevel,
+ skyframeExecutor.getReporter());
+ Throwable cause = errorInfo.getException();
+ Preconditions.checkState(cause != null || !Iterables.isEmpty(errorInfo.getCycleInfo()),
+ errorInfo);
+ String errorMsg = "Analysis of target '" + ConfiguredTargetValue.extractLabel(topLevel)
+ + "' failed; build aborted";
+ throw new ViewCreationFailedException(errorMsg);
+ }
+
+ // --keep_going : We notify the error and return a ConfiguredTargetValue
+ for (Map.Entry<SkyKey, ErrorInfo> errorEntry : result.errorMap().entrySet()) {
+ if (values.contains(errorEntry.getKey().argument())) {
+ SkyKey errorKey = errorEntry.getKey();
+ ConfiguredTargetKey label = (ConfiguredTargetKey) errorKey.argument();
+ ErrorInfo errorInfo = errorEntry.getValue();
+ assertSaneAnalysisError(errorInfo, errorKey);
+
+ skyframeExecutor.getCyclesReporter().reportCycles(errorInfo.getCycleInfo(), errorKey,
+ skyframeExecutor.getReporter());
+ // We try to get the root cause key first from ErrorInfo rootCauses. If we don't have one
+ // we try to use the cycle culprit if the error is a cycle. Otherwise we use the top-level
+ // error key.
+ Label root;
+ if (!Iterables.isEmpty(errorEntry.getValue().getRootCauses())) {
+ SkyKey culprit = Preconditions.checkNotNull(Iterables.getFirst(
+ errorEntry.getValue().getRootCauses(), null));
+ root = ((ConfiguredTargetKey) culprit.argument()).getLabel();
+ } else {
+ root = maybeGetConfiguredTargetCycleCulprit(errorInfo.getCycleInfo());
+ }
+ if (warningListener != null) {
+ warningListener.handle(Event.warn("errors encountered while analyzing target '"
+ + label + "': it will not be built"));
+ }
+ eventBus.post(new AnalysisFailureEvent(
+ LabelAndConfiguration.of(label.getLabel(), label.getConfiguration()), root));
+ }
+ }
+
+ Collection<Exception> reportedExceptions = Sets.newHashSet();
+ for (Map.Entry<Action, ConflictException> bad : badActions.entrySet()) {
+ ConflictException ex = bad.getValue();
+ try {
+ ex.rethrowTyped();
+ } catch (MutableActionGraph.ActionConflictException ace) {
+ ace.reportTo(skyframeExecutor.getReporter());
+ if (warningListener != null) {
+ warningListener.handle(Event.warn("errors encountered while analyzing target '"
+ + bad.getKey().getOwner().getLabel() + "': it will not be built"));
+ }
+ } catch (ArtifactPrefixConflictException apce) {
+ if (reportedExceptions.add(apce)) {
+ skyframeExecutor.getReporter().handle(Event.error(apce.getMessage()));
+ }
+ }
+ }
+
+ if (!badActions.isEmpty()) {
+ // In order to determine the set of configured targets transitively error free from action
+ // conflict issues, we run a post-processing update() that uses the bad action map.
+ EvaluationResult<PostConfiguredTargetValue> actionConflictResult =
+ skyframeExecutor.postConfigureTargets(values, keepGoing, badActions);
+
+ goodCts = Lists.newArrayListWithCapacity(values.size());
+ for (ConfiguredTargetKey value : values) {
+ PostConfiguredTargetValue postCt =
+ actionConflictResult.get(PostConfiguredTargetValue.key(value));
+ if (postCt != null) {
+ goodCts.add(postCt.getCt());
+ }
+ }
+ }
+ setDeserializedArtifactOwners();
+ return goodCts;
+ }
+
+ @Nullable
+ Label maybeGetConfiguredTargetCycleCulprit(Iterable<CycleInfo> cycleInfos) {
+ for (CycleInfo cycleInfo : cycleInfos) {
+ SkyKey culprit = Iterables.getFirst(cycleInfo.getCycle(), null);
+ if (culprit == null) {
+ continue;
+ }
+ if (culprit.functionName().equals(SkyFunctions.CONFIGURED_TARGET)) {
+ return ((LabelAndConfiguration) culprit.argument()).getLabel();
+ }
+ }
+ return null;
+ }
+
+ private static void assertSaneAnalysisError(ErrorInfo errorInfo, SkyKey key) {
+ Throwable cause = errorInfo.getException();
+ if (cause != null) {
+ // We should only be trying to configure targets when the loading phase succeeds, meaning
+ // that the only errors should be analysis errors.
+ Preconditions.checkState(cause instanceof ConfiguredValueCreationException,
+ "%s -> %s", key, errorInfo);
+ }
+ }
+
+ ArtifactFactory getArtifactFactory() {
+ return artifactFactory;
+ }
+
+ @Nullable
+ EventHandler getWarningListener() {
+ return warningListener;
+ }
+
+ /**
+ * Because we don't know what build-info artifacts this configured target may request, we
+ * conservatively register a dep on all of them.
+ */
+ // TODO(bazel-team): Allow analysis to return null so the value builder can exit and wait for a
+ // restart deps are not present.
+ private boolean getWorkspaceStatusValues(Environment env) {
+ env.getValue(WorkspaceStatusValue.SKY_KEY);
+ Map<BuildInfoKey, BuildInfoFactory> buildInfoFactories =
+ PrecomputedValue.BUILD_INFO_FACTORIES.get(env);
+ if (buildInfoFactories == null) {
+ return false;
+ }
+ BuildConfigurationCollection configurations = getBuildConfigurationCollection(env);
+ if (configurations == null) {
+ return false;
+ }
+ // These factories may each create their own build info artifacts, all depending on the basic
+ // build-info.txt and build-changelist.txt.
+ List<SkyKey> depKeys = Lists.newArrayList();
+ for (BuildInfoKey key : buildInfoFactories.keySet()) {
+ for (BuildConfiguration config : configurations.getAllConfigurations()) {
+ if (buildInfoFactories.get(key).isEnabled(config)) {
+ depKeys.add(BuildInfoCollectionValue.key(new BuildInfoKeyAndConfig(key, config)));
+ }
+ }
+ }
+ env.getValues(depKeys);
+ return !env.valuesMissing();
+ }
+
+ /** Returns null if any build-info values are not ready. */
+ @Nullable
+ CachingAnalysisEnvironment createAnalysisEnvironment(ArtifactOwner owner,
+ boolean isSystemEnv, boolean extendedSanityChecks, EventHandler eventHandler,
+ Environment env, boolean allowRegisteringActions) {
+ if (!getWorkspaceStatusValues(env)) {
+ return null;
+ }
+ return new CachingAnalysisEnvironment(
+ artifactFactory, owner, isSystemEnv, extendedSanityChecks, eventHandler, env,
+ allowRegisteringActions, binTools);
+ }
+
+ /**
+ * Invokes the appropriate constructor to create a {@link ConfiguredTarget} instance.
+ *
+ * <p>For use in {@code ConfiguredTargetFunction}.
+ *
+ * <p>Returns null if Skyframe deps are missing or upon certain errors.
+ */
+ @Nullable
+ ConfiguredTarget createConfiguredTarget(Target target, BuildConfiguration configuration,
+ CachingAnalysisEnvironment analysisEnvironment,
+ ListMultimap<Attribute, ConfiguredTarget> prerequisiteMap,
+ Set<ConfigMatchingProvider> configConditions)
+ throws InterruptedException {
+ Preconditions.checkState(enableAnalysis,
+ "Already in execution phase %s %s", target, configuration);
+ return factory.createConfiguredTarget(analysisEnvironment, artifactFactory, target,
+ configuration, prerequisiteMap, configConditions);
+ }
+
+ @Nullable
+ public Aspect createAspect(
+ AnalysisEnvironment env, RuleConfiguredTarget associatedTarget,
+ ConfiguredAspectFactory aspectFactory,
+ ListMultimap<Attribute, ConfiguredTarget> prerequisiteMap,
+ Set<ConfigMatchingProvider> configConditions) {
+ return factory.createAspect(
+ env, associatedTarget, aspectFactory, prerequisiteMap, configConditions);
+ }
+
+ @Nullable
+ private BuildConfigurationCollection getBuildConfigurationCollection(Environment env) {
+ ConfigurationCollectionValue configurationsValue =
+ (ConfigurationCollectionValue) env.getValue(configurationKey);
+ return configurationsValue == null ? null : configurationsValue.getConfigurationCollection();
+ }
+
+ @Nullable
+ SkyframeDependencyResolver createDependencyResolver(Environment env) {
+ BuildConfigurationCollection configurations = getBuildConfigurationCollection(env);
+ return configurations == null ? null : new SkyframeDependencyResolver(env);
+ }
+
+ /**
+ * Workaround to clear all legacy data, like the action graph and the artifact factory. We need
+ * to clear them to avoid conflicts.
+ * TODO(bazel-team): Remove this workaround. [skyframe-execution]
+ */
+ void clearLegacyData() {
+ legacyDataCleaner.run();
+ }
+
+ /**
+ * Hack to invalidate actions in legacy action graph when their values are invalidated in
+ * skyframe.
+ */
+ EvaluationProgressReceiver getInvalidationReceiver() {
+ return invalidationReceiver;
+ }
+
+ /** Clear the invalidated configured targets detected during loading and analysis phases. */
+ public void clearInvalidatedConfiguredTargets() {
+ dirtyConfiguredTargets = Sets.newConcurrentHashSet();
+ anyConfiguredTargetDeleted = false;
+ }
+
+ public boolean isSomeConfiguredTargetInvalidated() {
+ return anyConfiguredTargetDeleted || !dirtyConfiguredTargets.isEmpty();
+ }
+
+ /**
+ * Called from SkyframeExecutor to see whether the graph needs to be checked for artifact
+ * conflicts. Returns true if some configured target has been evaluated since the last time the
+ * graph was checked for artifact conflicts (with that last time marked by a call to
+ * {@link #resetEvaluatedConfiguredTargetFlag()}).
+ */
+ boolean isSomeConfiguredTargetEvaluated() {
+ Preconditions.checkState(!enableAnalysis);
+ return someConfiguredTargetEvaluated;
+ }
+
+ /**
+ * Called from SkyframeExecutor after the graph is checked for artifact conflicts so that
+ * the next time {@link #isSomeConfiguredTargetEvaluated} is called, it will return true only if
+ * some configured target has been evaluated since the last check for artifact conflicts.
+ */
+ void resetEvaluatedConfiguredTargetFlag() {
+ someConfiguredTargetEvaluated = false;
+ }
+
+ /**
+ * {@link #createConfiguredTarget} will only create configured targets if this is set to true. It
+ * should be set to true before any Skyframe update call that might call into {@link
+ * #createConfiguredTarget}, and false immediately after the call. Use it to fail-fast in the case
+ * that a target is requested for analysis not during the analysis phase.
+ */
+ void enableAnalysis(boolean enable) {
+ this.enableAnalysis = enable;
+ }
+
+ private class ConfiguredTargetValueInvalidationReceiver implements EvaluationProgressReceiver {
+ @Override
+ public void invalidated(SkyValue value, InvalidationState state) {
+ if (value instanceof ConfiguredTargetValue) {
+ ConfiguredTargetValue ctValue = (ConfiguredTargetValue) value;
+ // If the value was just dirtied and not deleted, then it may not be truly invalid, since
+ // it may later get re-validated.
+ if (state == InvalidationState.DELETED) {
+ anyConfiguredTargetDeleted = true;
+ } else {
+ dirtyConfiguredTargets.add(ctValue);
+ }
+ }
+ }
+
+ @Override
+ public void enqueueing(SkyKey skyKey) {}
+
+ @Override
+ public void evaluated(SkyKey skyKey, SkyValue value, EvaluationState state) {
+ if (skyKey.functionName() == SkyFunctions.CONFIGURED_TARGET) {
+ if (state == EvaluationState.BUILT) {
+ evaluatedConfiguredTargets.add(skyKey);
+ // During multithreaded operation, this is only set to true, so no concurrency issues.
+ someConfiguredTargetEvaluated = true;
+ }
+ Preconditions.checkNotNull(value, "%s %s", skyKey, state);
+ ConfiguredTargetValue ctValue = (ConfiguredTargetValue) value;
+ dirtyConfiguredTargets.remove(ctValue);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeDependencyResolver.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeDependencyResolver.java
new file mode 100644
index 0000000..1c7cbfa
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeDependencyResolver.java
@@ -0,0 +1,68 @@
+// 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.devtools.build.lib.analysis.DependencyResolver;
+import com.google.devtools.build.lib.analysis.TargetAndConfiguration;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.packages.NoSuchThingException;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.packages.TargetUtils;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.skyframe.SkyFunction.Environment;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import javax.annotation.Nullable;
+
+/**
+ * A dependency resolver for use within Skyframe. Loads packages lazily when possible.
+ */
+public final class SkyframeDependencyResolver extends DependencyResolver {
+
+ private final Environment env;
+
+ public SkyframeDependencyResolver(Environment env) {
+ this.env = env;
+ }
+
+ @Override
+ protected void invalidVisibilityReferenceHook(TargetAndConfiguration value, Label label) {
+ env.getListener().handle(
+ Event.error(TargetUtils.getLocationMaybe(value.getTarget()), String.format(
+ "Label '%s' in visibility attribute does not refer to a package group", label)));
+ }
+
+ @Override
+ protected void invalidPackageGroupReferenceHook(TargetAndConfiguration value, Label label) {
+ env.getListener().handle(
+ Event.error(TargetUtils.getLocationMaybe(value.getTarget()), String.format(
+ "label '%s' does not refer to a package group", label)));
+ }
+
+ @Nullable
+ @Override
+ protected Target getTarget(Label label) throws NoSuchThingException {
+ if (env.getValue(TargetMarkerValue.key(label)) == null) {
+ return null;
+ }
+ SkyKey key = PackageValue.key(label.getPackageIdentifier());
+ SkyValue value = env.getValue(key);
+ if (value == null) {
+ return null;
+ }
+ PackageValue packageValue = (PackageValue) value;
+ return packageValue.getPackage().getTarget(label.getName());
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java
new file mode 100644
index 0000000..29b4c23
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java
@@ -0,0 +1,1476 @@
+// 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.annotations.VisibleForTesting;
+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.base.Throwables;
+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.Maps;
+import com.google.common.collect.Sets;
+import com.google.common.eventbus.EventBus;
+import com.google.devtools.build.lib.actions.Action;
+import com.google.devtools.build.lib.actions.ActionCacheChecker;
+import com.google.devtools.build.lib.actions.ActionExecutionStatusReporter;
+import com.google.devtools.build.lib.actions.ActionInputFileCache;
+import com.google.devtools.build.lib.actions.ActionLogBufferPathGenerator;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.ArtifactFactory;
+import com.google.devtools.build.lib.actions.ArtifactOwner;
+import com.google.devtools.build.lib.actions.Executor;
+import com.google.devtools.build.lib.actions.ResourceManager;
+import com.google.devtools.build.lib.actions.Root;
+import com.google.devtools.build.lib.analysis.Aspect;
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.analysis.BuildView.Options;
+import com.google.devtools.build.lib.analysis.ConfiguredAspectFactory;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.DependencyResolver.Dependency;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTarget;
+import com.google.devtools.build.lib.analysis.TopLevelArtifactContext;
+import com.google.devtools.build.lib.analysis.WorkspaceStatusAction;
+import com.google.devtools.build.lib.analysis.WorkspaceStatusAction.Factory;
+import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoFactory;
+import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoFactory.BuildInfoKey;
+import com.google.devtools.build.lib.analysis.config.BinTools;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
+import com.google.devtools.build.lib.analysis.config.BuildConfigurationCollection;
+import com.google.devtools.build.lib.analysis.config.BuildConfigurationKey;
+import com.google.devtools.build.lib.analysis.config.BuildOptions;
+import com.google.devtools.build.lib.analysis.config.ConfigurationFactory;
+import com.google.devtools.build.lib.analysis.config.ConfigurationFragmentFactory;
+import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
+import com.google.devtools.build.lib.concurrent.ThreadSafety;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadCompatible;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.events.Reporter;
+import com.google.devtools.build.lib.packages.BuildFileContainsErrorsException;
+import com.google.devtools.build.lib.packages.NoSuchPackageException;
+import com.google.devtools.build.lib.packages.NoSuchThingException;
+import com.google.devtools.build.lib.packages.Package;
+import com.google.devtools.build.lib.packages.PackageFactory;
+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.pkgcache.PackageCacheOptions;
+import com.google.devtools.build.lib.pkgcache.PackageManager;
+import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
+import com.google.devtools.build.lib.pkgcache.TransitivePackageLoader;
+import com.google.devtools.build.lib.skyframe.SkyframeActionExecutor.ActionCompletedReceiver;
+import com.google.devtools.build.lib.skyframe.SkyframeActionExecutor.ProgressSupplier;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.util.AbruptExitException;
+import com.google.devtools.build.lib.util.ExitCode;
+import com.google.devtools.build.lib.util.ResourceUsage;
+import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor;
+import com.google.devtools.build.lib.vfs.BatchStat;
+import com.google.devtools.build.lib.vfs.ModifiedFileSet;
+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.lib.vfs.UnixGlob;
+import com.google.devtools.build.skyframe.BuildDriver;
+import com.google.devtools.build.skyframe.CycleInfo;
+import com.google.devtools.build.skyframe.CyclesReporter;
+import com.google.devtools.build.skyframe.Differencer;
+import com.google.devtools.build.skyframe.ErrorInfo;
+import com.google.devtools.build.skyframe.EvaluationProgressReceiver;
+import com.google.devtools.build.skyframe.EvaluationResult;
+import com.google.devtools.build.skyframe.Injectable;
+import com.google.devtools.build.skyframe.MemoizingEvaluator;
+import com.google.devtools.build.skyframe.MemoizingEvaluator.EvaluatorSupplier;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionName;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.logging.Logger;
+
+import javax.annotation.Nullable;
+
+/**
+ * A helper object to support Skyframe-driven execution.
+ *
+ * <p>This object is mostly used to inject external state, such as the executor engine or
+ * some additional artifacts (workspace status and build info artifacts) into SkyFunctions
+ * for use during the build.
+ */
+public abstract class SkyframeExecutor {
+ private final EvaluatorSupplier evaluatorSupplier;
+ protected MemoizingEvaluator memoizingEvaluator;
+ private final MemoizingEvaluator.EmittedEventState emittedEventState =
+ new MemoizingEvaluator.EmittedEventState();
+ protected final Reporter reporter;
+ private final PackageFactory pkgFactory;
+ private final WorkspaceStatusAction.Factory workspaceStatusActionFactory;
+ private final BlazeDirectories directories;
+ @Nullable
+ private BatchStat batchStatter;
+
+ // TODO(bazel-team): Figure out how to handle value builders that block internally. Blocking
+ // operations may need to be handled in another (bigger?) thread pool. Also, we should detect
+ // the number of cores and use that as the thread-pool size for CPU-bound operations.
+ // I just bumped this to 200 to get reasonable execution phase performance; that may cause
+ // significant overhead for CPU-bound processes (i.e. analysis). [skyframe-analysis]
+ @VisibleForTesting
+ public static final int DEFAULT_THREAD_COUNT = 200;
+
+ // Stores Packages between reruns of the PackageFunction (because of missing dependencies,
+ // within the same evaluate() run) to avoid loading the same package twice (first time loading
+ // to find subincludes and declare value dependencies).
+ // TODO(bazel-team): remove this cache once we have skyframe-native package loading
+ // [skyframe-loading]
+ private final ConcurrentMap<PackageIdentifier, Package.LegacyBuilder> packageFunctionCache =
+ Maps.newConcurrentMap();
+ private final AtomicInteger numPackagesLoaded = new AtomicInteger(0);
+
+ protected SkyframeBuildView skyframeBuildView;
+ private EventHandler errorEventListener;
+ private ActionLogBufferPathGenerator actionLogBufferPathGenerator;
+
+ protected BuildDriver buildDriver;
+
+ // AtomicReferences are used here as mutable boxes shared with value builders.
+ private final AtomicBoolean showLoadingProgress = new AtomicBoolean();
+ protected final AtomicReference<UnixGlob.FilesystemCalls> syscalls =
+ new AtomicReference<>(UnixGlob.DEFAULT_SYSCALLS);
+ protected final AtomicReference<PathPackageLocator> pkgLocator =
+ new AtomicReference<>();
+ protected final AtomicReference<ImmutableSet<String>> deletedPackages =
+ new AtomicReference<>(ImmutableSet.<String>of());
+ private final AtomicReference<EventBus> eventBus = new AtomicReference<>();
+
+ private final ImmutableList<BuildInfoFactory> buildInfoFactories;
+ // Under normal circumstances, the artifact factory persists for the life of a Blaze server, but
+ // since it is not yet created when we create the value builders, we have to use a supplier,
+ // initialized when the build view is created.
+ private final MutableSupplier<ArtifactFactory> artifactFactory = new MutableSupplier<>();
+ // Used to give to WriteBuildInfoAction via a supplier. Relying on BuildVariableValue.BUILD_ID
+ // would be preferable, but we have no way to have the Action depend on that value directly.
+ // Having the BuildInfoFunction own the supplier is currently not possible either, because then
+ // it would be invalidated on every build, since it would depend on the build id value.
+ private MutableSupplier<UUID> buildId = new MutableSupplier<>();
+
+ protected boolean active = true;
+ private final PackageManager packageManager;
+
+ private final Preprocessor.Factory.Supplier preprocessorFactorySupplier;
+ private Preprocessor.Factory preprocessorFactory;
+
+ protected final TimestampGranularityMonitor tsgm;
+
+ private final ResourceManager resourceManager;
+
+ /** Used to lock evaluator on legacy calls to get existing values. */
+ private final Object valueLookupLock = new Object();
+ private final AtomicReference<ActionExecutionStatusReporter> statusReporterRef =
+ new AtomicReference<>();
+ private final SkyframeActionExecutor skyframeActionExecutor;
+ protected SkyframeProgressReceiver progressReceiver;
+ private final AtomicReference<CyclesReporter> cyclesReporter = new AtomicReference<>();
+
+ private final Set<Path> immutableDirectories;
+
+ private BinTools binTools = null;
+ private boolean needToInjectEmbeddedArtifacts = true;
+ private boolean needToInjectPrecomputedValuesForAnalysis = true;
+ protected int modifiedFiles;
+ private final Predicate<PathFragment> allowedMissingInputs;
+
+ private final ImmutableMap<SkyFunctionName, SkyFunction> extraSkyFunctions;
+ private final ImmutableList<PrecomputedValue.Injected> extraPrecomputedValues;
+
+ protected SkyframeIncrementalBuildMonitor incrementalBuildMonitor =
+ new SkyframeIncrementalBuildMonitor();
+
+ private MutableSupplier<ConfigurationFactory> configurationFactory = new MutableSupplier<>();
+ private MutableSupplier<Map<String, String>> clientEnv = new MutableSupplier<>();
+ private MutableSupplier<ImmutableList<ConfigurationFragmentFactory>> configurationFragments =
+ new MutableSupplier<>();
+ private MutableSupplier<Set<Package>> configurationPackages = new MutableSupplier<>();
+ private SkyKey configurationSkyKey = null;
+
+ private static final Logger LOG = Logger.getLogger(SkyframeExecutor.class.getName());
+
+ protected SkyframeExecutor(
+ Reporter reporter,
+ EvaluatorSupplier evaluatorSupplier,
+ PackageFactory pkgFactory,
+ TimestampGranularityMonitor tsgm,
+ BlazeDirectories directories,
+ Factory workspaceStatusActionFactory,
+ ImmutableList<BuildInfoFactory> buildInfoFactories,
+ Set<Path> immutableDirectories,
+ Predicate<PathFragment> allowedMissingInputs,
+ Preprocessor.Factory.Supplier preprocessorFactorySupplier,
+ ImmutableMap<SkyFunctionName, SkyFunction> extraSkyFunctions,
+ ImmutableList<PrecomputedValue.Injected> extraPrecomputedValues) {
+ // Strictly speaking, these arguments are not required for initialization, but all current
+ // callsites have them at hand, so we might as well set them during construction.
+ this.reporter = Preconditions.checkNotNull(reporter);
+ this.evaluatorSupplier = evaluatorSupplier;
+ this.pkgFactory = pkgFactory;
+ this.pkgFactory.setSyscalls(syscalls);
+ this.tsgm = tsgm;
+ this.workspaceStatusActionFactory = workspaceStatusActionFactory;
+ this.packageManager = new SkyframePackageManager(
+ new SkyframePackageLoader(), new SkyframeTransitivePackageLoader(),
+ new SkyframeTargetPatternEvaluator(this), syscalls, cyclesReporter, pkgLocator,
+ numPackagesLoaded, this);
+ this.errorEventListener = this.reporter;
+ this.resourceManager = ResourceManager.instance();
+ this.skyframeActionExecutor = new SkyframeActionExecutor(reporter, resourceManager, eventBus,
+ statusReporterRef);
+ this.directories = Preconditions.checkNotNull(directories);
+ this.buildInfoFactories = buildInfoFactories;
+ this.immutableDirectories = immutableDirectories;
+ this.allowedMissingInputs = allowedMissingInputs;
+ this.preprocessorFactorySupplier = preprocessorFactorySupplier;
+ this.extraSkyFunctions = extraSkyFunctions;
+ this.extraPrecomputedValues = extraPrecomputedValues;
+ }
+
+ private ImmutableMap<SkyFunctionName, SkyFunction> skyFunctions(
+ Root buildDataDirectory,
+ PackageFactory pkgFactory,
+ Predicate<PathFragment> allowedMissingInputs) {
+ ExternalFilesHelper externalFilesHelper = new ExternalFilesHelper(pkgLocator,
+ immutableDirectories);
+ // We use an immutable map builder for the nice side effect that it throws if a duplicate key
+ // is inserted.
+ ImmutableMap.Builder<SkyFunctionName, SkyFunction> map = ImmutableMap.builder();
+ map.put(SkyFunctions.PRECOMPUTED, new PrecomputedFunction());
+ map.put(SkyFunctions.FILE_STATE, new FileStateFunction(tsgm, externalFilesHelper));
+ map.put(SkyFunctions.DIRECTORY_LISTING_STATE,
+ new DirectoryListingStateFunction(externalFilesHelper));
+ map.put(SkyFunctions.FILE_SYMLINK_CYCLE_UNIQUENESS,
+ new FileSymlinkCycleUniquenessFunction());
+ map.put(SkyFunctions.FILE, new FileFunction(pkgLocator, externalFilesHelper));
+ map.put(SkyFunctions.DIRECTORY_LISTING, new DirectoryListingFunction());
+ map.put(SkyFunctions.PACKAGE_LOOKUP, new PackageLookupFunction(deletedPackages));
+ map.put(SkyFunctions.CONTAINING_PACKAGE_LOOKUP, new ContainingPackageLookupFunction());
+ map.put(SkyFunctions.AST_FILE_LOOKUP, new ASTFileLookupFunction(
+ pkgLocator, packageManager, pkgFactory.getRuleClassProvider()));
+ map.put(SkyFunctions.SKYLARK_IMPORTS_LOOKUP, new SkylarkImportLookupFunction(
+ pkgFactory.getRuleClassProvider(), pkgFactory));
+ map.put(SkyFunctions.GLOB, new GlobFunction());
+ map.put(SkyFunctions.TARGET_PATTERN, new TargetPatternFunction(pkgLocator));
+ map.put(SkyFunctions.RECURSIVE_PKG, new RecursivePkgFunction());
+ map.put(SkyFunctions.PACKAGE, new PackageFunction(
+ reporter, pkgFactory, packageManager, showLoadingProgress, packageFunctionCache,
+ eventBus, numPackagesLoaded));
+ map.put(SkyFunctions.TARGET_MARKER, new TargetMarkerFunction());
+ map.put(SkyFunctions.TRANSITIVE_TARGET, new TransitiveTargetFunction());
+ map.put(SkyFunctions.CONFIGURED_TARGET,
+ new ConfiguredTargetFunction(new BuildViewProvider()));
+ map.put(SkyFunctions.ASPECT, new AspectFunction(new BuildViewProvider()));
+ map.put(SkyFunctions.POST_CONFIGURED_TARGET,
+ new PostConfiguredTargetFunction(new BuildViewProvider()));
+ map.put(SkyFunctions.CONFIGURATION_COLLECTION, new ConfigurationCollectionFunction(
+ configurationFactory, clientEnv, configurationPackages));
+ map.put(SkyFunctions.CONFIGURATION_FRAGMENT, new ConfigurationFragmentFunction(
+ configurationFragments, configurationPackages));
+ map.put(SkyFunctions.WORKSPACE_FILE, new WorkspaceFileFunction(pkgFactory));
+ map.put(SkyFunctions.TARGET_COMPLETION, new TargetCompletionFunction(eventBus));
+ map.put(SkyFunctions.TEST_COMPLETION, new TestCompletionFunction());
+ map.put(SkyFunctions.ARTIFACT, new ArtifactFunction(allowedMissingInputs));
+ map.put(SkyFunctions.BUILD_INFO_COLLECTION, new BuildInfoCollectionFunction(artifactFactory,
+ buildDataDirectory));
+ map.put(SkyFunctions.BUILD_INFO, new WorkspaceStatusFunction());
+ map.put(SkyFunctions.COVERAGE_REPORT, new CoverageReportFunction());
+ map.put(SkyFunctions.ACTION_EXECUTION,
+ new ActionExecutionFunction(skyframeActionExecutor, tsgm));
+ map.put(SkyFunctions.RECURSIVE_FILESYSTEM_TRAVERSAL,
+ new RecursiveFilesystemTraversalFunction());
+ map.put(SkyFunctions.FILESET_ENTRY, new FilesetEntryFunction());
+ map.putAll(extraSkyFunctions);
+ return map.build();
+ }
+
+ @ThreadCompatible
+ public void setActive(boolean active) {
+ this.active = active;
+ }
+
+ protected void checkActive() {
+ Preconditions.checkState(active);
+ }
+
+ public void setFileCache(ActionInputFileCache fileCache) {
+ this.skyframeActionExecutor.setFileCache(fileCache);
+ }
+
+ public void dump(boolean summarize, PrintStream out) {
+ memoizingEvaluator.dump(summarize, out);
+ }
+
+ public abstract void dumpPackages(PrintStream out);
+
+ public void setBatchStatter(@Nullable BatchStat batchStatter) {
+ this.batchStatter = batchStatter;
+ }
+
+ /**
+ * Notify listeners about changed files, and release any associated memory afterwards.
+ */
+ public void drainChangedFiles() {
+ incrementalBuildMonitor.alertListeners(getEventBus());
+ incrementalBuildMonitor = null;
+ }
+
+ @VisibleForTesting
+ public BuildDriver getDriverForTesting() {
+ return buildDriver;
+ }
+
+ /**
+ * This method exists only to allow a module to make a top-level Skyframe call during the
+ * transition to making it fully Skyframe-compatible. Do not add additional callers!
+ */
+ public <E extends Exception> SkyValue evaluateSkyKeyForCodeMigration(final SkyKey key,
+ final Class<E> clazz) throws E {
+ try {
+ return callUninterruptibly(new Callable<SkyValue>() {
+ @Override
+ public SkyValue call() throws E, InterruptedException {
+ synchronized (valueLookupLock) {
+ // We evaluate in keepGoing mode because in the case that the graph does not store its
+ // edges, nokeepGoing builds are not allowed, whereas keepGoing builds are always
+ // permitted.
+ EvaluationResult<ActionLookupValue> result = buildDriver.evaluate(
+ ImmutableList.of(key), true, ResourceUsage.getAvailableProcessors(),
+ errorEventListener);
+ if (!result.hasError()) {
+ return Preconditions.checkNotNull(result.get(key), "%s %s", result, key);
+ }
+ ErrorInfo errorInfo = Preconditions.checkNotNull(result.getError(key),
+ "%s %s", key, result);
+ Throwables.propagateIfPossible(errorInfo.getException(), clazz);
+ if (errorInfo.getException() != null) {
+ throw new IllegalStateException(errorInfo.getException());
+ }
+ throw new IllegalStateException(errorInfo.toString());
+ }
+ }
+ });
+ } catch (Exception e) {
+ Throwables.propagateIfPossible(e, clazz);
+ throw new IllegalStateException(e);
+ }
+ }
+
+ class BuildViewProvider {
+ /**
+ * Returns the current {@link SkyframeBuildView} instance.
+ */
+ SkyframeBuildView getSkyframeBuildView() {
+ return skyframeBuildView;
+ }
+ }
+
+ /**
+ * Must be called before the {@link SkyframeExecutor} can be used (should only be called in
+ * factory methods and as an implementation detail of {@link #resetEvaluator}).
+ */
+ protected void init() {
+ progressReceiver = new SkyframeProgressReceiver();
+ Map<SkyFunctionName, SkyFunction> skyFunctions = skyFunctions(
+ directories.getBuildDataDirectory(), pkgFactory, allowedMissingInputs);
+ memoizingEvaluator = evaluatorSupplier.create(
+ skyFunctions, evaluatorDiffer(), progressReceiver, emittedEventState,
+ hasIncrementalState());
+ buildDriver = newBuildDriver();
+ }
+
+ /**
+ * Reinitializes the Skyframe evaluator, dropping all previously computed values.
+ *
+ * <p>Be careful with this method as it also deletes all injected values. You need to make sure
+ * that any necessary precomputed values are reinjected before the next build. Constants can be
+ * put in {@link #reinjectConstantValuesLazily}.
+ */
+ public void resetEvaluator() {
+ init();
+ emittedEventState.clear();
+ if (skyframeBuildView != null) {
+ skyframeBuildView.clearLegacyData();
+ }
+ reinjectConstantValuesLazily();
+ }
+
+ protected abstract Differencer evaluatorDiffer();
+
+ protected abstract BuildDriver newBuildDriver();
+
+ /**
+ * Values whose values are known at startup and guaranteed constant are still wiped from the
+ * evaluator when we create a new one, so they must be re-injected each time we create a new
+ * evaluator.
+ */
+ private void reinjectConstantValuesLazily() {
+ needToInjectEmbeddedArtifacts = true;
+ needToInjectPrecomputedValuesForAnalysis = true;
+ }
+
+ /**
+ * Deletes all ConfiguredTarget values from the Skyframe cache. This is done to save memory (e.g.
+ * on a configuration change); since the configuration is part of the key, these key/value pairs
+ * will be sitting around doing nothing until the configuration changes back to the previous
+ * value.
+ *
+ * <p>The next evaluation will delete all invalid values.
+ */
+ public abstract void dropConfiguredTargets();
+
+ /**
+ * Removes ConfigurationFragmentValuess and ConfigurationCollectionValues from the cache.
+ */
+ @VisibleForTesting
+ public void invalidateConfigurationCollection() {
+ invalidate(SkyFunctionName.functionIsIn(ImmutableSet.of(SkyFunctions.CONFIGURATION_FRAGMENT,
+ SkyFunctions.CONFIGURATION_COLLECTION)));
+ }
+
+ /**
+ * Decides if graph edges should be stored for this build. If not, re-creates the graph to not
+ * store graph edges. Necessary conditions to not store graph edges are:
+ * (1) batch (since incremental builds are not possible);
+ * (2) skyframe build (since otherwise the memory savings are too slight to bother);
+ * (3) keep-going (since otherwise bubbling errors up may require edges of done nodes);
+ * (4) discard_analysis_cache (since otherwise user isn't concerned about saving memory this way).
+ */
+ public void decideKeepIncrementalState(boolean batch, Options viewOptions) {
+ // Assume incrementality.
+ }
+
+ public boolean hasIncrementalState() {
+ return true;
+ }
+
+ @VisibleForTesting
+ protected abstract Injectable injectable();
+
+ /**
+ * Saves memory by clearing analysis objects from Skyframe. If using legacy execution, actually
+ * deletes the relevant values. If using Skyframe execution, clears their data without deleting
+ * them (they will be deleted on the next build).
+ */
+ public abstract void clearAnalysisCache(Collection<ConfiguredTarget> topLevelTargets);
+
+ /**
+ * Injects the contents of the computed tools/defaults package.
+ */
+ @VisibleForTesting
+ public void setupDefaultPackage(String defaultsPackageContents) {
+ PrecomputedValue.DEFAULTS_PACKAGE_CONTENTS.set(injectable(), defaultsPackageContents);
+ }
+
+ /**
+ * Injects the top-level artifact options.
+ */
+ public void injectTopLevelContext(TopLevelArtifactContext options) {
+ PrecomputedValue.TOP_LEVEL_CONTEXT.set(injectable(), options);
+ }
+
+ public void injectWorkspaceStatusData() {
+ PrecomputedValue.WORKSPACE_STATUS_KEY.set(injectable(),
+ workspaceStatusActionFactory.createWorkspaceStatusAction(
+ artifactFactory.get(), WorkspaceStatusValue.ARTIFACT_OWNER, buildId));
+ }
+
+ public void injectCoverageReportData(Action action) {
+ PrecomputedValue.COVERAGE_REPORT_KEY.set(injectable(), action);
+ }
+
+ /**
+ * Sets the default visibility.
+ */
+ private void setDefaultVisibility(RuleVisibility defaultVisibility) {
+ PrecomputedValue.DEFAULT_VISIBILITY.set(injectable(), defaultVisibility);
+ }
+
+ private void maybeInjectPrecomputedValuesForAnalysis() {
+ if (needToInjectPrecomputedValuesForAnalysis) {
+ injectBuildInfoFactories();
+ injectExtraPrecomputedValues();
+ needToInjectPrecomputedValuesForAnalysis = false;
+ }
+ }
+
+ private void injectExtraPrecomputedValues() {
+ for (PrecomputedValue.Injected injected : extraPrecomputedValues) {
+ injected.inject(injectable());
+ }
+ }
+
+ /**
+ * Injects the build info factory map that will be used when constructing build info
+ * actions/artifacts. Unchanged across the life of the Blaze server, although it must be injected
+ * each time the evaluator is created.
+ */
+ private void injectBuildInfoFactories() {
+ ImmutableMap.Builder<BuildInfoKey, BuildInfoFactory> factoryMapBuilder =
+ ImmutableMap.builder();
+ for (BuildInfoFactory factory : buildInfoFactories) {
+ factoryMapBuilder.put(factory.getKey(), factory);
+ }
+ PrecomputedValue.BUILD_INFO_FACTORIES.set(injectable(), factoryMapBuilder.build());
+ }
+
+ private void setShowLoadingProgress(boolean showLoadingProgressValue) {
+ showLoadingProgress.set(showLoadingProgressValue);
+ }
+
+ @VisibleForTesting
+ public void setCommandId(UUID commandId) {
+ PrecomputedValue.BUILD_ID.set(injectable(), commandId);
+ buildId.set(commandId);
+ }
+
+ /** Returns the build-info.txt and build-changelist.txt artifacts. */
+ public Collection<Artifact> getWorkspaceStatusArtifacts() throws InterruptedException {
+ // Should already be present, unless the user didn't request any targets for analysis.
+ EvaluationResult<WorkspaceStatusValue> result = buildDriver.evaluate(
+ ImmutableList.of(WorkspaceStatusValue.SKY_KEY), /*keepGoing=*/true, /*numThreads=*/1,
+ reporter);
+ WorkspaceStatusValue value =
+ Preconditions.checkNotNull(result.get(WorkspaceStatusValue.SKY_KEY));
+ return ImmutableList.of(value.getStableArtifact(), value.getVolatileArtifact());
+ }
+
+ // TODO(bazel-team): Make this take a PackageIdentifier.
+ public Map<PathFragment, Root> getArtifactRoots(Iterable<PathFragment> execPaths) {
+ final List<SkyKey> packageKeys = new ArrayList<>();
+ for (PathFragment execPath : execPaths) {
+ Preconditions.checkArgument(!execPath.isAbsolute(), execPath);
+ packageKeys.add(ContainingPackageLookupValue.key(
+ PackageIdentifier.createInDefaultRepo(execPath)));
+ }
+
+ EvaluationResult<ContainingPackageLookupValue> result;
+ try {
+ result = callUninterruptibly(new Callable<EvaluationResult<ContainingPackageLookupValue>>() {
+ @Override
+ public EvaluationResult<ContainingPackageLookupValue> call() throws InterruptedException {
+ return buildDriver.evaluate(packageKeys, /*keepGoing=*/true, /*numThreads=*/1, reporter);
+ }
+ });
+ } catch (Exception e) {
+ throw new IllegalStateException(e); // Should never happen.
+ }
+
+ Map<PathFragment, Root> roots = new HashMap<>();
+ for (PathFragment execPath : execPaths) {
+ ContainingPackageLookupValue value = result.get(ContainingPackageLookupValue.key(
+ PackageIdentifier.createInDefaultRepo(execPath)));
+ if (value.hasContainingPackage()) {
+ roots.put(execPath, Root.asSourceRoot(value.getContainingPackageRoot()));
+ } else {
+ roots.put(execPath, null);
+ }
+ }
+ return roots;
+ }
+
+ @VisibleForTesting
+ public WorkspaceStatusAction getLastWorkspaceStatusActionForTesting() {
+ PrecomputedValue value = (PrecomputedValue) buildDriver.getGraphForTesting()
+ .getExistingValueForTesting(PrecomputedValue.WORKSPACE_STATUS_KEY.getKeyForTesting());
+ return (WorkspaceStatusAction) value.get();
+ }
+
+ /**
+ * Informs user about number of modified files (source and output files).
+ */
+ // Note, that number of modified files in some cases can be bigger than actual number of
+ // modified files for targets in current request. Skyframe may check for modification all files
+ // from previous requests.
+ protected void informAboutNumberOfModifiedFiles() {
+ LOG.info(String.format("Found %d modified files from last build", modifiedFiles));
+ }
+
+ public Reporter getReporter() {
+ return reporter;
+ }
+
+ public EventBus getEventBus() {
+ return eventBus.get();
+ }
+
+ /**
+ * The map from package names to the package root where each package was found; this is used to
+ * set up the symlink tree.
+ */
+ public ImmutableMap<PackageIdentifier, Path> getPackageRoots() {
+ // Make a map of the package names to their root paths.
+ ImmutableMap.Builder<PackageIdentifier, Path> packageRoots = ImmutableMap.builder();
+ for (Package pkg : configurationPackages.get()) {
+ packageRoots.put(pkg.getPackageIdentifier(), pkg.getSourceRoot());
+ }
+ return packageRoots.build();
+ }
+
+ @VisibleForTesting
+ ImmutableList<Path> getPathEntries() {
+ return pkgLocator.get().getPathEntries();
+ }
+
+ protected abstract void invalidate(Predicate<SkyKey> pred);
+
+ protected static Iterable<SkyKey> getSkyKeysPotentiallyAffected(
+ Iterable<PathFragment> modifiedSourceFiles, final Path pathEntry) {
+ // TODO(bazel-team): change ModifiedFileSet to work with RootedPaths instead of PathFragments.
+ Iterable<SkyKey> fileStateSkyKeys = Iterables.transform(modifiedSourceFiles,
+ new Function<PathFragment, SkyKey>() {
+ @Override
+ public SkyKey apply(PathFragment pathFragment) {
+ Preconditions.checkState(!pathFragment.isAbsolute(),
+ "found absolute PathFragment: %s", pathFragment);
+ return FileStateValue.key(RootedPath.toRootedPath(pathEntry, pathFragment));
+ }
+ });
+ // TODO(bazel-team): Strictly speaking, we only need to invalidate directory values when a file
+ // has been created or deleted, not when it has been modified. Unfortunately we
+ // do not have that information here, although fancy filesystems could provide it with a
+ // hypothetically modified DiffAwareness interface.
+ // TODO(bazel-team): Even if we don't have that information, we could avoid invalidating
+ // directories when the state of a file does not change by statting them and comparing
+ // the new filetype (nonexistent/file/symlink/directory) with the old one.
+ Iterable<SkyKey> dirListingStateSkyKeys = Iterables.transform(
+ modifiedSourceFiles,
+ new Function<PathFragment, SkyKey>() {
+ @Override
+ public SkyKey apply(PathFragment pathFragment) {
+ Preconditions.checkState(!pathFragment.isAbsolute(),
+ "found absolute PathFragment: %s", pathFragment);
+ return DirectoryListingStateValue.key(RootedPath.toRootedPath(pathEntry,
+ pathFragment.getParentDirectory()));
+ }
+ });
+ return Iterables.concat(fileStateSkyKeys, dirListingStateSkyKeys);
+ }
+
+ protected static SkyKey createFileStateKey(RootedPath rootedPath) {
+ return FileStateValue.key(rootedPath);
+ }
+
+ protected static SkyKey createDirectoryListingStateKey(RootedPath rootedPath) {
+ return DirectoryListingStateValue.key(rootedPath);
+ }
+
+ /**
+ * Creates a FileValue pointing of type directory. No matter that the rootedPath points to a
+ * symlink.
+ *
+ * <p> Use it with caution as it would prevent invalidation when the destination file in the
+ * symlink changes.
+ */
+ protected static FileValue createFileDirValue(RootedPath rootedPath) {
+ return FileValue.value(rootedPath, FileStateValue.DIRECTORY_FILE_STATE_NODE,
+ rootedPath, FileStateValue.DIRECTORY_FILE_STATE_NODE);
+ }
+
+ /**
+ * Sets the packages that should be treated as deleted and ignored.
+ */
+ @VisibleForTesting // productionVisibility = Visibility.PRIVATE
+ public abstract void setDeletedPackages(Iterable<String> pkgs);
+
+ /**
+ * Prepares the evaluator for loading.
+ *
+ * <p>MUST be run before every incremental build.
+ */
+ @VisibleForTesting // productionVisibility = Visibility.PRIVATE
+ public void preparePackageLoading(PathPackageLocator pkgLocator, RuleVisibility defaultVisibility,
+ boolean showLoadingProgress,
+ String defaultsPackageContents, UUID commandId) {
+ Preconditions.checkNotNull(pkgLocator);
+ setActive(true);
+
+ maybeInjectPrecomputedValuesForAnalysis();
+ setCommandId(commandId);
+ setShowLoadingProgress(showLoadingProgress);
+ setDefaultVisibility(defaultVisibility);
+ setupDefaultPackage(defaultsPackageContents);
+ setPackageLocator(pkgLocator);
+
+ syscalls.set(new PerBuildSyscallCache());
+ checkPreprocessorFactory();
+ emittedEventState.clear();
+
+ // If the PackageFunction was interrupted, there may be stale entries here.
+ packageFunctionCache.clear();
+ numPackagesLoaded.set(0);
+
+ // Reset the stateful SkyframeCycleReporter, which contains cycles from last run.
+ cyclesReporter.set(createCyclesReporter());
+ }
+
+ @SuppressWarnings("unchecked")
+ private void setPackageLocator(PathPackageLocator pkgLocator) {
+ PathPackageLocator oldLocator = this.pkgLocator.getAndSet(pkgLocator);
+ PrecomputedValue.PATH_PACKAGE_LOCATOR.set(injectable(), pkgLocator);
+
+ if (!pkgLocator.equals(oldLocator)) {
+ // The package path is read not only by SkyFunctions but also by some other code paths.
+ // We need to take additional steps to keep the corresponding data structures in sync.
+ // (Some of the additional steps are carried out by ConfiguredTargetValueInvalidationListener,
+ // and some by BuildView#buildHasIncompatiblePackageRoots and #updateSkyframe.)
+ onNewPackageLocator(oldLocator, pkgLocator);
+ }
+ }
+
+ protected abstract void onNewPackageLocator(PathPackageLocator oldLocator,
+ PathPackageLocator pkgLocator);
+
+ private void checkPreprocessorFactory() {
+ if (preprocessorFactory == null) {
+ Preprocessor.Factory newPreprocessorFactory = preprocessorFactorySupplier.getFactory(
+ packageManager);
+ pkgFactory.setPreprocessorFactory(newPreprocessorFactory);
+ preprocessorFactory = newPreprocessorFactory;
+ } else if (!preprocessorFactory.isStillValid()) {
+ Preprocessor.Factory newPreprocessorFactory = preprocessorFactorySupplier.getFactory(
+ packageManager);
+ invalidate(SkyFunctionName.functionIs(SkyFunctions.PACKAGE));
+ pkgFactory.setPreprocessorFactory(newPreprocessorFactory);
+ preprocessorFactory = newPreprocessorFactory;
+ }
+ }
+
+ /**
+ * Specifies the current {@link SkyframeBuildView} instance. This should only be set once over the
+ * lifetime of the Blaze server, except in tests.
+ */
+ public void setSkyframeBuildView(SkyframeBuildView skyframeBuildView) {
+ this.skyframeBuildView = skyframeBuildView;
+ setConfigurationSkyKey(configurationSkyKey);
+ this.artifactFactory.set(skyframeBuildView.getArtifactFactory());
+ if (skyframeBuildView.getWarningListener() != null) {
+ setErrorEventListener(skyframeBuildView.getWarningListener());
+ }
+ }
+
+ /**
+ * Sets the eventBus to use for posting events.
+ */
+ public void setEventBus(EventBus eventBus) {
+ this.eventBus.set(eventBus);
+ }
+
+ /**
+ * Sets the eventHandler to use for reporting errors.
+ */
+ public void setErrorEventListener(EventHandler eventHandler) {
+ this.errorEventListener = eventHandler;
+ }
+
+ /**
+ * Sets the path for action log buffers.
+ */
+ public void setActionOutputRoot(Path actionOutputRoot) {
+ Preconditions.checkNotNull(actionOutputRoot);
+ this.actionLogBufferPathGenerator = new ActionLogBufferPathGenerator(actionOutputRoot);
+ this.skyframeActionExecutor.setActionLogBufferPathGenerator(actionLogBufferPathGenerator);
+ }
+
+ private void setConfigurationSkyKey(SkyKey skyKey) {
+ this.configurationSkyKey = skyKey;
+ if (skyframeBuildView != null) {
+ skyframeBuildView.setConfigurationSkyKey(skyKey);
+ }
+ }
+
+ @VisibleForTesting
+ public void setConfigurationDataForTesting(BuildOptions options,
+ BlazeDirectories directories, ConfigurationFactory configurationFactory) {
+ SkyKey skyKey = ConfigurationCollectionValue.key(options, ImmutableSet.<String>of());
+ setConfigurationSkyKey(skyKey);
+ PrecomputedValue.BLAZE_DIRECTORIES.set(injectable(), directories);
+ this.configurationFactory.set(configurationFactory);
+ this.configurationFragments.set(ImmutableList.copyOf(configurationFactory.getFactories()));
+ this.configurationPackages.set(Sets.<Package>newConcurrentHashSet());
+ }
+
+ @VisibleForTesting
+ public BuildConfigurationCollection createConfigurations(
+ ConfigurationFactory configurationFactory, BuildConfigurationKey configurationKey)
+ throws InvalidConfigurationException, InterruptedException {
+ return createConfigurations(false, configurationFactory, configurationKey);
+ }
+
+ /**
+ * Asks the Skyframe evaluator to build the value for BuildConfigurationCollection and
+ * returns result. Also invalidates {@link PrecomputedValue#TEST_ENVIRONMENT_VARIABLES} and
+ * {@link PrecomputedValue#BLAZE_DIRECTORIES} if they have changed.
+ */
+ public BuildConfigurationCollection createConfigurations(boolean keepGoing,
+ ConfigurationFactory configurationFactory, BuildConfigurationKey configurationKey)
+ throws InvalidConfigurationException, InterruptedException {
+
+ this.configurationPackages.set(Sets.<Package>newConcurrentHashSet());
+ this.clientEnv.set(configurationKey.getClientEnv());
+ this.configurationFactory.set(configurationFactory);
+ this.configurationFragments.set(ImmutableList.copyOf(configurationFactory.getFactories()));
+ BuildOptions buildOptions = configurationKey.getBuildOptions();
+ Map<String, String> testEnv = BuildConfiguration.getTestEnv(
+ buildOptions.get(BuildConfiguration.Options.class).testEnvironment,
+ configurationKey.getClientEnv());
+ // TODO(bazel-team): find a way to use only BuildConfigurationKey instead of
+ // TestEnvironmentVariables and BlazeDirectories. There is a problem only with
+ // TestEnvironmentVariables because BuildConfigurationKey stores client environment variables
+ // and we don't want to rebuild everything when any variable changes.
+ PrecomputedValue.TEST_ENVIRONMENT_VARIABLES.set(injectable(), testEnv);
+ PrecomputedValue.BLAZE_DIRECTORIES.set(injectable(), configurationKey.getDirectories());
+
+ SkyKey skyKey = ConfigurationCollectionValue.key(configurationKey.getBuildOptions(),
+ configurationKey.getMultiCpu());
+ setConfigurationSkyKey(skyKey);
+ EvaluationResult<ConfigurationCollectionValue> result = buildDriver.evaluate(
+ Arrays.asList(skyKey), keepGoing, DEFAULT_THREAD_COUNT, errorEventListener);
+ if (result.hasError()) {
+ Throwable e = result.getError(skyKey).getException();
+ // Wrap loading failed exceptions
+ if (e instanceof NoSuchThingException) {
+ e = new InvalidConfigurationException(e);
+ }
+ Throwables.propagateIfInstanceOf(e, InvalidConfigurationException.class);
+ throw new IllegalStateException(
+ "Unknown error during ConfigurationCollectionValue evaluation", e);
+ }
+ Preconditions.checkState(result.values().size() == 1,
+ "Result of evaluate() must contain exactly one value %s", result);
+ ConfigurationCollectionValue configurationValue =
+ Iterables.getOnlyElement(result.values());
+ this.configurationPackages.set(
+ Sets.newConcurrentHashSet(configurationValue.getConfigurationPackages()));
+ return configurationValue.getConfigurationCollection();
+ }
+
+ private Iterable<ActionLookupValue> getActionLookupValues() {
+ // This filter keeps subclasses of ActionLookupValue.
+ return Iterables.filter(memoizingEvaluator.getDoneValues().values(), ActionLookupValue.class);
+ }
+
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ Map<SkyKey, ActionLookupValue> getActionLookupValueMap() {
+ return (Map) Maps.filterValues(memoizingEvaluator.getDoneValues(),
+ Predicates.instanceOf(ActionLookupValue.class));
+ }
+
+ /**
+ * Checks the actions in Skyframe for conflicts between their output artifacts. Delegates to
+ * {@link SkyframeActionExecutor#findAndStoreArtifactConflicts} to do the work, since any
+ * conflicts found will only be reported during execution.
+ */
+ ImmutableMap<Action, SkyframeActionExecutor.ConflictException> findArtifactConflicts()
+ throws InterruptedException {
+ if (skyframeBuildView.isSomeConfiguredTargetEvaluated()
+ || skyframeBuildView.isSomeConfiguredTargetInvalidated()) {
+ // This operation is somewhat expensive, so we only do it if the graph might have changed in
+ // some way -- either we analyzed a new target or we invalidated an old one.
+ skyframeActionExecutor.findAndStoreArtifactConflicts(getActionLookupValues());
+ skyframeBuildView.resetEvaluatedConfiguredTargetFlag();
+ // The invalidated configured targets flag will be reset later in the evaluate() call.
+ }
+ return skyframeActionExecutor.badActions();
+ }
+
+ /**
+ * Asks the Skyframe evaluator to build the given artifacts and targets, and to test the
+ * given test targets.
+ */
+ public EvaluationResult<?> buildArtifacts(
+ Executor executor,
+ Set<Artifact> artifactsToBuild,
+ Collection<ConfiguredTarget> targetsToBuild,
+ Collection<ConfiguredTarget> targetsToTest,
+ boolean exclusiveTesting,
+ boolean keepGoing,
+ boolean explain,
+ int numJobs,
+ ActionCacheChecker actionCacheChecker,
+ @Nullable EvaluationProgressReceiver executionProgressReceiver) throws InterruptedException {
+ checkActive();
+ Preconditions.checkState(actionLogBufferPathGenerator != null);
+
+ skyframeActionExecutor.prepareForExecution(executor, keepGoing, explain, actionCacheChecker);
+
+ resourceManager.resetResourceUsage();
+ try {
+ progressReceiver.executionProgressReceiver = executionProgressReceiver;
+ Iterable<SkyKey> artifactKeys = ArtifactValue.mandatoryKeys(artifactsToBuild);
+ Iterable<SkyKey> targetKeys = TargetCompletionValue.keys(targetsToBuild);
+ Iterable<SkyKey> testKeys = TestCompletionValue.keys(targetsToTest, exclusiveTesting);
+ return buildDriver.evaluate(Iterables.concat(artifactKeys, targetKeys, testKeys), keepGoing,
+ numJobs, errorEventListener);
+ } finally {
+ progressReceiver.executionProgressReceiver = null;
+ // Also releases thread locks.
+ resourceManager.resetResourceUsage();
+ skyframeActionExecutor.executionOver();
+ }
+ }
+
+ @VisibleForTesting
+ public void prepareBuildingForTestingOnly(Executor executor, boolean keepGoing, boolean explain,
+ ActionCacheChecker checker) {
+ skyframeActionExecutor.prepareForExecution(executor, keepGoing, explain, checker);
+ }
+
+ EvaluationResult<TargetPatternValue> targetPatterns(Iterable<SkyKey> patternSkyKeys,
+ boolean keepGoing, EventHandler eventHandler) throws InterruptedException {
+ checkActive();
+ return buildDriver.evaluate(patternSkyKeys, keepGoing, DEFAULT_THREAD_COUNT,
+ eventHandler);
+ }
+
+ /**
+ * Returns the {@link ConfiguredTarget}s corresponding to the given keys.
+ *
+ * <p>For use for legacy support from {@code BuildView} only.
+ *
+ * <p>If a requested configured target is in error, the corresponding value is omitted from the
+ * returned list.
+ */
+ @ThreadSafety.ThreadSafe
+ public ImmutableList<ConfiguredTarget> getConfiguredTargets(Iterable<Dependency> keys) {
+ checkActive();
+ if (skyframeBuildView == null) {
+ // If build view has not yet been initialized, no configured targets can have been created.
+ // This is most likely to happen after a failed loading phase.
+ return ImmutableList.of();
+ }
+ final List<SkyKey> skyKeys = new ArrayList<>();
+ for (Dependency key : keys) {
+ skyKeys.add(ConfiguredTargetValue.key(key.getLabel(), key.getConfiguration()));
+ for (Class<? extends ConfiguredAspectFactory> aspect : key.getAspects()) {
+ skyKeys.add(AspectValue.key(key.getLabel(), key.getConfiguration(), aspect));
+ }
+ }
+
+ EvaluationResult<SkyValue> result;
+ try {
+ result = callUninterruptibly(new Callable<EvaluationResult<SkyValue>>() {
+ @Override
+ public EvaluationResult<SkyValue> call() throws Exception {
+ synchronized (valueLookupLock) {
+ try {
+ skyframeBuildView.enableAnalysis(true);
+ return buildDriver.evaluate(skyKeys, false, DEFAULT_THREAD_COUNT,
+ errorEventListener);
+ } finally {
+ skyframeBuildView.enableAnalysis(false);
+ }
+ }
+ }
+ });
+ } catch (Exception e) {
+ throw new IllegalStateException(e); // Should never happen.
+ }
+
+ ImmutableList.Builder<ConfiguredTarget> cts = ImmutableList.builder();
+
+ DependentNodeLoop:
+ for (Dependency key : keys) {
+ SkyKey configuredTargetKey = ConfiguredTargetValue.key(
+ key.getLabel(), key.getConfiguration());
+ if (result.get(configuredTargetKey) == null) {
+ continue;
+ }
+
+ ConfiguredTarget configuredTarget =
+ ((ConfiguredTargetValue) result.get(configuredTargetKey)).getConfiguredTarget();
+ List<Aspect> aspects = new ArrayList<>();
+
+ for (Class<? extends ConfiguredAspectFactory> aspect : key.getAspects()) {
+ SkyKey aspectKey = AspectValue.key(key.getLabel(), key.getConfiguration(), aspect);
+ if (result.get(aspectKey) == null) {
+ continue DependentNodeLoop;
+ }
+
+ aspects.add(((AspectValue) result.get(aspectKey)).get());
+ }
+
+ cts.add(RuleConfiguredTarget.mergeAspects(configuredTarget, aspects));
+ }
+
+ return cts.build();
+ }
+
+ /**
+ * Returns a particular configured target.
+ *
+ * <p>Used only for testing.
+ */
+ @VisibleForTesting
+ @Nullable
+ public ConfiguredTarget getConfiguredTargetForTesting(
+ Label label, BuildConfiguration configuration) {
+ if (memoizingEvaluator.getExistingValueForTesting(
+ PrecomputedValue.WORKSPACE_STATUS_KEY.getKeyForTesting()) == null) {
+ injectWorkspaceStatusData();
+ }
+ return Iterables.getFirst(getConfiguredTargets(ImmutableList.of(
+ new Dependency(label, configuration))), null);
+ }
+
+ /**
+ * Invalidates Skyframe values corresponding to the given set of modified files under the given
+ * path entry.
+ *
+ * <p>May throw an {@link InterruptedException}, which means that no values have been invalidated.
+ */
+ @VisibleForTesting
+ public abstract void invalidateFilesUnderPathForTesting(ModifiedFileSet modifiedFileSet,
+ Path pathEntry) throws InterruptedException;
+
+ /**
+ * Invalidates SkyFrame values that may have failed for transient reasons.
+ */
+ public abstract void invalidateTransientErrors();
+
+ @VisibleForTesting
+ public TimestampGranularityMonitor getTimestampGranularityMonitorForTesting() {
+ return tsgm;
+ }
+
+ /**
+ * Configures a given set of configured targets.
+ */
+ public EvaluationResult<ConfiguredTargetValue> configureTargets(
+ List<ConfiguredTargetKey> values, boolean keepGoing) throws InterruptedException {
+ checkActive();
+
+ // Make sure to not run too many analysis threads. This can cause memory thrashing.
+ return buildDriver.evaluate(ConfiguredTargetValue.keys(values), keepGoing,
+ ResourceUsage.getAvailableProcessors(), errorEventListener);
+ }
+
+ /**
+ * Post-process the targets. Values in the EvaluationResult are known to be transitively
+ * error-free from action conflicts.
+ */
+ public EvaluationResult<PostConfiguredTargetValue> postConfigureTargets(
+ List<ConfiguredTargetKey> values, boolean keepGoing,
+ ImmutableMap<Action, SkyframeActionExecutor.ConflictException> badActions)
+ throws InterruptedException {
+ checkActive();
+ PrecomputedValue.BAD_ACTIONS.set(injectable(), badActions);
+ // Make sure to not run too many analysis threads. This can cause memory thrashing.
+ EvaluationResult<PostConfiguredTargetValue> result =
+ buildDriver.evaluate(PostConfiguredTargetValue.keys(values), keepGoing,
+ ResourceUsage.getAvailableProcessors(), errorEventListener);
+
+ // Remove all post-configured target values immediately for memory efficiency. We are OK with
+ // this mini-phase being non-incremental as the failure mode of action conflict is rare.
+ memoizingEvaluator.delete(SkyFunctionName.functionIs(SkyFunctions.POST_CONFIGURED_TARGET));
+
+ return result;
+ }
+
+ /**
+ * Returns a Skyframe-based {@link SkyframeTransitivePackageLoader} implementation.
+ */
+ @VisibleForTesting
+ public TransitivePackageLoader pkgLoader() {
+ checkActive();
+ return new SkyframeLabelVisitor(new SkyframeTransitivePackageLoader(), cyclesReporter);
+ }
+
+ class SkyframeTransitivePackageLoader {
+ /**
+ * Loads the specified {@link TransitiveTargetValue}s.
+ */
+ EvaluationResult<TransitiveTargetValue> loadTransitiveTargets(
+ Iterable<Target> targetsToVisit, Iterable<Label> labelsToVisit, boolean keepGoing)
+ throws InterruptedException {
+ List<SkyKey> valueNames = new ArrayList<>();
+ for (Target target : targetsToVisit) {
+ valueNames.add(TransitiveTargetValue.key(target.getLabel()));
+ }
+ for (Label label : labelsToVisit) {
+ valueNames.add(TransitiveTargetValue.key(label));
+ }
+
+ return buildDriver.evaluate(valueNames, keepGoing, DEFAULT_THREAD_COUNT,
+ errorEventListener);
+ }
+
+ public Set<Package> retrievePackages(Set<PackageIdentifier> packageIds) {
+ final List<SkyKey> valueNames = new ArrayList<>();
+ for (PackageIdentifier pkgId : packageIds) {
+ valueNames.add(PackageValue.key(pkgId));
+ }
+
+ try {
+ return callUninterruptibly(new Callable<Set<Package>>() {
+ @Override
+ public Set<Package> call() throws Exception {
+ EvaluationResult<PackageValue> result = buildDriver.evaluate(
+ valueNames, false, ResourceUsage.getAvailableProcessors(), errorEventListener);
+ Preconditions.checkState(!result.hasError(),
+ "unexpected errors: %s", result.errorMap());
+ Set<Package> packages = Sets.newHashSet();
+ for (PackageValue value : result.values()) {
+ packages.add(value.getPackage());
+ }
+ return packages;
+ }
+ });
+ } catch (Exception e) {
+ throw new IllegalStateException(e);
+ }
+
+ }
+ }
+
+ /**
+ * Returns the generating {@link Action} of the given {@link Artifact}.
+ *
+ * <p>For use for legacy support from {@code BuildView} only.
+ */
+ @ThreadSafety.ThreadSafe
+ public Action getGeneratingAction(final Artifact artifact) {
+ if (artifact.isSourceArtifact()) {
+ return null;
+ }
+
+ try {
+ return callUninterruptibly(new Callable<Action>() {
+ @Override
+ public Action call() throws InterruptedException {
+ ArtifactOwner artifactOwner = artifact.getArtifactOwner();
+ Preconditions.checkState(artifactOwner instanceof ActionLookupValue.ActionLookupKey,
+ "%s %s", artifact, artifactOwner);
+ SkyKey actionLookupKey =
+ ActionLookupValue.key((ActionLookupValue.ActionLookupKey) artifactOwner);
+
+ synchronized (valueLookupLock) {
+ // Note that this will crash (attempting to run a configured target value builder after
+ // analysis) after a failed --nokeep_going analysis in which the configured target that
+ // failed was a (transitive) dependency of the configured target that should generate
+ // this action. We don't expect callers to query generating actions in such cases.
+ EvaluationResult<ActionLookupValue> result = buildDriver.evaluate(
+ ImmutableList.of(actionLookupKey), false, ResourceUsage.getAvailableProcessors(),
+ errorEventListener);
+ return result.hasError()
+ ? null
+ : result.get(actionLookupKey).getGeneratingAction(artifact);
+ }
+ }
+ });
+ } catch (Exception e) {
+ throw new IllegalStateException("Error getting generating action: " + artifact.prettyPrint(),
+ e);
+ }
+ }
+
+ public PackageManager getPackageManager() {
+ return packageManager;
+ }
+
+ class SkyframePackageLoader {
+ /**
+ * Looks up a particular package (mostly used after the loading phase, so packages should
+ * already be present, but occasionally used pre-loading phase). Use should be discouraged,
+ * since this cannot be used inside a Skyframe evaluation, and concurrent calls are
+ * synchronized.
+ *
+ * <p>Note that this method needs to be synchronized since InMemoryMemoizingEvaluator.evaluate()
+ * method does not support concurrent calls.
+ */
+ Package getPackage(EventHandler eventHandler, PackageIdentifier pkgName)
+ throws InterruptedException, NoSuchPackageException {
+ synchronized (valueLookupLock) {
+ SkyKey key = PackageValue.key(pkgName);
+ // Any call to this method post-loading phase should either be error-free or be in a
+ // keep_going build, since otherwise the build would have failed during loading. Thus
+ // we set keepGoing=true unconditionally.
+ EvaluationResult<PackageValue> result =
+ buildDriver.evaluate(ImmutableList.of(key), /*keepGoing=*/true,
+ DEFAULT_THREAD_COUNT, eventHandler);
+ if (result.hasError()) {
+ ErrorInfo error = result.getError();
+ if (!Iterables.isEmpty(error.getCycleInfo())) {
+ reportCycles(result.getError().getCycleInfo(), key);
+ // This can only happen if a package is freshly loaded outside of the target parsing
+ // or loading phase
+ throw new BuildFileContainsErrorsException(pkgName.toString(),
+ "Cycle encountered while loading package " + pkgName);
+ }
+ Throwable e = error.getException();
+ // PackageFunction should be catching, swallowing, and rethrowing all transitive
+ // errors as NoSuchPackageExceptions.
+ Throwables.propagateIfInstanceOf(e, NoSuchPackageException.class);
+ throw new IllegalStateException("Unexpected Exception type from PackageValue for '"
+ + pkgName + "'' with root causes: " + Iterables.toString(error.getRootCauses()), e);
+ }
+ return result.get(key).getPackage();
+ }
+ }
+
+ Package getLoadedPackage(final PackageIdentifier pkgName) throws NoSuchPackageException {
+ // Note that in Skyframe there is no way to tell if the package has been loaded before or not,
+ // so this will never throw for packages that are not loaded. However, no code currently
+ // relies on having the exception thrown.
+ try {
+ return callUninterruptibly(new Callable<Package>() {
+ @Override
+ public Package call() throws Exception {
+ return getPackage(errorEventListener, pkgName);
+ }
+ });
+ } catch (NoSuchPackageException e) {
+ if (e.getPackage() != null) {
+ return e.getPackage();
+ }
+ throw e;
+ } catch (Exception e) {
+ throw new IllegalStateException(e); // Should never happen.
+ }
+ }
+
+ /**
+ * Returns whether the given package should be consider deleted and thus should be ignored.
+ */
+ public boolean isPackageDeleted(String packageName) {
+ return deletedPackages.get().contains(packageName);
+ }
+
+ /** Same as {@link PackageManager#partiallyClear}. */
+ void partiallyClear() {
+ packageFunctionCache.clear();
+ }
+ }
+
+ /**
+ * Calls the given callable uninterruptibly.
+ *
+ * <p>If the callable throws {@link InterruptedException}, calls it again, until the callable
+ * returns a result. Sets the {@code currentThread().interrupted()} bit if the callable threw
+ * {@link InterruptedException} at least once.
+ *
+ * <p>This is almost identical to {@code Uninterruptibles#getUninterruptibly}.
+ */
+ protected static final <T> T callUninterruptibly(Callable<T> callable) throws Exception {
+ boolean interrupted = false;
+ try {
+ while (true) {
+ try {
+ return callable.call();
+ } catch (InterruptedException e) {
+ interrupted = true;
+ }
+ }
+ } finally {
+ if (interrupted) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+
+ @VisibleForTesting
+ public MemoizingEvaluator getEvaluatorForTesting() {
+ return memoizingEvaluator;
+ }
+
+ /**
+ * Stores the set of loaded packages and, if needed, evicts ConfiguredTarget values.
+ *
+ * <p>The set represents all packages from the transitive closure of the top-level targets from
+ * the latest build.
+ */
+ @ThreadCompatible
+ public abstract void updateLoadedPackageSet(Set<PackageIdentifier> loadedPackages);
+
+ public void sync(PackageCacheOptions packageCacheOptions, Path workingDirectory,
+ String defaultsPackageContents, UUID commandId) throws InterruptedException,
+ AbruptExitException{
+
+ preparePackageLoading(
+ createPackageLocator(packageCacheOptions, directories.getWorkspace(), workingDirectory),
+ packageCacheOptions.defaultVisibility, packageCacheOptions.showLoadingProgress,
+ defaultsPackageContents, commandId);
+ setDeletedPackages(ImmutableSet.copyOf(packageCacheOptions.deletedPackages));
+
+ incrementalBuildMonitor = new SkyframeIncrementalBuildMonitor();
+ invalidateTransientErrors();
+ }
+
+ protected PathPackageLocator createPackageLocator(PackageCacheOptions packageCacheOptions,
+ Path workspace, Path workingDirectory) throws AbruptExitException{
+ return PathPackageLocator.create(
+ packageCacheOptions.packagePath, getReporter(), workspace, workingDirectory);
+ }
+
+ private CyclesReporter createCyclesReporter() {
+ return new CyclesReporter(
+ new TransitiveTargetCycleReporter(packageManager),
+ new ActionArtifactCycleReporter(packageManager),
+ new SkylarkModuleCycleReporter());
+ }
+
+ CyclesReporter getCyclesReporter() {
+ return cyclesReporter.get();
+ }
+
+ /** Convenience method with same semantics as {@link CyclesReporter#reportCycles}. */
+ public void reportCycles(Iterable<CycleInfo> cycles, SkyKey topLevelKey) {
+ getCyclesReporter().reportCycles(cycles, topLevelKey, errorEventListener);
+ }
+
+ public void setActionExecutionProgressReportingObjects(@Nullable ProgressSupplier supplier,
+ @Nullable ActionCompletedReceiver completionReceiver,
+ @Nullable ActionExecutionStatusReporter statusReporter) {
+ skyframeActionExecutor.setActionExecutionProgressReportingObjects(supplier, completionReceiver);
+ this.statusReporterRef.set(statusReporter);
+ }
+
+ /**
+ * This should be called at most once in the lifetime of the SkyframeExecutor (except for
+ * tests), and it should be called before the execution phase.
+ */
+ void setArtifactFactoryAndBinTools(ArtifactFactory artifactFactory, BinTools binTools) {
+ this.artifactFactory.set(artifactFactory);
+ this.binTools = binTools;
+ }
+
+ public void prepareExecution(boolean checkOutputFiles) throws AbruptExitException,
+ InterruptedException {
+ maybeInjectEmbeddedArtifacts();
+
+ if (checkOutputFiles) {
+ // Detect external modifications in the output tree.
+ FilesystemValueChecker fsnc = new FilesystemValueChecker(memoizingEvaluator, tsgm);
+ invalidateDirtyActions(fsnc.getDirtyActionValues(batchStatter));
+ modifiedFiles += fsnc.getNumberOfModifiedOutputFiles();
+ }
+ informAboutNumberOfModifiedFiles();
+ }
+
+ protected abstract void invalidateDirtyActions(Iterable<SkyKey> dirtyActionValues);
+
+ @VisibleForTesting void maybeInjectEmbeddedArtifacts() throws AbruptExitException {
+ // The blaze client already ensures that the contents of the embedded binaries never change,
+ // so we just need to make sure that the appropriate artifacts are present in the skyframe
+ // graph.
+
+ if (!needToInjectEmbeddedArtifacts) {
+ return;
+ }
+
+ Preconditions.checkNotNull(artifactFactory.get());
+ Preconditions.checkNotNull(binTools);
+ Map<SkyKey, SkyValue> values = Maps.newHashMap();
+ // Blaze separately handles the symlinks that target these binaries. See BinTools#setupTool.
+ for (Artifact artifact : binTools.getAllEmbeddedArtifacts(artifactFactory.get())) {
+ FileArtifactValue fileArtifactValue;
+ try {
+ fileArtifactValue = FileArtifactValue.create(artifact);
+ } catch (IOException e) {
+ // See ExtractData in blaze.cc.
+ String message = "Error: corrupt installation: file " + artifact.getPath() + " missing. "
+ + "Please remove '" + directories.getInstallBase() + "' and try again.";
+ throw new AbruptExitException(message, ExitCode.LOCAL_ENVIRONMENTAL_ERROR, e);
+ }
+ values.put(ArtifactValue.key(artifact, /*isMandatory=*/true), fileArtifactValue);
+ }
+ injectable().inject(values);
+ needToInjectEmbeddedArtifacts = false;
+ }
+
+ /**
+ * Mark dirty values for deletion if they've been dirty for longer than N versions.
+ *
+ * <p>Specifying a value N means, if the current version is V and a value was dirtied (and
+ * has remained so) in version U, and U + N <= V, then the value will be marked for deletion
+ * and purged in version V+1.
+ */
+ public abstract void deleteOldNodes(long versionWindowForDirtyGc);
+
+ /**
+ * A progress received to track analysis invalidation and update progress messages.
+ */
+ protected class SkyframeProgressReceiver implements EvaluationProgressReceiver {
+ /**
+ * This flag is needed in order to avoid invalidating legacy data when we clear the
+ * analysis cache because of --discard_analysis_cache flag. For that case we want to keep
+ * the legacy data but get rid of the Skyframe data.
+ */
+ protected boolean ignoreInvalidations = false;
+ /** This receiver is only needed for execution, so it is null otherwise. */
+ @Nullable EvaluationProgressReceiver executionProgressReceiver = null;
+
+ @Override
+ public void invalidated(SkyValue value, InvalidationState state) {
+ if (ignoreInvalidations) {
+ return;
+ }
+ if (skyframeBuildView != null) {
+ skyframeBuildView.getInvalidationReceiver().invalidated(value, state);
+ }
+ }
+
+ @Override
+ public void enqueueing(SkyKey skyKey) {
+ if (ignoreInvalidations) {
+ return;
+ }
+ if (skyframeBuildView != null) {
+ skyframeBuildView.getInvalidationReceiver().enqueueing(skyKey);
+ }
+ if (executionProgressReceiver != null) {
+ executionProgressReceiver.enqueueing(skyKey);
+ }
+ }
+
+ @Override
+ public void evaluated(SkyKey skyKey, SkyValue value, EvaluationState state) {
+ if (ignoreInvalidations) {
+ return;
+ }
+ if (skyframeBuildView != null) {
+ skyframeBuildView.getInvalidationReceiver().evaluated(skyKey, value, state);
+ }
+ if (executionProgressReceiver != null) {
+ executionProgressReceiver.evaluated(skyKey, value, state);
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutorFactory.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutorFactory.java
new file mode 100644
index 0000000..a1615cf
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutorFactory.java
@@ -0,0 +1,68 @@
+// 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.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.analysis.WorkspaceStatusAction.Factory;
+import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoFactory;
+import com.google.devtools.build.lib.events.Reporter;
+import com.google.devtools.build.lib.packages.PackageFactory;
+import com.google.devtools.build.lib.packages.Preprocessor;
+import com.google.devtools.build.lib.util.AbruptExitException;
+import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionName;
+
+import java.util.Set;
+
+/**
+* A factory that creates instances of SkyframeExecutor.
+*/
+public interface SkyframeExecutorFactory {
+
+ /**
+ * Creates an instance of SkyframeExecutor
+ *
+ * @param reporter the reporter to be used by the executor
+ * @param pkgFactory the package factory
+ * @param skyframeBuild use Skyframe for the build phase. Should be always true after we are in
+ * the skyframe full mode.
+ * @param tsgm timestamp granularity monitor
+ * @param directories Blaze directories
+ * @param workspaceStatusActionFactory a factory for creating WorkspaceStatusAction objects
+ * @param buildInfoFactories list of BuildInfoFactories
+ * @param diffAwarenessFactories
+ * @param allowedMissingInputs
+ * @param preprocessorFactorySupplier
+ * @param extraSkyFunctions
+ * @param extraPrecomputedValues
+ * @return an instance of the SkyframeExecutor
+ * @throws AbruptExitException if the executor cannot be created
+ */
+ SkyframeExecutor create(Reporter reporter, PackageFactory pkgFactory,
+ TimestampGranularityMonitor tsgm, BlazeDirectories directories,
+ Factory workspaceStatusActionFactory,
+ ImmutableList<BuildInfoFactory> buildInfoFactories,
+ Set<Path> immutableDirectories,
+ Iterable<? extends DiffAwareness.Factory> diffAwarenessFactories,
+ Predicate<PathFragment> allowedMissingInputs,
+ Preprocessor.Factory.Supplier preprocessorFactorySupplier,
+ ImmutableMap<SkyFunctionName, SkyFunction> extraSkyFunctions,
+ ImmutableList<PrecomputedValue.Injected> extraPrecomputedValues) throws AbruptExitException;
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeIncrementalBuildMonitor.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeIncrementalBuildMonitor.java
new file mode 100644
index 0000000..c0fea26
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeIncrementalBuildMonitor.java
@@ -0,0 +1,59 @@
+// 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.eventbus.EventBus;
+import com.google.devtools.build.lib.actions.ChangedFilesMessage;
+import com.google.devtools.build.lib.concurrent.ThreadSafety;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.skyframe.SkyKey;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * A package-private class intended to track a small number of modified files during the build. This
+ * class should stop recording changed files if there are too many of them, instead of holding onto
+ * a large collection of files.
+ */
+@ThreadSafety.ThreadCompatible
+class SkyframeIncrementalBuildMonitor {
+ private Set<PathFragment> files = new HashSet<>();
+ private static final int MAX_FILES = 100;
+
+ public void accrue(Iterable<SkyKey> invalidatedValues) {
+ for (SkyKey skyKey : invalidatedValues) {
+ if (skyKey.functionName() == SkyFunctions.FILE_STATE) {
+ RootedPath file = (RootedPath) skyKey.argument();
+ maybeAddFile(file.getRelativePath());
+ }
+ }
+ }
+
+ private void maybeAddFile(PathFragment path) {
+ if (files != null) {
+ files.add(path);
+ if (files.size() >= MAX_FILES) {
+ files = null;
+ }
+ }
+ }
+
+ public void alertListeners(EventBus eventBus) {
+ if (files != null) {
+ eventBus.post(new ChangedFilesMessage(files));
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeLabelVisitor.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeLabelVisitor.java
new file mode 100644
index 0000000..2844cc0
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeLabelVisitor.java
@@ -0,0 +1,262 @@
+// 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.base.Predicate;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Multimaps;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.packages.Package;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.pkgcache.TransitivePackageLoader;
+import com.google.devtools.build.lib.skyframe.SkyframeExecutor.SkyframeTransitivePackageLoader;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.skyframe.CycleInfo;
+import com.google.devtools.build.skyframe.CyclesReporter;
+import com.google.devtools.build.skyframe.ErrorInfo;
+import com.google.devtools.build.skyframe.EvaluationResult;
+import com.google.devtools.build.skyframe.SkyKey;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map.Entry;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.annotation.Nullable;
+
+/**
+ * Skyframe-based transitive package loader.
+ */
+final class SkyframeLabelVisitor implements TransitivePackageLoader {
+
+ private final SkyframeTransitivePackageLoader transitivePackageLoader;
+ private final AtomicReference<CyclesReporter> skyframeCyclesReporter;
+
+ private Set<PackageIdentifier> allVisitedPackages;
+ private Set<PackageIdentifier> errorFreeVisitedPackages;
+ private Set<Label> visitedTargets;
+ private Set<TransitiveTargetValue> previousBuildTargetValueSet = null;
+ private boolean lastBuildKeepGoing = false;
+ private final Multimap<Label, Label> rootCauses = HashMultimap.create();
+
+ SkyframeLabelVisitor(SkyframeTransitivePackageLoader transitivePackageLoader,
+ AtomicReference<CyclesReporter> skyframeCyclesReporter) {
+ this.transitivePackageLoader = transitivePackageLoader;
+ this.skyframeCyclesReporter = skyframeCyclesReporter;
+ }
+
+ @Override
+ public boolean sync(EventHandler eventHandler, Set<Target> targetsToVisit,
+ Set<Label> labelsToVisit, boolean keepGoing, int parallelThreads, int maxDepth)
+ throws InterruptedException {
+ rootCauses.clear();
+ lastBuildKeepGoing = false;
+ EvaluationResult<TransitiveTargetValue> result =
+ transitivePackageLoader.loadTransitiveTargets(targetsToVisit, labelsToVisit, keepGoing);
+ updateVisitedValues(result.values());
+ lastBuildKeepGoing = keepGoing;
+
+ if (!hasErrors(result)) {
+ return true;
+ }
+
+ Set<Entry<SkyKey, ErrorInfo>> errors = result.errorMap().entrySet();
+ if (!keepGoing) {
+ // We may have multiple errors, but in non keep_going builds, we're obligated to print only
+ // one of them.
+ Preconditions.checkState(!errors.isEmpty(), result);
+ Entry<SkyKey, ErrorInfo> error = errors.iterator().next();
+ ErrorInfo errorInfo = error.getValue();
+ SkyKey topLevel = error.getKey();
+ Label topLevelLabel = (Label) topLevel.argument();
+ if (!Iterables.isEmpty(errorInfo.getCycleInfo())) {
+ skyframeCyclesReporter.get().reportCycles(errorInfo.getCycleInfo(), topLevel, eventHandler);
+ errorAboutLoadingFailure(topLevelLabel, null, eventHandler);
+ } else if (isDirectErrorFromTopLevelLabel(topLevelLabel, labelsToVisit, errorInfo)) {
+ // An error caused by a non-top-level label has already been reported during error
+ // bubbling but an error caused by the top-level non-target label itself hasn't been
+ // reported yet. Note that errors from top-level targets have already been reported
+ // during target parsing.
+ errorAboutLoadingFailure(topLevelLabel, errorInfo.getException(), eventHandler);
+ }
+ return false;
+ }
+
+ for (Entry<SkyKey, ErrorInfo> errorEntry : errors) {
+ SkyKey key = errorEntry.getKey();
+ ErrorInfo errorInfo = errorEntry.getValue();
+ Preconditions.checkState(key.functionName().equals(SkyFunctions.TRANSITIVE_TARGET), errorEntry);
+ Label topLevelLabel = (Label) key.argument();
+ if (!Iterables.isEmpty(errorInfo.getCycleInfo())) {
+ skyframeCyclesReporter.get().reportCycles(errorInfo.getCycleInfo(), key, eventHandler);
+ for (Label rootCause : getRootCausesOfCycles(topLevelLabel, errorInfo.getCycleInfo())) {
+ rootCauses.put(topLevelLabel, rootCause);
+ }
+ }
+ if (isDirectErrorFromTopLevelLabel(topLevelLabel, labelsToVisit, errorInfo)) {
+ // Unlike top-level targets, which have already gone through target parsing,
+ // errors directly coming from top-level labels have not been reported yet.
+ //
+ // See the note in the --nokeep_going case above.
+ eventHandler.handle(Event.error(errorInfo.getException().getMessage()));
+ }
+ warnAboutLoadingFailure(topLevelLabel, eventHandler);
+ for (SkyKey badKey : errorInfo.getRootCauses()) {
+ Preconditions.checkState(badKey.argument() instanceof Label,
+ "%s %s %s", key, errorInfo, badKey);
+ rootCauses.put(topLevelLabel, (Label) badKey.argument());
+ }
+ }
+ for (Label topLevelLabel : result.<Label>keyNames()) {
+ SkyKey topLevelTransitiveTargetKey = TransitiveTargetValue.key(topLevelLabel);
+ TransitiveTargetValue topLevelTransitiveTargetValue = result.get(topLevelTransitiveTargetKey);
+ if (topLevelTransitiveTargetValue.getTransitiveRootCauses() != null) {
+ for (Label rootCause : topLevelTransitiveTargetValue.getTransitiveRootCauses()) {
+ rootCauses.put(topLevelLabel, rootCause);
+ }
+ warnAboutLoadingFailure(topLevelLabel, eventHandler);
+ }
+ }
+ return false;
+ }
+
+ private static boolean hasErrors(EvaluationResult<TransitiveTargetValue> result) {
+ if (result.hasError()) {
+ return true;
+ }
+ for (TransitiveTargetValue transitiveTargetValue : result.values()) {
+ if (transitiveTargetValue.getTransitiveRootCauses() != null) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean isDirectErrorFromTopLevelLabel(Label label, Set<Label> topLevelLabels,
+ ErrorInfo errorInfo) {
+ return errorInfo.getException() != null && topLevelLabels.contains(label)
+ && Iterables.contains(errorInfo.getRootCauses(), TransitiveTargetValue.key(label));
+ }
+
+ private static void errorAboutLoadingFailure(Label topLevelLabel, @Nullable Throwable throwable,
+ EventHandler eventHandler) {
+ eventHandler.handle(Event.error(
+ "Loading of target '" + topLevelLabel + "' failed; build aborted" +
+ (throwable == null ? "" : ": " + throwable.getMessage())));
+ }
+
+ private static void warnAboutLoadingFailure(Label label, EventHandler eventHandler) {
+ eventHandler.handle(Event.warn(
+ // TODO(bazel-team): We use 'analyzing' here so that we print the same message as legacy
+ // Blaze. Once we get rid of legacy we should be able to change to 'loading' or
+ // similar.
+ "errors encountered while analyzing target '" + label + "': it will not be built"));
+ }
+
+ private static Set<Label> getRootCausesOfCycles(Label labelToLoad, Iterable<CycleInfo> cycles) {
+ ImmutableSet.Builder<Label> builder = ImmutableSet.builder();
+ for (CycleInfo cycleInfo : cycles) {
+ // The root cause of a cycle depends on the type of a cycle.
+
+ SkyKey culprit = Iterables.getFirst(cycleInfo.getCycle(), null);
+ if (culprit == null) {
+ continue;
+ }
+ if (culprit.functionName().equals(SkyFunctions.TRANSITIVE_TARGET)) {
+ // For a cycle between build targets, the root cause is the first element of the cycle.
+ builder.add((Label) culprit.argument());
+ } else {
+ // For other types of cycles (e.g. file symlink cycles), the root cause is the furthest
+ // target dependency that itself depended on the cycle.
+ Label furthestTarget = labelToLoad;
+ for (SkyKey skyKey : cycleInfo.getPathToCycle()) {
+ if (skyKey.functionName().equals(SkyFunctions.TRANSITIVE_TARGET)) {
+ furthestTarget = (Label) skyKey.argument();
+ } else {
+ break;
+ }
+ }
+ builder.add(furthestTarget);
+ }
+ }
+ return builder.build();
+ }
+
+ // Unfortunately we have to do an effective O(TC) visitation after the eval() call above to
+ // determine all of the packages in the closure.
+ private void updateVisitedValues(Collection<TransitiveTargetValue> targetValues) {
+ Set<TransitiveTargetValue> currentBuildTargetValueSet = new HashSet<>(targetValues);
+ if (Objects.equals(previousBuildTargetValueSet, currentBuildTargetValueSet)) {
+ // The next stanza is slow (and scales with the edge count of the target graph), so avoid
+ // the computation if the previous build already did it.
+ return;
+ }
+ NestedSetBuilder<PackageIdentifier> nestedAllPkgsBuilder = NestedSetBuilder.stableOrder();
+ NestedSetBuilder<PackageIdentifier> nestedErrorFreePkgsBuilder = NestedSetBuilder.stableOrder();
+ NestedSetBuilder<Label> nestedTargetBuilder = NestedSetBuilder.stableOrder();
+ for (TransitiveTargetValue value : targetValues) {
+ nestedAllPkgsBuilder.addTransitive(value.getTransitiveSuccessfulPackages());
+ nestedAllPkgsBuilder.addTransitive(value.getTransitiveUnsuccessfulPackages());
+ nestedErrorFreePkgsBuilder.addTransitive(value.getTransitiveSuccessfulPackages());
+ nestedTargetBuilder.addTransitive(value.getTransitiveTargets());
+ }
+ allVisitedPackages = nestedAllPkgsBuilder.build().toSet();
+ errorFreeVisitedPackages = nestedErrorFreePkgsBuilder.build().toSet();
+ visitedTargets = nestedTargetBuilder.build().toSet();
+ previousBuildTargetValueSet = currentBuildTargetValueSet;
+ }
+
+
+ @Override
+ public Set<PackageIdentifier> getVisitedPackageNames() {
+ return allVisitedPackages;
+ }
+
+ @Override
+ public Set<Package> getErrorFreeVisitedPackages() {
+ return transitivePackageLoader.retrievePackages(errorFreeVisitedPackages);
+ }
+
+ /**
+ * Doesn't necessarily include all top-level targets visited in error, because of issues with
+ * skyframe semantics (e.g. impossible to load a target if it transitively depends on a file
+ * symlink cycle). This is actually fine for the non-test usages of this method since such bad
+ * targets get filtered out.
+ */
+ @Override
+ public Set<Label> getVisitedTargets() {
+ return visitedTargets;
+ }
+
+ @Override
+ public Multimap<Label, Label> getRootCauses(final Collection<Label> targetsToLoad) {
+ Preconditions.checkState(lastBuildKeepGoing);
+ return Multimaps.filterKeys(rootCauses,
+ new Predicate<Label>() {
+ @Override
+ public boolean apply(Label label) {
+ return targetsToLoad.contains(label);
+ }
+ });
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframePackageLoaderWithValueEnvironment.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframePackageLoaderWithValueEnvironment.java
new file mode 100644
index 0000000..e467ae0
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframePackageLoaderWithValueEnvironment.java
@@ -0,0 +1,120 @@
+// 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.devtools.build.lib.analysis.BlazeDirectories;
+import com.google.devtools.build.lib.analysis.config.BuildConfiguration.Fragment;
+import com.google.devtools.build.lib.analysis.config.BuildOptions;
+import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
+import com.google.devtools.build.lib.analysis.config.PackageProviderForConfigurations;
+import com.google.devtools.build.lib.packages.NoSuchPackageException;
+import com.google.devtools.build.lib.packages.NoSuchTargetException;
+import com.google.devtools.build.lib.packages.Package;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.skyframe.SkyframeExecutor.SkyframePackageLoader;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.Label.SyntaxException;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.skyframe.MemoizingEvaluator;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyKey;
+
+import java.io.IOException;
+import java.util.Set;
+
+/**
+ * Repeats functionality of {@link SkyframePackageLoader} but uses
+ * {@link SkyFunction.Environment#getValue} instead of {@link MemoizingEvaluator#evaluate}
+ * for node evaluation
+ */
+class SkyframePackageLoaderWithValueEnvironment implements
+ PackageProviderForConfigurations {
+ private final SkyFunction.Environment env;
+ private final Set<Package> packages;
+
+ public SkyframePackageLoaderWithValueEnvironment(SkyFunction.Environment env,
+ Set<Package> packages) {
+ this.env = env;
+ this.packages = packages;
+ }
+
+ private Package getPackage(PackageIdentifier pkgIdentifier) throws NoSuchPackageException{
+ SkyKey key = PackageValue.key(pkgIdentifier);
+ PackageValue value = (PackageValue) env.getValueOrThrow(key, NoSuchPackageException.class);
+ if (value != null) {
+ packages.add(value.getPackage());
+ return value.getPackage();
+ }
+ return null;
+ }
+
+ @Override
+ public Package getLoadedPackage(final PackageIdentifier pkgIdentifier)
+ throws NoSuchPackageException {
+ try {
+ return getPackage(pkgIdentifier);
+ } catch (NoSuchPackageException e) {
+ if (e.getPackage() != null) {
+ return e.getPackage();
+ }
+ throw e;
+ }
+ }
+
+ @Override
+ public Target getLoadedTarget(Label label) throws NoSuchPackageException,
+ NoSuchTargetException {
+ Package pkg = getLoadedPackage(label.getPackageIdentifier());
+ return pkg == null ? null : pkg.getTarget(label.getName());
+ }
+
+ @Override
+ public boolean isTargetCurrent(Target target) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void addDependency(Package pkg, String fileName) throws SyntaxException, IOException {
+ RootedPath fileRootedPath = RootedPath.toRootedPath(pkg.getSourceRoot(),
+ pkg.getNameFragment().getRelative(fileName));
+ FileValue result = (FileValue) env.getValue(FileValue.key(fileRootedPath));
+ if (result != null && !result.exists()) {
+ throw new IOException();
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public <T extends Fragment> T getFragment(BuildOptions buildOptions, Class<T> fragmentType)
+ throws InvalidConfigurationException {
+ ConfigurationFragmentValue fragmentNode = (ConfigurationFragmentValue) env.getValueOrThrow(
+ ConfigurationFragmentValue.key(buildOptions, fragmentType),
+ InvalidConfigurationException.class);
+ if (fragmentNode == null) {
+ return null;
+ }
+ return (T) fragmentNode.getFragment();
+ }
+
+ @Override
+ public BlazeDirectories getDirectories() {
+ return PrecomputedValue.BLAZE_DIRECTORIES.get(env);
+ }
+
+ @Override
+ public boolean valuesMissing() {
+ return env.valuesMissing();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframePackageManager.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframePackageManager.java
new file mode 100644
index 0000000..cc32bf8
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframePackageManager.java
@@ -0,0 +1,177 @@
+// 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.devtools.build.lib.cmdline.LabelValidator;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.packages.NoSuchPackageException;
+import com.google.devtools.build.lib.packages.NoSuchTargetException;
+import com.google.devtools.build.lib.packages.Package;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.pkgcache.PackageManager;
+import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
+import com.google.devtools.build.lib.pkgcache.TargetPatternEvaluator;
+import com.google.devtools.build.lib.pkgcache.TransitivePackageLoader;
+import com.google.devtools.build.lib.skyframe.SkyframeExecutor.SkyframePackageLoader;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.UnixGlob;
+import com.google.devtools.build.skyframe.CyclesReporter;
+
+import java.io.PrintStream;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Skyframe-based package manager.
+ *
+ * <p>This is essentially a compatibility shim between the native Skyframe and non-Skyframe
+ * parts of Blaze and should not be long-lived.
+ */
+class SkyframePackageManager implements PackageManager {
+
+ private final SkyframePackageLoader packageLoader;
+ private final SkyframeExecutor.SkyframeTransitivePackageLoader transitiveLoader;
+ private final TargetPatternEvaluator patternEvaluator;
+ private final AtomicReference<UnixGlob.FilesystemCalls> syscalls;
+ private final AtomicReference<CyclesReporter> skyframeCyclesReporter;
+ private final AtomicReference<PathPackageLocator> pkgLocator;
+ private final AtomicInteger numPackagesLoaded;
+ private final SkyframeExecutor skyframeExecutor;
+
+ public SkyframePackageManager(SkyframePackageLoader packageLoader,
+ SkyframeExecutor.SkyframeTransitivePackageLoader transitiveLoader,
+ TargetPatternEvaluator patternEvaluator,
+ AtomicReference<UnixGlob.FilesystemCalls> syscalls,
+ AtomicReference<CyclesReporter> skyframeCyclesReporter,
+ AtomicReference<PathPackageLocator> pkgLocator,
+ AtomicInteger numPackagesLoaded,
+ SkyframeExecutor skyframeExecutor) {
+ this.packageLoader = packageLoader;
+ this.transitiveLoader = transitiveLoader;
+ this.patternEvaluator = patternEvaluator;
+ this.skyframeCyclesReporter = skyframeCyclesReporter;
+ this.pkgLocator = pkgLocator;
+ this.syscalls = syscalls;
+ this.numPackagesLoaded = numPackagesLoaded;
+ this.skyframeExecutor = skyframeExecutor;
+ }
+
+ @Override
+ public Package getLoadedPackage(PackageIdentifier pkgIdentifier) throws NoSuchPackageException {
+ return packageLoader.getLoadedPackage(pkgIdentifier);
+ }
+
+ @ThreadSafe
+ @Override
+ public Package getPackage(EventHandler eventHandler, PackageIdentifier packageIdentifier)
+ throws NoSuchPackageException, InterruptedException {
+ try {
+ return packageLoader.getPackage(eventHandler, packageIdentifier);
+ } catch (NoSuchPackageException e) {
+ if (e.getPackage() != null) {
+ return e.getPackage();
+ }
+ throw e;
+ }
+ }
+
+ @Override
+ public Target getLoadedTarget(Label label) throws NoSuchPackageException, NoSuchTargetException {
+ return getLoadedPackage(label.getPackageIdentifier()).getTarget(label.getName());
+ }
+
+ @Override
+ public Target getTarget(EventHandler eventHandler, Label label)
+ throws NoSuchPackageException, NoSuchTargetException, InterruptedException {
+ return getPackage(eventHandler, label.getPackageIdentifier()).getTarget(label.getName());
+ }
+
+ @Override
+ public boolean isTargetCurrent(Target target) {
+ Package pkg = target.getPackage();
+ try {
+ return getLoadedPackage(target.getLabel().getPackageIdentifier()) == pkg;
+ } catch (NoSuchPackageException e) {
+ return false;
+ }
+ }
+
+ @Override
+ public void partiallyClear() {
+ packageLoader.partiallyClear();
+ }
+
+ @Override
+ public PackageManagerStatistics getStatistics() {
+ return new PackageManagerStatistics() {
+ @Override
+ public int getPackagesLoaded() {
+ return numPackagesLoaded.get();
+ }
+
+ @Override
+ public int getPackagesLookedUp() {
+ return -1;
+ }
+
+ @Override
+ public int getCacheSize() {
+ return -1;
+ }
+ };
+ }
+
+ @Override
+ public boolean isPackage(String packageName) {
+ return getBuildFileForPackage(packageName) != null;
+ }
+
+ @Override
+ public void dump(PrintStream printStream) {
+ skyframeExecutor.dumpPackages(printStream);
+ }
+
+ @ThreadSafe
+ @Override
+ public Path getBuildFileForPackage(String packageName) {
+ // Note that this method needs to be thread-safe, as it is currently used concurrently by
+ // legacy blaze code.
+ if (packageLoader.isPackageDeleted(packageName)
+ || LabelValidator.validatePackageName(packageName) != null) {
+ return null;
+ }
+ // TODO(bazel-team): Use a PackageLookupValue here [skyframe-loading]
+ // TODO(bazel-team): The implementation in PackageCache also checks for duplicate packages, see
+ // BuildFileCache#getBuildFile [skyframe-loading]
+ return pkgLocator.get().getPackageBuildFileNullable(packageName, syscalls);
+ }
+
+ @Override
+ public PathPackageLocator getPackagePath() {
+ return pkgLocator.get();
+ }
+
+ @Override
+ public TransitivePackageLoader newTransitiveLoader() {
+ return new SkyframeLabelVisitor(transitiveLoader, skyframeCyclesReporter);
+ }
+
+ @Override
+ public TargetPatternEvaluator getTargetPatternEvaluator() {
+ return patternEvaluator;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeTargetPatternEvaluator.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeTargetPatternEvaluator.java
new file mode 100644
index 0000000..9e619e3
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeTargetPatternEvaluator.java
@@ -0,0 +1,146 @@
+// 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.base.Predicates;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
+import com.google.devtools.build.lib.cmdline.ResolvedTargets;
+import com.google.devtools.build.lib.cmdline.TargetParsingException;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.pkgcache.FilteringPolicies;
+import com.google.devtools.build.lib.pkgcache.FilteringPolicy;
+import com.google.devtools.build.lib.pkgcache.ParseFailureListener;
+import com.google.devtools.build.lib.pkgcache.TargetPatternEvaluator;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.skyframe.ErrorInfo;
+import com.google.devtools.build.skyframe.EvaluationResult;
+import com.google.devtools.build.skyframe.SkyKey;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Skyframe-based target pattern parsing.
+ */
+final class SkyframeTargetPatternEvaluator implements TargetPatternEvaluator {
+ private final SkyframeExecutor skyframeExecutor;
+ private String offset = "";
+
+ SkyframeTargetPatternEvaluator(SkyframeExecutor skyframeExecutor) {
+ this.skyframeExecutor = skyframeExecutor;
+ }
+
+ @Override
+ public ResolvedTargets<Target> parseTargetPatternList(EventHandler eventHandler,
+ List<String> targetPatterns, FilteringPolicy policy, boolean keepGoing)
+ throws TargetParsingException, InterruptedException {
+ return parseTargetPatternList(offset, eventHandler, targetPatterns, policy, keepGoing);
+ }
+
+ @Override
+ public ResolvedTargets<Target> parseTargetPattern(EventHandler eventHandler,
+ String pattern, boolean keepGoing) throws TargetParsingException, InterruptedException {
+ return parseTargetPatternList(eventHandler, ImmutableList.of(pattern),
+ FilteringPolicies.NO_FILTER, keepGoing);
+ }
+
+ @Override
+ public void updateOffset(PathFragment relativeWorkingDirectory) {
+ offset = relativeWorkingDirectory.getPathString();
+ }
+
+ @Override
+ public String getOffset() {
+ return offset;
+ }
+
+ @Override
+ public Map<String, ResolvedTargets<Target>> preloadTargetPatterns(EventHandler eventHandler,
+ Collection<String> patterns, boolean keepGoing)
+ throws TargetParsingException, InterruptedException {
+ // TODO(bazel-team): This is used only in "blaze query". There are plans to dramatically change
+ // how query works on Skyframe, in which case this method is likely to go away.
+ // We cannot use an ImmutableMap here because there may be null values.
+ Map<String, ResolvedTargets<Target>> result = Maps.newHashMapWithExpectedSize(patterns.size());
+ for (String pattern : patterns) {
+ // TODO(bazel-team): This could be parallelized to improve performance. [skyframe-loading]
+ result.put(pattern, parseTargetPattern(eventHandler, pattern, keepGoing));
+ }
+ return result;
+ }
+
+ /**
+ * Loads a list of target patterns (eg, "foo/...").
+ */
+ ResolvedTargets<Target> parseTargetPatternList(String offset, EventHandler eventHandler,
+ List<String> targetPatterns, FilteringPolicy policy, boolean keepGoing)
+ throws InterruptedException, TargetParsingException {
+ Iterable<SkyKey> patternSkyKeys = TargetPatternValue.keys(targetPatterns, policy, offset);
+ EvaluationResult<TargetPatternValue> result =
+ skyframeExecutor.targetPatterns(patternSkyKeys, keepGoing, eventHandler);
+
+ String errorMessage = null;
+ ResolvedTargets.Builder<Target> builder = ResolvedTargets.builder();
+ for (SkyKey key : patternSkyKeys) {
+ TargetPatternValue resultValue = result.get(key);
+ if (resultValue != null) {
+ ResolvedTargets<Target> results = resultValue.getTargets();
+ if (((TargetPatternValue.TargetPattern) key.argument()).isNegative()) {
+ builder.filter(Predicates.not(Predicates.in(results.getTargets())));
+ } else {
+ builder.merge(results);
+ }
+ } else {
+ TargetPatternValue.TargetPattern pattern =
+ (TargetPatternValue.TargetPattern) key.argument();
+ String rawPattern = pattern.getPattern();
+ ErrorInfo error = result.errorMap().get(key);
+ if (error == null) {
+ Preconditions.checkState(!keepGoing);
+ continue;
+ }
+ if (error.getException() != null) {
+ errorMessage = error.getException().getMessage();
+ } else if (!Iterables.isEmpty(error.getCycleInfo())) {
+ errorMessage = "cycles detected during target parsing";
+ skyframeExecutor.getCyclesReporter().reportCycles(
+ error.getCycleInfo(), key, eventHandler);
+ } else {
+ throw new IllegalStateException(error.toString());
+ }
+ if (keepGoing) {
+ eventHandler.handle(Event.error("Skipping '" + rawPattern + "': " + errorMessage));
+ }
+ builder.setError();
+
+ if (eventHandler instanceof ParseFailureListener) {
+ ParseFailureListener parseListener = (ParseFailureListener) eventHandler;
+ parseListener.parsingError(rawPattern, errorMessage);
+ }
+ }
+ }
+
+ if (!keepGoing && result.hasError()) {
+ Preconditions.checkState(errorMessage != null, "unexpected errors: %s", result.errorMap());
+ throw new TargetParsingException(errorMessage);
+ }
+ return builder.build();
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkylarkFileDependency.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkylarkFileDependency.java
new file mode 100644
index 0000000..02d2e91
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkylarkFileDependency.java
@@ -0,0 +1,48 @@
+// 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.collect.ImmutableList;
+import com.google.devtools.build.lib.syntax.Label;
+
+/**
+ * A simple value class to store the direct Skylark file dependencies of a Skylark
+ * extension file. It also contains a Label identifying the extension file.
+ */
+class SkylarkFileDependency {
+
+ private final Label label;
+ private final ImmutableList<SkylarkFileDependency> dependencies;
+
+ SkylarkFileDependency(Label label, ImmutableList<SkylarkFileDependency> dependencies) {
+ this.label = label;
+ this.dependencies = dependencies;
+ }
+
+ /**
+ * Returns the list of direct Skylark file dependencies of the Skylark extension file
+ * corresponding to this object.
+ */
+ ImmutableList<SkylarkFileDependency> getDependencies() {
+ return dependencies;
+ }
+
+ /**
+ * Returns the Label of the Skylark extension file corresponding to this object.
+ */
+ Label getLabel() {
+ return label;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkylarkImportLookupFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkylarkImportLookupFunction.java
new file mode 100644
index 0000000..02d41e6
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkylarkImportLookupFunction.java
@@ -0,0 +1,238 @@
+// 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.collect.ImmutableList;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.StoredEventHandler;
+import com.google.devtools.build.lib.packages.BuildFileNotFoundException;
+import com.google.devtools.build.lib.packages.PackageFactory;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.packages.PackageIdentifier.RepositoryName;
+import com.google.devtools.build.lib.packages.RuleClassProvider;
+import com.google.devtools.build.lib.skyframe.ASTFileLookupValue.ASTLookupInputException;
+import com.google.devtools.build.lib.syntax.BuildFileAST;
+import com.google.devtools.build.lib.syntax.Function;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.Label.SyntaxException;
+import com.google.devtools.build.lib.syntax.SkylarkEnvironment;
+import com.google.devtools.build.lib.vfs.PathFragment;
+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 java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A Skyframe function to look up and import a single Skylark extension.
+ */
+public class SkylarkImportLookupFunction implements SkyFunction {
+
+ private final RuleClassProvider ruleClassProvider;
+ private final ImmutableList<Function> nativeRuleFunctions;
+
+ public SkylarkImportLookupFunction(
+ RuleClassProvider ruleClassProvider, PackageFactory packageFactory) {
+ this.ruleClassProvider = ruleClassProvider;
+ this.nativeRuleFunctions = packageFactory.collectNativeRuleFunctions();
+ }
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) throws SkyFunctionException,
+ InterruptedException {
+ PackageIdentifier arg = (PackageIdentifier) skyKey.argument();
+ PathFragment file = arg.getPackageFragment();
+ ASTFileLookupValue astLookupValue = null;
+ try {
+ SkyKey astLookupKey = ASTFileLookupValue.key(file);
+ astLookupValue = (ASTFileLookupValue) env.getValueOrThrow(astLookupKey,
+ ErrorReadingSkylarkExtensionException.class, InconsistentFilesystemException.class);
+ } catch (ErrorReadingSkylarkExtensionException e) {
+ throw new SkylarkImportLookupFunctionException(SkylarkImportFailedException.errorReadingFile(
+ file, e.getMessage()));
+ } catch (InconsistentFilesystemException e) {
+ throw new SkylarkImportLookupFunctionException(e, Transience.PERSISTENT);
+ } catch (ASTLookupInputException e) {
+ throw new SkylarkImportLookupFunctionException(e, Transience.PERSISTENT);
+ }
+ if (astLookupValue == null) {
+ return null;
+ }
+ if (astLookupValue == ASTFileLookupValue.NO_FILE) {
+ // Skylark import files have to exist.
+ throw new SkylarkImportLookupFunctionException(SkylarkImportFailedException.noFile(file));
+ }
+
+ Map<PathFragment, SkylarkEnvironment> importMap = new HashMap<>();
+ ImmutableList.Builder<SkylarkFileDependency> fileDependencies = ImmutableList.builder();
+ BuildFileAST ast = astLookupValue.getAST();
+ // TODO(bazel-team): Refactor this code and PackageFunction to reduce code duplications.
+ for (PathFragment importFile : ast.getImports()) {
+ try {
+ SkyKey importsLookupKey = SkylarkImportLookupValue.key(arg.getRepository(), importFile);
+ SkylarkImportLookupValue importsLookupValue;
+ importsLookupValue = (SkylarkImportLookupValue) env.getValueOrThrow(
+ importsLookupKey, ASTLookupInputException.class);
+ if (importsLookupValue != null) {
+ importMap.put(importFile, importsLookupValue.getImportedEnvironment());
+ fileDependencies.add(importsLookupValue.getDependency());
+ }
+ } catch (ASTLookupInputException e) {
+ throw new SkylarkImportLookupFunctionException(e, Transience.PERSISTENT);
+ }
+ }
+ Label label = pathFragmentToLabel(arg.getRepository(), file, env);
+ if (env.valuesMissing()) {
+ // This means some imports are unavailable.
+ return null;
+ }
+
+ if (ast.containsErrors()) {
+ throw new SkylarkImportLookupFunctionException(SkylarkImportFailedException.skylarkErrors(
+ file));
+ }
+
+ SkylarkEnvironment extensionEnv = createEnv(ast, importMap, env);
+ // Skylark UserDefinedFunctions are sharing function definition Environments, so it's extremely
+ // important not to modify them from this point. Ideally they should be only used to import
+ // symbols and serve as global Environments of UserDefinedFunctions.
+ return new SkylarkImportLookupValue(
+ extensionEnv, new SkylarkFileDependency(label, fileDependencies.build()));
+ }
+
+ /**
+ * Converts the PathFragment of the Skylark file to a Label using the BUILD file closest to the
+ * Skylark file in its directory hierarchy - finds the package to which the Skylark file belongs.
+ * Throws an exception if no such BUILD file exists.
+ */
+ private Label pathFragmentToLabel(RepositoryName repo, PathFragment file, Environment env)
+ throws SkylarkImportLookupFunctionException {
+ ContainingPackageLookupValue containingPackageLookupValue = null;
+ try {
+ PackageIdentifier newPkgId = new PackageIdentifier(repo, file.getParentDirectory());
+ containingPackageLookupValue = (ContainingPackageLookupValue) env.getValueOrThrow(
+ ContainingPackageLookupValue.key(newPkgId),
+ BuildFileNotFoundException.class, InconsistentFilesystemException.class);
+ } catch (BuildFileNotFoundException e) {
+ // Thrown when there are IO errors looking for BUILD files.
+ throw new SkylarkImportLookupFunctionException(e, Transience.PERSISTENT);
+ } catch (InconsistentFilesystemException e) {
+ throw new SkylarkImportLookupFunctionException(e, Transience.PERSISTENT);
+ }
+
+ if (containingPackageLookupValue == null) {
+ return null;
+ }
+
+ if (!containingPackageLookupValue.hasContainingPackage()) {
+ throw new SkylarkImportLookupFunctionException(
+ SkylarkImportFailedException.noBuildFile(file));
+ }
+
+ PathFragment pkgName =
+ containingPackageLookupValue.getContainingPackageName().getPackageFragment();
+ PathFragment fileInPkg = file.relativeTo(pkgName);
+
+ try {
+ // This code relies on PackageIdentifier.RepositoryName.toString()
+ return Label.parseRepositoryLabel(repo + "//" + pkgName.getPathString() + ":" + fileInPkg);
+ } catch (SyntaxException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ /**
+ * Creates the SkylarkEnvironment to be imported. After it's returned, the Environment
+ * must not be modified.
+ */
+ private SkylarkEnvironment createEnv(BuildFileAST ast,
+ Map<PathFragment, SkylarkEnvironment> importMap, Environment env)
+ throws InterruptedException {
+ StoredEventHandler eventHandler = new StoredEventHandler();
+ // TODO(bazel-team): this method overestimates the changes which can affect the
+ // Skylark RuleClass. For example changes to comments or unused functions can modify the hash.
+ // A more accurate - however much more complicated - way would be to calculate a hash based on
+ // the transitive closure of the accessible AST nodes.
+ SkylarkEnvironment extensionEnv = ruleClassProvider
+ .createSkylarkRuleClassEnvironment(eventHandler, ast.getContentHashCode());
+ // Adding native rules module for build extensions.
+ // TODO(bazel-team): this might not be the best place to do this.
+ extensionEnv.update("native", ruleClassProvider.getNativeModule());
+ for (Function function : nativeRuleFunctions) {
+ extensionEnv.registerFunction(
+ ruleClassProvider.getNativeModule().getClass(), function.getName(), function);
+ }
+ extensionEnv.setImportedExtensions(importMap);
+ ast.exec(extensionEnv, eventHandler);
+ // Don't fail just replay the events so the original package lookup can fail.
+ Event.replayEventsOn(env.getListener(), eventHandler.getEvents());
+ return extensionEnv;
+ }
+
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+
+ static final class SkylarkImportFailedException extends Exception {
+ private SkylarkImportFailedException(String errorMessage) {
+ super(errorMessage);
+ }
+
+ static SkylarkImportFailedException errorReadingFile(PathFragment file, String error) {
+ return new SkylarkImportFailedException(
+ String.format("Encountered error while reading extension file '%s': %s", file, error));
+ }
+
+ static SkylarkImportFailedException noFile(PathFragment file) {
+ return new SkylarkImportFailedException(
+ String.format("Extension file not found: '%s'", file));
+ }
+
+ static SkylarkImportFailedException noBuildFile(PathFragment file) {
+ return new SkylarkImportFailedException(
+ String.format("Every .bzl file must have a corresponding package, but '%s' "
+ + "does not have one. Please create a BUILD file in the same or any parent directory."
+ + " Note that this BUILD file does not need to do anything except exist.", file));
+ }
+
+ static SkylarkImportFailedException skylarkErrors(PathFragment file) {
+ return new SkylarkImportFailedException(String.format("Extension '%s' has errors", file));
+ }
+ }
+
+ private static final class SkylarkImportLookupFunctionException extends SkyFunctionException {
+ private SkylarkImportLookupFunctionException(SkylarkImportFailedException cause) {
+ super(cause, Transience.PERSISTENT);
+ }
+
+ private SkylarkImportLookupFunctionException(InconsistentFilesystemException e,
+ Transience transience) {
+ super(e, transience);
+ }
+
+ private SkylarkImportLookupFunctionException(ASTLookupInputException e,
+ Transience transience) {
+ super(e, transience);
+ }
+
+ private SkylarkImportLookupFunctionException(BuildFileNotFoundException e,
+ Transience transience) {
+ super(e, transience);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkylarkImportLookupValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkylarkImportLookupValue.java
new file mode 100644
index 0000000..3c87431
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkylarkImportLookupValue.java
@@ -0,0 +1,70 @@
+// 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.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.packages.PackageIdentifier.RepositoryName;
+import com.google.devtools.build.lib.skyframe.ASTFileLookupValue.ASTLookupInputException;
+import com.google.devtools.build.lib.syntax.SkylarkEnvironment;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+/**
+ * A value that represents a Skylark import lookup result. The lookup value corresponds to
+ * exactly one Skylark file, identified by the PathFragment SkyKey argument.
+ */
+public class SkylarkImportLookupValue implements SkyValue {
+
+ private final SkylarkEnvironment importedEnvironment;
+ /**
+ * The immediate Skylark file dependency descriptor class corresponding to this value.
+ * Using this reference it's possible to reach the transitive closure of Skylark files
+ * on which this Skylark file depends.
+ */
+ private final SkylarkFileDependency dependency;
+
+ public SkylarkImportLookupValue(
+ SkylarkEnvironment importedEnvironment, SkylarkFileDependency dependency) {
+ this.importedEnvironment = Preconditions.checkNotNull(importedEnvironment);
+ this.dependency = Preconditions.checkNotNull(dependency);
+ }
+
+ /**
+ * Returns the imported SkylarkEnvironment.
+ */
+ public SkylarkEnvironment getImportedEnvironment() {
+ return importedEnvironment;
+ }
+
+ /**
+ * Returns the immediate Skylark file dependency corresponding to this import lookup value.
+ */
+ public SkylarkFileDependency getDependency() {
+ return dependency;
+ }
+
+ static SkyKey key(PackageIdentifier pkgIdentifier) throws ASTLookupInputException {
+ return key(pkgIdentifier.getRepository(), pkgIdentifier.getPackageFragment());
+ }
+
+ static SkyKey key(RepositoryName repo, PathFragment fileToImport) throws ASTLookupInputException {
+ // Skylark import lookup keys need to be valid AST file lookup keys.
+ ASTFileLookupValue.checkInputArgument(fileToImport);
+ return new SkyKey(
+ SkyFunctions.SKYLARK_IMPORTS_LOOKUP,
+ new PackageIdentifier(repo, fileToImport));
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkylarkModuleCycleReporter.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkylarkModuleCycleReporter.java
new file mode 100644
index 0000000..a0f37a9
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkylarkModuleCycleReporter.java
@@ -0,0 +1,71 @@
+// 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.Function;
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+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 Skylark files.
+ */
+public class SkylarkModuleCycleReporter implements CyclesReporter.SingleCycleReporter {
+
+ private static final Predicate<SkyKey> IS_SKYLARK_MODULE_SKY_KEY =
+ SkyFunctions.isSkyFunction(SkyFunctions.SKYLARK_IMPORTS_LOOKUP);
+
+ private static final Predicate<SkyKey> IS_PACKAGE_SKY_KEY =
+ SkyFunctions.isSkyFunction(SkyFunctions.PACKAGE);
+
+ @Override
+ public boolean maybeReportCycle(SkyKey topLevelKey, CycleInfo cycleInfo, boolean alreadyReported,
+ EventHandler eventHandler) {
+ ImmutableList<SkyKey> pathToCycle = cycleInfo.getPathToCycle();
+ if (pathToCycle.size() == 0) {
+ return false;
+ }
+ SkyKey lastPathElement = cycleInfo.getPathToCycle().get(pathToCycle.size() - 1);
+ if (alreadyReported) {
+ return true;
+ } else if (Iterables.all(cycleInfo.getCycle(), IS_SKYLARK_MODULE_SKY_KEY)
+ // The last element of the path to the cycle has to be a PackageFunction.
+ && IS_PACKAGE_SKY_KEY.apply(lastPathElement)) {
+ StringBuilder cycleMessage = new StringBuilder()
+ .append(((PackageIdentifier) lastPathElement.argument()).toString() + "/BUILD: ")
+ .append("cycle in referenced extension files: ");
+
+ AbstractLabelCycleReporter.printCycle(cycleInfo.getCycle(), cycleMessage,
+ new Function<SkyKey, String>() {
+ @Override
+ public String apply(SkyKey input) {
+ return ((PackageIdentifier) input.argument()).toString();
+ }
+ });
+
+ // 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;
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/TargetCompletionFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/TargetCompletionFunction.java
new file mode 100644
index 0000000..2e4aea9
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/TargetCompletionFunction.java
@@ -0,0 +1,138 @@
+// 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.eventbus.EventBus;
+import com.google.devtools.build.lib.actions.ActionExecutionException;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.MissingInputFileException;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.LabelAndConfiguration;
+import com.google.devtools.build.lib.analysis.TargetCompleteEvent;
+import com.google.devtools.build.lib.analysis.TopLevelArtifactContext;
+import com.google.devtools.build.lib.analysis.TopLevelArtifactHelper;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionException;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+import com.google.devtools.build.skyframe.ValueOrException2;
+
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.annotation.Nullable;
+
+/**
+ * TargetCompletionFunction builds the artifactsToBuild collection of a {@link ConfiguredTarget}.
+ */
+public final class TargetCompletionFunction implements SkyFunction {
+
+ private final AtomicReference<EventBus> eventBusRef;
+
+ public TargetCompletionFunction(AtomicReference<EventBus> eventBusRef) {
+ this.eventBusRef = eventBusRef;
+ }
+
+ @Nullable
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) throws TargetCompletionFunctionException {
+ LabelAndConfiguration lac = (LabelAndConfiguration) skyKey.argument();
+ ConfiguredTargetValue ctValue = (ConfiguredTargetValue)
+ env.getValue(ConfiguredTargetValue.key(lac.getLabel(), lac.getConfiguration()));
+ TopLevelArtifactContext topLevelContext = PrecomputedValue.TOP_LEVEL_CONTEXT.get(env);
+ if (env.valuesMissing()) {
+ return null;
+ }
+
+ Map<SkyKey, ValueOrException2<MissingInputFileException, ActionExecutionException>> inputDeps =
+ env.getValuesOrThrow(ArtifactValue.mandatoryKeys(
+ TopLevelArtifactHelper.getAllArtifactsToBuild(
+ ctValue.getConfiguredTarget(), topLevelContext)),
+ MissingInputFileException.class, ActionExecutionException.class);
+
+ int missingCount = 0;
+ ActionExecutionException firstActionExecutionException = null;
+ MissingInputFileException missingInputException = null;
+ NestedSetBuilder<Label> rootCausesBuilder = NestedSetBuilder.stableOrder();
+ for (Map.Entry<SkyKey, ValueOrException2<MissingInputFileException,
+ ActionExecutionException>> depsEntry : inputDeps.entrySet()) {
+ Artifact input = ArtifactValue.artifact(depsEntry.getKey());
+ try {
+ depsEntry.getValue().get();
+ } catch (MissingInputFileException e) {
+ missingCount++;
+ if (input.getOwner() != null) {
+ rootCausesBuilder.add(input.getOwner());
+ env.getListener().handle(Event.error(
+ ctValue.getConfiguredTarget().getTarget().getLocation(),
+ String.format("%s: missing input file '%s'",
+ lac.getLabel(), input.getOwner())));
+ }
+ } catch (ActionExecutionException e) {
+ rootCausesBuilder.addTransitive(e.getRootCauses());
+ if (firstActionExecutionException == null) {
+ firstActionExecutionException = e;
+ }
+ }
+ }
+
+ if (missingCount > 0) {
+ missingInputException = new MissingInputFileException(
+ ctValue.getConfiguredTarget().getTarget().getLocation() + " " + missingCount
+ + " input file(s) do not exist", ctValue.getConfiguredTarget().getTarget().getLocation());
+ }
+
+ NestedSet<Label> rootCauses = rootCausesBuilder.build();
+ if (!rootCauses.isEmpty()) {
+ eventBusRef.get().post(
+ TargetCompleteEvent.createFailed(ctValue.getConfiguredTarget(), rootCauses));
+ if (firstActionExecutionException != null) {
+ throw new TargetCompletionFunctionException(firstActionExecutionException);
+ } else {
+ throw new TargetCompletionFunctionException(missingInputException);
+ }
+ }
+
+ return env.valuesMissing() ? null : new TargetCompletionValue(ctValue.getConfiguredTarget());
+ }
+
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return Label.print(((LabelAndConfiguration) skyKey.argument()).getLabel());
+ }
+
+ private static final class TargetCompletionFunctionException extends SkyFunctionException {
+
+ private final ActionExecutionException actionException;
+
+ public TargetCompletionFunctionException(ActionExecutionException e) {
+ super(e, Transience.PERSISTENT);
+ this.actionException = e;
+ }
+
+ public TargetCompletionFunctionException(MissingInputFileException e) {
+ super(e, Transience.TRANSIENT);
+ this.actionException = null;
+ }
+
+ @Override
+ public boolean isCatastrophic() {
+ return actionException != null && actionException.isCatastrophe();
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/TargetCompletionValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/TargetCompletionValue.java
new file mode 100644
index 0000000..5d7153d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/TargetCompletionValue.java
@@ -0,0 +1,51 @@
+// 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.Function;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.LabelAndConfiguration;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.util.Collection;
+
+/**
+ * The value of a TargetCompletion. Currently this just stores a ConfiguredTarget.
+ */
+public class TargetCompletionValue implements SkyValue {
+ private final ConfiguredTarget ct;
+
+ TargetCompletionValue(ConfiguredTarget ct) {
+ this.ct = ct;
+ }
+
+ public ConfiguredTarget getConfiguredTarget() {
+ return ct;
+ }
+
+ public static SkyKey key(LabelAndConfiguration labelAndConfiguration) {
+ return new SkyKey(SkyFunctions.TARGET_COMPLETION, labelAndConfiguration);
+ }
+
+ public static Iterable<SkyKey> keys(Collection<ConfiguredTarget> targets) {
+ return Iterables.transform(targets, new Function<ConfiguredTarget, SkyKey>() {
+ @Override
+ public SkyKey apply(ConfiguredTarget ct) {
+ return new SkyKey(SkyFunctions.TARGET_COMPLETION, new LabelAndConfiguration(ct));
+ }
+ });
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/TargetMarkerFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/TargetMarkerFunction.java
new file mode 100644
index 0000000..3f9f22f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/TargetMarkerFunction.java
@@ -0,0 +1,134 @@
+// 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.devtools.build.lib.packages.BuildFileNotFoundException;
+import com.google.devtools.build.lib.packages.NoSuchPackageException;
+import com.google.devtools.build.lib.packages.NoSuchTargetException;
+import com.google.devtools.build.lib.packages.Package;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionException;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+/**
+ * A SkyFunction for {@link TargetMarkerValue}s.
+ */
+public final class TargetMarkerFunction implements SkyFunction {
+
+ public TargetMarkerFunction() {
+ }
+
+ @Override
+ public SkyValue compute(SkyKey key, Environment env) throws TargetMarkerFunctionException {
+ Label label = (Label) key.argument();
+ PathFragment pkgForLabel = label.getPackageFragment();
+
+ if (label.getName().contains("/")) {
+ // This target is in a subdirectory, therefore it could potentially be invalidated by
+ // a new BUILD file appearing in the hierarchy.
+ PathFragment containingDirectory = label.toPathFragment().getParentDirectory();
+ ContainingPackageLookupValue containingPackageLookupValue = null;
+ try {
+ PackageIdentifier newPkgId = new PackageIdentifier(
+ label.getPackageIdentifier().getRepository(), containingDirectory);
+ containingPackageLookupValue = (ContainingPackageLookupValue) env.getValueOrThrow(
+ ContainingPackageLookupValue.key(newPkgId),
+ BuildFileNotFoundException.class, InconsistentFilesystemException.class);
+ } catch (BuildFileNotFoundException e) {
+ // Thrown when there are IO errors looking for BUILD files.
+ throw new TargetMarkerFunctionException(e);
+ } catch (InconsistentFilesystemException e) {
+ throw new TargetMarkerFunctionException(new NoSuchTargetException(label,
+ e.getMessage()));
+ }
+ if (containingPackageLookupValue == null) {
+ return null;
+ }
+ if (!containingPackageLookupValue.hasContainingPackage()) {
+ // This means the label's package doesn't exist. E.g. there is no package 'a' and we are
+ // trying to build the target for label 'a:b/foo'.
+ throw new TargetMarkerFunctionException(new BuildFileNotFoundException(
+ pkgForLabel.getPathString(), "BUILD file not found on package path for '"
+ + pkgForLabel.getPathString() + "'"));
+ }
+ if (!containingPackageLookupValue.getContainingPackageName().equals(
+ label.getPackageIdentifier())) {
+ throw new TargetMarkerFunctionException(new NoSuchTargetException(label,
+ String.format("Label '%s' crosses boundary of subpackage '%s'", label,
+ containingPackageLookupValue.getContainingPackageName())));
+ }
+ }
+
+ SkyKey pkgSkyKey = PackageValue.key(label.getPackageIdentifier());
+ NoSuchPackageException nspe = null;
+ Package pkg;
+ try {
+ PackageValue value = (PackageValue)
+ env.getValueOrThrow(pkgSkyKey, NoSuchPackageException.class);
+ if (value == null) {
+ return null;
+ }
+ pkg = value.getPackage();
+ } catch (NoSuchPackageException e) {
+ // For consistency with pre-Skyframe Blaze, we can return a valid Target from a Package
+ // containing errors.
+ pkg = e.getPackage();
+ if (pkg == null) {
+ // Re-throw this exception with our key because root causes should be targets, not packages.
+ throw new TargetMarkerFunctionException(e);
+ }
+ nspe = e;
+ }
+
+ Target target;
+ try {
+ target = pkg.getTarget(label.getName());
+ } catch (NoSuchTargetException e) {
+ throw new TargetMarkerFunctionException(e);
+ }
+
+ if (nspe != null) {
+ // There is a target, but its package is in error. We rethrow so that the root cause is the
+ // target, not the package. Note that targets are only in error when their package is
+ // "in error" (because a package is in error if there was an error evaluating the package, or
+ // if one of its targets was in error).
+ throw new TargetMarkerFunctionException(new NoSuchTargetException(target, nspe));
+ }
+ return TargetMarkerValue.TARGET_MARKER_INSTANCE;
+ }
+
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return Label.print((Label) skyKey.argument());
+ }
+
+ /**
+ * Used to declare all the exception types that can be wrapped in the exception thrown by
+ * {@link TargetMarkerFunction#compute}.
+ */
+ private static final class TargetMarkerFunctionException extends SkyFunctionException {
+ public TargetMarkerFunctionException(NoSuchTargetException e) {
+ super(e, Transience.PERSISTENT);
+ }
+
+ public TargetMarkerFunctionException(NoSuchPackageException e) {
+ super(e, Transience.PERSISTENT);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/TargetMarkerValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/TargetMarkerValue.java
new file mode 100644
index 0000000..eef7084
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/TargetMarkerValue.java
@@ -0,0 +1,38 @@
+// 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.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+/**
+ * Value represents visited target in the Skyframe graph after error checking.
+ */
+@Immutable
+@ThreadSafe
+public final class TargetMarkerValue implements SkyValue {
+
+ static final TargetMarkerValue TARGET_MARKER_INSTANCE = new TargetMarkerValue();
+
+ private TargetMarkerValue() {
+ }
+
+ @ThreadSafe
+ public static SkyKey key(Label label) {
+ return new SkyKey(SkyFunctions.TARGET_MARKER, label);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/TargetPatternFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/TargetPatternFunction.java
new file mode 100644
index 0000000..6e631ed
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/TargetPatternFunction.java
@@ -0,0 +1,278 @@
+// 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.devtools.build.lib.cmdline.LabelValidator;
+import com.google.devtools.build.lib.cmdline.ResolvedTargets;
+import com.google.devtools.build.lib.cmdline.TargetParsingException;
+import com.google.devtools.build.lib.cmdline.TargetPattern;
+import com.google.devtools.build.lib.cmdline.TargetPatternResolver;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.packages.NoSuchPackageException;
+import com.google.devtools.build.lib.packages.NoSuchThingException;
+import com.google.devtools.build.lib.packages.Package;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.pkgcache.FilteringPolicies;
+import com.google.devtools.build.lib.pkgcache.FilteringPolicy;
+import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
+import com.google.devtools.build.lib.pkgcache.TargetPatternResolverUtil;
+import com.google.devtools.build.lib.syntax.Label;
+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.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.annotation.Nullable;
+
+/**
+ * TargetPatternFunction translates a target pattern (eg, "foo/...") into a set of resolved
+ * Targets.
+ */
+public class TargetPatternFunction implements SkyFunction {
+
+ private final AtomicReference<PathPackageLocator> pkgPath;
+
+ public TargetPatternFunction(AtomicReference<PathPackageLocator> pkgPath) {
+ this.pkgPath = pkgPath;
+ }
+
+ @Override
+ public SkyValue compute(SkyKey key, Environment env) throws TargetPatternFunctionException,
+ InterruptedException {
+ TargetPatternValue.TargetPattern patternKey =
+ ((TargetPatternValue.TargetPattern) key.argument());
+
+ TargetPattern.Parser parser = new TargetPattern.Parser(patternKey.getOffset());
+ try {
+ Resolver resolver = new Resolver(env, patternKey.getPolicy(), pkgPath);
+ TargetPattern resolvedPattern = parser.parse(patternKey.getPattern());
+ return new TargetPatternValue(resolvedPattern.eval(resolver));
+ } catch (TargetParsingException e) {
+ throw new TargetPatternFunctionException(e);
+ } catch (TargetPatternResolver.MissingDepException e) {
+ return null;
+ }
+ }
+
+ @Nullable
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+
+ private static class Resolver implements TargetPatternResolver<Target> {
+ private final Environment env;
+ private final FilteringPolicy policy;
+ private final AtomicReference<PathPackageLocator> pkgPath;
+
+ public Resolver(Environment env, FilteringPolicy policy,
+ AtomicReference<PathPackageLocator> pkgPath) {
+ this.policy = policy;
+ this.env = env;
+ this.pkgPath = pkgPath;
+ }
+
+ @Override
+ public void warn(String msg) {
+ env.getListener().handle(Event.warn(msg));
+ }
+
+ /**
+ * Gets a Package via the Skyframe env. May return a Package that has errors.
+ */
+ private Package getPackage(PackageIdentifier pkgIdentifier)
+ throws MissingDepException, NoSuchThingException {
+ SkyKey pkgKey = PackageValue.key(pkgIdentifier);
+ Package pkg;
+ try {
+ PackageValue pkgValue =
+ (PackageValue) env.getValueOrThrow(pkgKey, NoSuchThingException.class);
+ if (pkgValue == null) {
+ throw new MissingDepException();
+ }
+ pkg = pkgValue.getPackage();
+ } catch (NoSuchPackageException e) {
+ pkg = e.getPackage();
+ if (pkg == null) {
+ throw e;
+ }
+ }
+ return pkg;
+ }
+
+ @Override
+ public Target getTargetOrNull(String targetName) throws InterruptedException,
+ MissingDepException {
+ try {
+ Label label = Label.parseAbsolute(targetName);
+ if (!isPackage(label.getPackageName())) {
+ return null;
+ }
+ Package pkg = getPackage(label.getPackageIdentifier());
+ return pkg.getTarget(label.getName());
+ } catch (Label.SyntaxException | NoSuchThingException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public ResolvedTargets<Target> getExplicitTarget(String targetName)
+ throws TargetParsingException, InterruptedException, MissingDepException {
+ Label label = TargetPatternResolverUtil.label(targetName);
+ try {
+ Package pkg = getPackage(label.getPackageIdentifier());
+ Target target = pkg.getTarget(label.getName());
+ return policy.shouldRetain(target, true)
+ ? ResolvedTargets.of(target)
+ : ResolvedTargets.<Target>empty();
+ } catch (NoSuchThingException e) {
+ throw new TargetParsingException(e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public ResolvedTargets<Target> getTargetsInPackage(String originalPattern, String packageName,
+ boolean rulesOnly)
+ throws TargetParsingException, InterruptedException, MissingDepException {
+ FilteringPolicy actualPolicy = rulesOnly
+ ? FilteringPolicies.and(FilteringPolicies.RULES_ONLY, policy)
+ : policy;
+ return getTargetsInPackage(originalPattern, packageName, actualPolicy);
+ }
+
+ private ResolvedTargets<Target> getTargetsInPackage(String originalPattern, String packageName,
+ FilteringPolicy policy)
+ throws TargetParsingException, MissingDepException {
+ // Normalise, e.g "foo//bar" -> "foo/bar"; "foo/" -> "foo":
+ PathFragment packageNameFragment = new PathFragment(packageName);
+ packageName = packageNameFragment.toString();
+
+ // It's possible for this check to pass, but for
+ // Label.validatePackageNameFull to report an error because the
+ // package name is illegal. That's a little weird, but we can live with
+ // that for now--see test case: testBadPackageNameButGoodEnoughForALabel.
+ // (BTW I tried duplicating that validation logic in Label but it was
+ // extremely tricky.)
+ if (LabelValidator.validatePackageName(packageName) != null) {
+ throw new TargetParsingException("'" + packageName + "' is not a valid package name");
+ }
+ if (!isPackage(packageName)) {
+ throw new TargetParsingException(
+ TargetPatternResolverUtil.getParsingErrorMessage(
+ "no such package '" + packageName + "': BUILD file not found on package path",
+ originalPattern));
+ }
+
+ try {
+ Package pkg = getPackage(
+ PackageIdentifier.createInDefaultRepo(packageNameFragment.toString()));
+ return TargetPatternResolverUtil.resolvePackageTargets(pkg, policy);
+ } catch (NoSuchThingException e) {
+ String message = TargetPatternResolverUtil.getParsingErrorMessage(
+ "package contains errors", originalPattern);
+ throw new TargetParsingException(message, e);
+ }
+ }
+
+ @Override
+ public boolean isPackage(String packageName) throws MissingDepException {
+ SkyKey packageLookupKey;
+ packageLookupKey = PackageLookupValue.key(new PathFragment(packageName));
+ PackageLookupValue packageLookupValue = (PackageLookupValue) env.getValue(packageLookupKey);
+ if (packageLookupValue == null) {
+ throw new MissingDepException();
+ }
+ return packageLookupValue.packageExists();
+ }
+
+ @Override
+ public String getTargetKind(Target target) {
+ return target.getTargetKind();
+ }
+
+ @Override
+ public ResolvedTargets<Target> findTargetsBeneathDirectory(
+ String originalPattern, String pathPrefix, boolean rulesOnly)
+ throws TargetParsingException, MissingDepException {
+ FilteringPolicy actualPolicy = rulesOnly
+ ? FilteringPolicies.and(FilteringPolicies.RULES_ONLY, policy)
+ : policy;
+
+ PathFragment directory = new PathFragment(pathPrefix);
+ if (directory.containsUplevelReferences()) {
+ throw new TargetParsingException("up-level references are not permitted: '"
+ + directory.getPathString() + "'");
+ }
+ if (!pathPrefix.isEmpty() && (LabelValidator.validatePackageName(pathPrefix) != null)) {
+ throw new TargetParsingException("'" + pathPrefix + "' is not a valid package name");
+ }
+
+ ResolvedTargets.Builder<Target> builder = ResolvedTargets.builder();
+
+ List<RecursivePkgValue> lookupValues = new ArrayList<>();
+ for (Path root : pkgPath.get().getPathEntries()) {
+ SkyKey key = RecursivePkgValue.key(RootedPath.toRootedPath(root, directory));
+ RecursivePkgValue lookup = (RecursivePkgValue) env.getValue(key);
+ if (lookup != null) {
+ lookupValues.add(lookup);
+ }
+ }
+ if (env.valuesMissing()) {
+ throw new MissingDepException();
+ }
+
+ for (RecursivePkgValue value : lookupValues) {
+ for (String pkg : value.getPackages()) {
+ builder.merge(getTargetsInPackage(originalPattern, pkg, FilteringPolicies.NO_FILTER));
+ }
+ }
+
+ if (builder.isEmpty()) {
+ throw new TargetParsingException("no targets found beneath '" + directory + "'");
+ }
+
+ // Apply the transform after the check so we only return the
+ // error if the tree really contains no targets.
+ ResolvedTargets<Target> intermediateResult = builder.build();
+ ResolvedTargets.Builder<Target> filteredBuilder = ResolvedTargets.builder();
+ if (intermediateResult.hasError()) {
+ filteredBuilder.setError();
+ }
+ for (Target target : intermediateResult.getTargets()) {
+ if (actualPolicy.shouldRetain(target, false)) {
+ filteredBuilder.add(target);
+ }
+ }
+ return filteredBuilder.build();
+ }
+ }
+
+ /**
+ * Used to declare all the exception types that can be wrapped in the exception thrown by
+ * {@link TargetPatternFunction#compute}.
+ */
+ private static final class TargetPatternFunctionException extends SkyFunctionException {
+ public TargetPatternFunctionException(TargetParsingException e) {
+ super(e, Transience.PERSISTENT);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/TargetPatternValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/TargetPatternValue.java
new file mode 100644
index 0000000..c97194f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/TargetPatternValue.java
@@ -0,0 +1,212 @@
+// 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.collect.Lists;
+import com.google.devtools.build.lib.cmdline.ResolvedTargets;
+import com.google.devtools.build.lib.cmdline.ResolvedTargets.Builder;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.packages.NoSuchTargetException;
+import com.google.devtools.build.lib.packages.Package;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.pkgcache.FilteringPolicies;
+import com.google.devtools.build.lib.pkgcache.FilteringPolicy;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * A value referring to a computed set of resolved targets. This is used for the results of target
+ * pattern parsing.
+ */
+@Immutable
+@ThreadSafe
+public final class TargetPatternValue implements SkyValue {
+
+ private ResolvedTargets<Target> targets;
+
+ TargetPatternValue(ResolvedTargets<Target> targets) {
+ this.targets = Preconditions.checkNotNull(targets);
+ }
+
+ private void writeObject(ObjectOutputStream out) throws IOException {
+ Set<Package> packages = new LinkedHashSet<>();
+ List<String> ts = new ArrayList<>();
+ List<String> filteredTs = new ArrayList<>();
+ for (Target target : targets.getTargets()) {
+ packages.add(target.getPackage());
+ ts.add(target.getLabel().toString());
+ }
+ for (Target target : targets.getFilteredTargets()) {
+ packages.add(target.getPackage());
+ filteredTs.add(target.getLabel().toString());
+ }
+
+ out.writeObject(packages);
+ out.writeObject(ts);
+ out.writeObject(filteredTs);
+ }
+
+ @SuppressWarnings("unchecked")
+ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
+ Set<Package> packages = (Set<Package>) in.readObject();
+ List<String> ts = (List<String>) in.readObject();
+ List<String> filteredTs = (List<String>) in.readObject();
+
+ Map<String, Package> packageMap = new HashMap<>();
+ for (Package p : packages) {
+ packageMap.put(p.getName(), p);
+ }
+
+ Builder<Target> builder = ResolvedTargets.<Target>builder();
+ for (String labelString : ts) {
+ builder.add(lookupTarget(packageMap, labelString));
+ }
+
+ for (String labelString : filteredTs) {
+ builder.remove(lookupTarget(packageMap, labelString));
+ }
+ this.targets = builder.build();
+ }
+
+ private static Target lookupTarget(Map<String, Package> packageMap, String labelString) {
+ Label label = Label.parseAbsoluteUnchecked(labelString);
+ Package p = packageMap.get(label.getPackageName());
+ try {
+ return p.getTarget(label.getName());
+ } catch (NoSuchTargetException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ private void readObjectNoData() {
+ throw new IllegalStateException();
+ }
+
+ /**
+ * Create a target pattern value key.
+ *
+ * @param pattern The pattern, eg "-foo/biz...". If the first character is "-", the pattern
+ * is treated as a negative pattern.
+ * @param policy The filtering policy, eg "only return test targets"
+ * @param offset The offset to apply to relative target patterns.
+ */
+ @ThreadSafe
+ public static SkyKey key(String pattern,
+ FilteringPolicy policy,
+ String offset) {
+ return new SkyKey(SkyFunctions.TARGET_PATTERN,
+ pattern.startsWith("-")
+ // Don't apply filters to negative patterns.
+ ? new TargetPattern(pattern.substring(1), FilteringPolicies.NO_FILTER, true, offset)
+ : new TargetPattern(pattern, policy, false, offset));
+ }
+
+ /**
+ * Like above, but accepts a collection of target patterns for the same filtering policy.
+ *
+ * @param patterns The collection of patterns, eg "-foo/biz...". If the first character is "-",
+ * the pattern is treated as a negative pattern.
+ * @param policy The filtering policy, eg "only return test targets"
+ * @param offset The offset to apply to relative target patterns.
+ */
+ @ThreadSafe
+ public static Iterable<SkyKey> keys(Collection<String> patterns,
+ FilteringPolicy policy,
+ String offset) {
+ List<SkyKey> keys = Lists.newArrayListWithCapacity(patterns.size());
+ for (String pattern : patterns) {
+ keys.add(key(pattern, policy, offset));
+ }
+ return keys;
+ }
+
+ public ResolvedTargets<Target> getTargets() {
+ return targets;
+ }
+
+ /**
+ * A TargetPattern is a tuple of pattern (eg, "foo/..."), filtering policy, a relative pattern
+ * offset, and whether it is a positive or negative match.
+ */
+ @ThreadSafe
+ public static class TargetPattern implements Serializable {
+ private final String pattern;
+ private final FilteringPolicy policy;
+ private final boolean isNegative;
+
+ private final String offset;
+
+ public TargetPattern(String pattern, FilteringPolicy policy,
+ boolean isNegative, String offset) {
+ this.pattern = Preconditions.checkNotNull(pattern);
+ this.policy = Preconditions.checkNotNull(policy);
+ this.isNegative = isNegative;
+ this.offset = offset;
+ }
+
+ public String getPattern() {
+ return pattern;
+ }
+
+ public boolean isNegative() {
+ return isNegative;
+ }
+
+ public FilteringPolicy getPolicy() {
+ return policy;
+ }
+
+ public String getOffset() {
+ return offset;
+ }
+
+ @Override
+ public String toString() {
+ return (isNegative ? "-" : "") + pattern;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(pattern, isNegative, policy, offset);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof TargetPattern)) {
+ return false;
+ }
+ TargetPattern other = (TargetPattern) obj;
+
+ return other.isNegative == this.isNegative && other.pattern.equals(this.pattern) &&
+ other.offset.equals(this.offset) && other.policy.equals(this.policy);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/TestCompletionFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/TestCompletionFunction.java
new file mode 100644
index 0000000..b6f9606
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/TestCompletionFunction.java
@@ -0,0 +1,71 @@
+// 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.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.LabelAndConfiguration;
+import com.google.devtools.build.lib.rules.test.TestProvider;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+/**
+ * TargetCompletionFunction builds all relevant test artifacts of a {@link
+ * com.google.devtools.build.lib.analysis.ConfiguredTarget}. This includes test shards and repeated
+ * runs.
+ */
+public final class TestCompletionFunction implements SkyFunction {
+
+ public TestCompletionFunction() {
+ }
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) {
+ TestCompletionValue.TestCompletionKey key =
+ (TestCompletionValue.TestCompletionKey) skyKey.argument();
+ LabelAndConfiguration lac = key.getLabelAndConfiguration();
+ if (env.getValue(TargetCompletionValue.key(lac)) == null) {
+ return null;
+ }
+
+ ConfiguredTargetValue ctValue = (ConfiguredTargetValue)
+ env.getValue(ConfiguredTargetValue.key(lac.getLabel(), lac.getConfiguration()));
+ if (ctValue == null) {
+ return null;
+ }
+
+ ConfiguredTarget ct = ctValue.getConfiguredTarget();
+ if (key.isExclusiveTesting()) {
+ // Request test artifacts iteratively if testing exclusively.
+ for (Artifact testArtifact : TestProvider.getTestStatusArtifacts(ct)) {
+ if (env.getValue(ArtifactValue.key(testArtifact, /*isMandatory=*/true)) == null) {
+ return null;
+ }
+ }
+ } else {
+ env.getValues(ArtifactValue.mandatoryKeys(TestProvider.getTestStatusArtifacts(ct)));
+ if (env.valuesMissing()) {
+ return null;
+ }
+ }
+ return TestCompletionValue.TEST_COMPLETION_MARKER;
+ }
+
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return Label.print(((LabelAndConfiguration) skyKey.argument()).getLabel());
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/TestCompletionValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/TestCompletionValue.java
new file mode 100644
index 0000000..da944ed
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/TestCompletionValue.java
@@ -0,0 +1,66 @@
+// 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.Function;
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.LabelAndConfiguration;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.util.Collection;
+
+/**
+ * A test completion value represents the completion of a test target. This includes the execution
+ * of all test shards and repeated runs, if applicable.
+ */
+public class TestCompletionValue implements SkyValue {
+ static final TestCompletionValue TEST_COMPLETION_MARKER = new TestCompletionValue();
+
+ private TestCompletionValue() { }
+
+ public static SkyKey key(LabelAndConfiguration lac, boolean exclusive) {
+ return new SkyKey(SkyFunctions.TEST_COMPLETION, new TestCompletionKey(lac, exclusive));
+ }
+
+ public static Iterable<SkyKey> keys(Collection<ConfiguredTarget> targets,
+ final boolean exclusive) {
+ return Iterables.transform(targets, new Function<ConfiguredTarget, SkyKey>() {
+ @Override
+ public SkyKey apply(ConfiguredTarget ct) {
+ return new SkyKey(SkyFunctions.TEST_COMPLETION,
+ new TestCompletionKey(new LabelAndConfiguration(ct), exclusive));
+ }
+ });
+ }
+
+ static class TestCompletionKey {
+ private final LabelAndConfiguration lac;
+ private final boolean exclusiveTesting;
+
+ TestCompletionKey(LabelAndConfiguration lac, boolean exclusive) {
+ this.lac = lac;
+ this.exclusiveTesting = exclusive;
+ }
+
+ public LabelAndConfiguration getLabelAndConfiguration() {
+ return lac;
+ }
+
+ public boolean isExclusiveTesting() {
+ return exclusiveTesting;
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/TransitiveTargetCycleReporter.java b/src/main/java/com/google/devtools/build/lib/skyframe/TransitiveTargetCycleReporter.java
new file mode 100644
index 0000000..03bbd25
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/TransitiveTargetCycleReporter.java
@@ -0,0 +1,86 @@
+// 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.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.devtools.build.lib.packages.PackageGroup;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.pkgcache.LoadedPackageProvider;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.skyframe.CycleInfo;
+import com.google.devtools.build.skyframe.SkyKey;
+
+import java.util.List;
+
+/**
+ * Reports cycles between {@link TransitiveTargetValue}s. These indicates cycles between targets
+ * (e.g. '//a:foo' depends on '//b:bar' and '//b:bar' depends on '//a:foo').
+ */
+class TransitiveTargetCycleReporter extends AbstractLabelCycleReporter {
+
+ private static final Predicate<SkyKey> IS_TRANSITIVE_TARGET_SKY_KEY =
+ SkyFunctions.isSkyFunction(SkyFunctions.TRANSITIVE_TARGET);
+
+ TransitiveTargetCycleReporter(LoadedPackageProvider loadedPackageProvider) {
+ super(loadedPackageProvider);
+ }
+
+ @Override
+ protected boolean canReportCycle(SkyKey topLevelKey, CycleInfo cycleInfo) {
+ return Iterables.all(Iterables.concat(ImmutableList.of(topLevelKey),
+ cycleInfo.getPathToCycle(), cycleInfo.getCycle()),
+ IS_TRANSITIVE_TARGET_SKY_KEY);
+ }
+
+ @Override
+ public String prettyPrint(SkyKey key) {
+ return getLabel(key).toString();
+ }
+
+ @Override
+ protected Label getLabel(SkyKey key) {
+ return (Label) key.argument();
+ }
+
+ @Override
+ protected String getAdditionalMessageAboutCycle(SkyKey topLevelKey, CycleInfo cycleInfo) {
+ Target currentTarget = getTargetForLabel(getLabel(topLevelKey));
+ List<SkyKey> keys = Lists.newArrayList();
+ if (!cycleInfo.getPathToCycle().isEmpty()) {
+ keys.add(topLevelKey);
+ keys.addAll(cycleInfo.getPathToCycle());
+ }
+ keys.addAll(cycleInfo.getCycle());
+ // Make sure we check the edge from the last element of the cycle to the first element of the
+ // cycle.
+ keys.add(cycleInfo.getCycle().get(0));
+ for (SkyKey nextKey : keys) {
+ Label nextLabel = getLabel(nextKey);
+ Target nextTarget = getTargetForLabel(nextLabel);
+ // This is inefficient but it's no big deal since we only do this when there's a cycle.
+ if (currentTarget.getVisibility().getDependencyLabels().contains(nextLabel)
+ && !nextTarget.getTargetKind().equals(PackageGroup.targetKind())) {
+ return "\nThe cycle is caused by a visibility edge from " + currentTarget.getLabel()
+ + " to the non-package-group target " + nextTarget.getLabel() + " . Note that "
+ + "visibility labels are supposed to be package group targets (which prevents cycles "
+ + "of this form)";
+ }
+ currentTarget = nextTarget;
+ }
+ return "";
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/TransitiveTargetFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/TransitiveTargetFunction.java
new file mode 100644
index 0000000..417cfca
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/TransitiveTargetFunction.java
@@ -0,0 +1,234 @@
+// 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.collect.Lists;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.packages.InputFile;
+import com.google.devtools.build.lib.packages.NoSuchPackageException;
+import com.google.devtools.build.lib.packages.NoSuchTargetException;
+import com.google.devtools.build.lib.packages.NoSuchThingException;
+import com.google.devtools.build.lib.packages.OutputFile;
+import com.google.devtools.build.lib.packages.PackageGroup;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.packages.Rule;
+import com.google.devtools.build.lib.packages.Target;
+import com.google.devtools.build.lib.packages.TargetUtils;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyFunctionException;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+import com.google.devtools.build.skyframe.ValueOrException;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * This class builds transitive Target values such that evaluating a Target value is similar to
+ * running it through the LabelVisitor.
+ */
+public class TransitiveTargetFunction implements SkyFunction {
+
+ @Override
+ public SkyValue compute(SkyKey key, Environment env) throws TransitiveTargetFunctionException {
+ Label label = (Label) key.argument();
+ SkyKey packageKey = PackageValue.key(label.getPackageIdentifier());
+ SkyKey targetKey = TargetMarkerValue.key(label);
+ Target target;
+ boolean packageLoadedSuccessfully;
+ boolean successfulTransitiveLoading = true;
+ NestedSetBuilder<Label> transitiveRootCauses = NestedSetBuilder.stableOrder();
+ NoSuchTargetException errorLoadingTarget = null;
+ try {
+ TargetMarkerValue targetValue = (TargetMarkerValue) env.getValueOrThrow(targetKey,
+ NoSuchThingException.class);
+ if (targetValue == null) {
+ return null;
+ }
+ PackageValue packageValue = (PackageValue) env.getValueOrThrow(packageKey,
+ NoSuchThingException.class);
+ if (packageValue == null) {
+ return null;
+ }
+
+ packageLoadedSuccessfully = true;
+ target = packageValue.getPackage().getTarget(label.getName());
+ } catch (NoSuchTargetException e) {
+ target = e.getTarget();
+ if (target == null) {
+ throw new TransitiveTargetFunctionException(e);
+ }
+ successfulTransitiveLoading = false;
+ transitiveRootCauses.add(label);
+ errorLoadingTarget = e;
+ packageLoadedSuccessfully = e.getPackageLoadedSuccessfully();
+ } catch (NoSuchPackageException e) {
+ throw new TransitiveTargetFunctionException(e);
+ } catch (NoSuchThingException e) {
+ throw new IllegalStateException(e
+ + " not NoSuchTargetException or NoSuchPackageException");
+ }
+
+ NestedSetBuilder<PackageIdentifier> transitiveSuccessfulPkgs = NestedSetBuilder.stableOrder();
+ NestedSetBuilder<PackageIdentifier> transitiveUnsuccessfulPkgs = NestedSetBuilder.stableOrder();
+ NestedSetBuilder<Label> transitiveTargets = NestedSetBuilder.stableOrder();
+
+ PackageIdentifier packageId = target.getPackage().getPackageIdentifier();
+ if (packageLoadedSuccessfully) {
+ transitiveSuccessfulPkgs.add(packageId);
+ } else {
+ transitiveUnsuccessfulPkgs.add(packageId);
+ }
+ transitiveTargets.add(target.getLabel());
+ for (Map.Entry<SkyKey, ValueOrException<NoSuchThingException>> entry :
+ env.getValuesOrThrow(getLabelDepKeys(target), NoSuchThingException.class).entrySet()) {
+ Label depLabel = (Label) entry.getKey().argument();
+ TransitiveTargetValue transitiveTargetValue;
+ try {
+ transitiveTargetValue = (TransitiveTargetValue) entry.getValue().get();
+ if (transitiveTargetValue == null) {
+ continue;
+ }
+ } catch (NoSuchPackageException | NoSuchTargetException e) {
+ successfulTransitiveLoading = false;
+ transitiveRootCauses.add(depLabel);
+ maybeReportErrorAboutMissingEdge(target, depLabel, e, env.getListener());
+ continue;
+ } catch (NoSuchThingException e) {
+ throw new IllegalStateException("Unexpected Exception type from TransitiveTargetValue.", e);
+ }
+ transitiveSuccessfulPkgs.addTransitive(
+ transitiveTargetValue.getTransitiveSuccessfulPackages());
+ transitiveUnsuccessfulPkgs.addTransitive(
+ transitiveTargetValue.getTransitiveUnsuccessfulPackages());
+ transitiveTargets.addTransitive(transitiveTargetValue.getTransitiveTargets());
+ NestedSet<Label> rootCauses = transitiveTargetValue.getTransitiveRootCauses();
+ if (rootCauses != null) {
+ successfulTransitiveLoading = false;
+ transitiveRootCauses.addTransitive(rootCauses);
+ if (transitiveTargetValue.getErrorLoadingTarget() != null) {
+ maybeReportErrorAboutMissingEdge(target, depLabel,
+ transitiveTargetValue.getErrorLoadingTarget(), env.getListener());
+ }
+ }
+ }
+
+ if (env.valuesMissing()) {
+ return null;
+ }
+
+ NestedSet<PackageIdentifier> successfullyLoadedPackages = transitiveSuccessfulPkgs.build();
+ NestedSet<PackageIdentifier> unsuccessfullyLoadedPackages = transitiveUnsuccessfulPkgs.build();
+ NestedSet<Label> loadedTargets = transitiveTargets.build();
+ if (successfulTransitiveLoading) {
+ return TransitiveTargetValue.successfulTransitiveLoading(successfullyLoadedPackages,
+ unsuccessfullyLoadedPackages, loadedTargets);
+ } else {
+ NestedSet<Label> rootCauses = transitiveRootCauses.build();
+ return TransitiveTargetValue.unsuccessfulTransitiveLoading(successfullyLoadedPackages,
+ unsuccessfullyLoadedPackages, loadedTargets, rootCauses, errorLoadingTarget);
+ }
+ }
+
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return Label.print(((Label) skyKey.argument()));
+ }
+
+ private static void maybeReportErrorAboutMissingEdge(Target target, Label depLabel,
+ NoSuchThingException e, EventHandler eventHandler) {
+ if (e instanceof NoSuchTargetException) {
+ NoSuchTargetException nste = (NoSuchTargetException) e;
+ if (depLabel.equals(nste.getLabel())) {
+ eventHandler.handle(Event.error(TargetUtils.getLocationMaybe(target),
+ TargetUtils.formatMissingEdge(target, depLabel, e)));
+ }
+ } else if (e instanceof NoSuchPackageException) {
+ NoSuchPackageException nspe = (NoSuchPackageException) e;
+ if (nspe.getPackageName().equals(depLabel.getPackageName())) {
+ eventHandler.handle(Event.error(TargetUtils.getLocationMaybe(target),
+ TargetUtils.formatMissingEdge(target, depLabel, e)));
+ }
+ }
+ }
+
+ private static Iterable<SkyKey> getLabelDepKeys(Target target) {
+ List<SkyKey> depKeys = Lists.newArrayList();
+ for (Label depLabel : getLabelDeps(target)) {
+ depKeys.add(TransitiveTargetValue.key(depLabel));
+ }
+ return depKeys;
+ }
+
+ // TODO(bazel-team): Unify this logic with that in LabelVisitor, and possibly DependencyResolver.
+ private static Iterable<Label> getLabelDeps(Target target) {
+ final Set<Label> labels = new HashSet<>();
+ if (target instanceof OutputFile) {
+ Rule rule = ((OutputFile) target).getGeneratingRule();
+ labels.add(rule.getLabel());
+ visitTargetVisibility(target, labels);
+ } else if (target instanceof InputFile) {
+ visitTargetVisibility(target, labels);
+ } else if (target instanceof Rule) {
+ visitTargetVisibility(target, labels);
+ labels.addAll(((Rule) target).getLabels(Rule.NO_NODEP_ATTRIBUTES));
+ } else if (target instanceof PackageGroup) {
+ visitPackageGroup((PackageGroup) target, labels);
+ }
+ return labels;
+ }
+
+ private static void visitTargetVisibility(Target target, Set<Label> labels) {
+ for (Label label : target.getVisibility().getDependencyLabels()) {
+ labels.add(label);
+ }
+ }
+
+ private static void visitPackageGroup(PackageGroup packageGroup, Set<Label> labels) {
+ for (final Label include : packageGroup.getIncludes()) {
+ labels.add(include);
+ }
+ }
+
+ /**
+ * Used to declare all the exception types that can be wrapped in the exception thrown by
+ * {@link TransitiveTargetFunction#compute}.
+ */
+ private static class TransitiveTargetFunctionException extends SkyFunctionException {
+ /**
+ * Used to propagate an error from a direct target dependency to the
+ * target that depended on it.
+ */
+ public TransitiveTargetFunctionException(NoSuchPackageException e) {
+ super(e, Transience.PERSISTENT);
+ }
+
+ /**
+ * In nokeep_going mode, used to propagate an error from a direct target dependency to the
+ * target that depended on it.
+ *
+ * In keep_going mode, used the same way, but only for targets that could not be loaded at all
+ * (we proceed with transitive loading on targets that contain errors).
+ */
+ public TransitiveTargetFunctionException(NoSuchTargetException e) {
+ super(e, Transience.PERSISTENT);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/TransitiveTargetValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/TransitiveTargetValue.java
new file mode 100644
index 0000000..69b9638
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/TransitiveTargetValue.java
@@ -0,0 +1,142 @@
+// 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.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
+import com.google.devtools.build.lib.collect.nestedset.Order;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+import com.google.devtools.build.lib.packages.NoSuchTargetException;
+import com.google.devtools.build.lib.packages.PackageIdentifier;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * A <i>transitive</i> target reference that, when built in skyframe, loads the entire
+ * transitive closure of a target.
+ *
+ * This will probably be unnecessary once other refactorings occur throughout the codebase
+ * which make loading/analysis interleaving more feasible, or we will migrate "blaze query" to
+ * use this to evaluate its Target graph.
+ */
+@Immutable
+@ThreadSafe
+public class TransitiveTargetValue implements SkyValue {
+
+ // Non-final for serialization purposes.
+ private NestedSet<PackageIdentifier> transitiveSuccessfulPkgs;
+ private NestedSet<PackageIdentifier> transitiveUnsuccessfulPkgs;
+ private NestedSet<Label> transitiveTargets;
+ @Nullable private NestedSet<Label> transitiveRootCauses;
+ @Nullable private NoSuchTargetException errorLoadingTarget;
+
+ private TransitiveTargetValue(NestedSet<PackageIdentifier> transitiveSuccessfulPkgs,
+ NestedSet<PackageIdentifier> transitiveUnsuccessfulPkgs, NestedSet<Label> transitiveTargets,
+ @Nullable NestedSet<Label> transitiveRootCauses,
+ @Nullable NoSuchTargetException errorLoadingTarget) {
+ this.transitiveSuccessfulPkgs = transitiveSuccessfulPkgs;
+ this.transitiveUnsuccessfulPkgs = transitiveUnsuccessfulPkgs;
+ this.transitiveTargets = transitiveTargets;
+ this.transitiveRootCauses = transitiveRootCauses;
+ this.errorLoadingTarget = errorLoadingTarget;
+ }
+
+ private void writeObject(ObjectOutputStream out) throws IOException {
+ // It helps to flatten the transitiveSuccessfulPkgs nested set as it has lots of duplicates.
+ Set<PackageIdentifier> successfulPkgs = transitiveSuccessfulPkgs.toSet();
+ out.writeInt(successfulPkgs.size());
+ for (PackageIdentifier pkg : successfulPkgs) {
+ out.writeObject(pkg);
+ }
+
+ out.writeObject(transitiveUnsuccessfulPkgs);
+ // Deliberately do not write out transitiveTargets. There is a lot of those and they drive
+ // serialization costs through the roof, both in terms of space and of time.
+ // TODO(bazel-team): Deal with this properly once we have efficient serialization of NestedSets.
+ out.writeObject(transitiveRootCauses);
+ out.writeObject(errorLoadingTarget);
+ }
+
+ @SuppressWarnings("unchecked")
+ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
+ int successfulPkgCount = in.readInt();
+ NestedSetBuilder<PackageIdentifier> pkgs = NestedSetBuilder.stableOrder();
+ for (int i = 0; i < successfulPkgCount; i++) {
+ pkgs.add((PackageIdentifier) in.readObject());
+ }
+ transitiveSuccessfulPkgs = pkgs.build();
+ transitiveUnsuccessfulPkgs = (NestedSet<PackageIdentifier>) in.readObject();
+ // TODO(bazel-team): Deal with transitiveTargets properly.
+ transitiveTargets = NestedSetBuilder.emptySet(Order.STABLE_ORDER);
+ transitiveRootCauses = (NestedSet<Label>) in.readObject();
+ errorLoadingTarget = (NoSuchTargetException) in.readObject();
+ }
+
+ static TransitiveTargetValue unsuccessfulTransitiveLoading(
+ NestedSet<PackageIdentifier> transitiveSuccessfulPkgs,
+ NestedSet<PackageIdentifier> transitiveUnsuccessfulPkgs, NestedSet<Label> transitiveTargets,
+ NestedSet<Label> rootCauses, @Nullable NoSuchTargetException errorLoadingTarget) {
+ return new TransitiveTargetValue(transitiveSuccessfulPkgs, transitiveUnsuccessfulPkgs,
+ transitiveTargets, rootCauses, errorLoadingTarget);
+ }
+
+ static TransitiveTargetValue successfulTransitiveLoading(
+ NestedSet<PackageIdentifier> transitiveSuccessfulPkgs,
+ NestedSet<PackageIdentifier> transitiveUnsuccessfulPkgs,
+ NestedSet<Label> transitiveTargets) {
+ return new TransitiveTargetValue(transitiveSuccessfulPkgs, transitiveUnsuccessfulPkgs,
+ transitiveTargets, null, null);
+ }
+
+ /** Returns the error, if any, from loading the target. */
+ @Nullable
+ public NoSuchTargetException getErrorLoadingTarget() {
+ return errorLoadingTarget;
+ }
+
+ /** Returns the packages that were transitively successfully loaded. */
+ public NestedSet<PackageIdentifier> getTransitiveSuccessfulPackages() {
+ return transitiveSuccessfulPkgs;
+ }
+
+ /** Returns the packages that were transitively successfully loaded. */
+ public NestedSet<PackageIdentifier> getTransitiveUnsuccessfulPackages() {
+ return transitiveUnsuccessfulPkgs;
+ }
+
+ /** Returns the targets that were transitively loaded. */
+ public NestedSet<Label> getTransitiveTargets() {
+ return transitiveTargets;
+ }
+
+ /** Returns the root causes, if any, of why targets weren't loaded. */
+ @Nullable
+ public NestedSet<Label> getTransitiveRootCauses() {
+ return transitiveRootCauses;
+ }
+
+ @ThreadSafe
+ public static SkyKey key(Label label) {
+ return new SkyKey(SkyFunctions.TRANSITIVE_TARGET, label);
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/WorkspaceFileFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/WorkspaceFileFunction.java
new file mode 100644
index 0000000..f518b8a
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/WorkspaceFileFunction.java
@@ -0,0 +1,224 @@
+// 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 static com.google.devtools.build.lib.syntax.Environment.NONE;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.cmdline.LabelValidator;
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.StoredEventHandler;
+import com.google.devtools.build.lib.packages.ExternalPackage.Binding;
+import com.google.devtools.build.lib.packages.ExternalPackage.ExternalPackageBuilder;
+import com.google.devtools.build.lib.packages.ExternalPackage.ExternalPackageBuilder.NoSuchBindingException;
+import com.google.devtools.build.lib.packages.Package.NameConflictException;
+import com.google.devtools.build.lib.packages.PackageFactory;
+import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.packages.RuleFactory;
+import com.google.devtools.build.lib.packages.Type;
+import com.google.devtools.build.lib.packages.Type.ConversionException;
+import com.google.devtools.build.lib.syntax.AbstractFunction;
+import com.google.devtools.build.lib.syntax.BuildFileAST;
+import com.google.devtools.build.lib.syntax.EvalException;
+import com.google.devtools.build.lib.syntax.FuncallExpression;
+import com.google.devtools.build.lib.syntax.Function;
+import com.google.devtools.build.lib.syntax.Label;
+import com.google.devtools.build.lib.syntax.Label.SyntaxException;
+import com.google.devtools.build.lib.syntax.MixedModeFunction;
+import com.google.devtools.build.lib.syntax.ParserInputSource;
+import com.google.devtools.build.lib.vfs.Path;
+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 java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A SkyFunction to parse WORKSPACE files.
+ */
+public class WorkspaceFileFunction implements SkyFunction {
+
+ private static final String BIND = "bind";
+
+ private final PackageFactory packageFactory;
+
+ WorkspaceFileFunction(PackageFactory packageFactory) {
+ this.packageFactory = packageFactory;
+ }
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) throws WorkspaceFileFunctionException,
+ InterruptedException {
+ RootedPath workspaceRoot = (RootedPath) skyKey.argument();
+ // Explicitly make skyframe load this file.
+ if (env.getValue(FileValue.key(workspaceRoot)) == null) {
+ return null;
+ }
+ Path workspaceFilePath = workspaceRoot.getRoot().getRelative(workspaceRoot.getRelativePath());
+ WorkspaceNameHolder holder = new WorkspaceNameHolder();
+ ExternalPackageBuilder builder = new ExternalPackageBuilder(workspaceFilePath);
+ StoredEventHandler localReporter = new StoredEventHandler();
+ BuildFileAST buildFileAST;
+ ParserInputSource inputSource = null;
+
+ try {
+ inputSource = ParserInputSource.create(workspaceFilePath);
+ } catch (IOException e) {
+ throw new WorkspaceFileFunctionException(e, Transience.TRANSIENT);
+ }
+ buildFileAST = BuildFileAST.parseBuildFile(inputSource, localReporter, null, false);
+ if (buildFileAST.containsErrors()) {
+ localReporter.handle(Event.error("WORKSPACE file could not be parsed"));
+ } else {
+ try {
+ if (!evaluateWorkspaceFile(buildFileAST, holder, builder)) {
+ localReporter.handle(
+ Event.error("Error evaluating WORKSPACE file " + workspaceFilePath));
+ }
+ } catch (EvalException e) {
+ throw new WorkspaceFileFunctionException(e);
+ }
+ }
+
+ builder.addEvents(localReporter.getEvents());
+ if (localReporter.hasErrors()) {
+ builder.setContainsErrors();
+ }
+ return new WorkspaceFileValue(holder.workspaceName, builder.build());
+ }
+
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+
+ private static Function newWorkspaceNameFunction(final WorkspaceNameHolder holder) {
+ List<String> params = ImmutableList.of("name");
+ return new MixedModeFunction("workspace", params, 1, true) {
+ @Override
+ public Object call(Object[] namedArgs, FuncallExpression ast) throws EvalException,
+ ConversionException, InterruptedException {
+ String name = Type.STRING.convert(namedArgs[0], "'name' argument");
+ String errorMessage = LabelValidator.validateTargetName(name);
+ if (errorMessage != null) {
+ throw new EvalException(ast.getLocation(), errorMessage);
+ }
+ holder.workspaceName = name;
+ return NONE;
+ }
+ };
+ }
+
+ private static Function newBindFunction(final ExternalPackageBuilder builder) {
+ List<String> params = ImmutableList.of("name", "actual");
+ return new MixedModeFunction(BIND, params, 2, true) {
+ @Override
+ public Object call(Object[] namedArgs, FuncallExpression ast)
+ throws EvalException, ConversionException {
+ String name = Type.STRING.convert(namedArgs[0], "'name' argument");
+ String actual = Type.STRING.convert(namedArgs[1], "'actual' argument");
+
+ Label nameLabel = null;
+ try {
+ nameLabel = Label.parseAbsolute("//external:" + name);
+ builder.addBinding(
+ nameLabel, new Binding(Label.parseRepositoryLabel(actual), ast.getLocation()));
+ } catch (SyntaxException e) {
+ throw new EvalException(ast.getLocation(), e.getMessage());
+ }
+
+ return NONE;
+ }
+ };
+ }
+
+ /**
+ * Returns a function-value implementing the build rule "ruleClass" (e.g. cc_library) in the
+ * specified package context.
+ */
+ private static Function newRuleFunction(final RuleFactory ruleFactory,
+ final ExternalPackageBuilder builder, final String ruleClassName) {
+ return new AbstractFunction(ruleClassName) {
+ @Override
+ public Object call(List<Object> args, Map<String, Object> kwargs, FuncallExpression ast,
+ com.google.devtools.build.lib.syntax.Environment env)
+ throws EvalException {
+ if (!args.isEmpty()) {
+ throw new EvalException(ast.getLocation(),
+ "build rules do not accept positional parameters");
+ }
+
+ try {
+ RuleClass ruleClass = ruleFactory.getRuleClass(ruleClassName);
+ builder.createAndAddRepositoryRule(ruleClass, kwargs, ast);
+ } catch (RuleFactory.InvalidRuleException | NameConflictException | SyntaxException e) {
+ throw new EvalException(ast.getLocation(), e.getMessage());
+ }
+ return NONE;
+ }
+ };
+ }
+
+ public boolean evaluateWorkspaceFile(BuildFileAST buildFileAST, WorkspaceNameHolder holder,
+ ExternalPackageBuilder builder)
+ throws InterruptedException, EvalException, WorkspaceFileFunctionException {
+ // Environment is defined in SkyFunction and the syntax package.
+ com.google.devtools.build.lib.syntax.Environment workspaceEnv =
+ new com.google.devtools.build.lib.syntax.Environment();
+
+ RuleFactory ruleFactory = new RuleFactory(packageFactory.getRuleClassProvider());
+ for (String ruleClass : ruleFactory.getRuleClassNames()) {
+ Function ruleFunction = newRuleFunction(ruleFactory, builder, ruleClass);
+ workspaceEnv.update(ruleClass, ruleFunction);
+ }
+
+ workspaceEnv.update(BIND, newBindFunction(builder));
+ workspaceEnv.update("workspace", newWorkspaceNameFunction(holder));
+
+ StoredEventHandler eventHandler = new StoredEventHandler();
+ if (!buildFileAST.exec(workspaceEnv, eventHandler)) {
+ return false;
+ }
+ try {
+ builder.resolveBindTargets(packageFactory.getRuleClass(BIND));
+ } catch (NoSuchBindingException e) {
+ throw new WorkspaceFileFunctionException(e);
+ }
+ return true;
+ }
+
+ private static final class WorkspaceNameHolder {
+ String workspaceName;
+ }
+
+ private static final class WorkspaceFileFunctionException extends SkyFunctionException {
+ public WorkspaceFileFunctionException(IOException e, Transience transience) {
+ super(e, transience);
+ }
+
+ public WorkspaceFileFunctionException(NoSuchBindingException e) {
+ super(e, Transience.PERSISTENT);
+ }
+
+ public WorkspaceFileFunctionException(EvalException e) {
+ super(e, Transience.PERSISTENT);
+ }
+ }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/WorkspaceFileValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/WorkspaceFileValue.java
new file mode 100644
index 0000000..200f23f
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/WorkspaceFileValue.java
@@ -0,0 +1,59 @@
+// 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.devtools.build.lib.packages.ExternalPackage;
+import com.google.devtools.build.lib.vfs.RootedPath;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+import javax.annotation.Nullable;
+
+/**
+ * Holds the contents of a WORKSPACE file as the //external package.
+ */
+public class WorkspaceFileValue implements SkyValue {
+
+ private final String workspace;
+ private final ExternalPackage pkg;
+
+ public WorkspaceFileValue(String workspace, ExternalPackage pkg) {
+ this.workspace = workspace;
+ this.pkg = pkg;
+ }
+
+ /**
+ * Returns the name of this workspace (or null for the default workspace).
+ */
+ @Nullable
+ public String getWorkspace() {
+ return workspace;
+ }
+
+ /**
+ * Returns the //external package.
+ */
+ public ExternalPackage getPackage() {
+ return pkg;
+ }
+
+ /**
+ * Generates a SkyKey based on the path to the WORKSPACE file.
+ */
+ public static SkyKey key(RootedPath workspacePath) {
+ return new SkyKey(SkyFunctions.WORKSPACE_FILE, workspacePath);
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/WorkspaceStatusFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/WorkspaceStatusFunction.java
new file mode 100644
index 0000000..b1c5ef3
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/WorkspaceStatusFunction.java
@@ -0,0 +1,48 @@
+// 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.devtools.build.lib.analysis.WorkspaceStatusAction;
+import com.google.devtools.build.skyframe.SkyFunction;
+import com.google.devtools.build.skyframe.SkyKey;
+import com.google.devtools.build.skyframe.SkyValue;
+
+/** Creates the workspace status artifacts and action. */
+public class WorkspaceStatusFunction implements SkyFunction {
+ WorkspaceStatusFunction() {
+ }
+
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env) {
+ Preconditions.checkState(
+ WorkspaceStatusValue.SKY_KEY.equals(skyKey), WorkspaceStatusValue.SKY_KEY);
+
+ WorkspaceStatusAction action = PrecomputedValue.WORKSPACE_STATUS_KEY.get(env);
+ if (action == null) {
+ return null;
+ }
+
+ return new WorkspaceStatusValue(
+ action.getStableStatus(),
+ action.getVolatileStatus(),
+ action);
+ }
+
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/WorkspaceStatusValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/WorkspaceStatusValue.java
new file mode 100644
index 0000000..21b9215
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/WorkspaceStatusValue.java
@@ -0,0 +1,62 @@
+// 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.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.ArtifactOwner;
+import com.google.devtools.build.lib.analysis.WorkspaceStatusAction;
+import com.google.devtools.build.skyframe.SkyFunctionName;
+import com.google.devtools.build.skyframe.SkyKey;
+
+/**
+ * Value that stores the workspace status artifacts and their generating action. There should be
+ * only one of these values in the graph at any time.
+ */
+// TODO(bazel-team): This seems to be superfluous now, but it cannot be removed without making
+// PrecomputedValue public instead of package-private
+public class WorkspaceStatusValue extends ActionLookupValue {
+ private final Artifact stableArtifact;
+ private final Artifact volatileArtifact;
+
+ // There should only ever be one BuildInfo value in the graph.
+ public static final SkyKey SKY_KEY = new SkyKey(SkyFunctions.BUILD_INFO, "BUILD_INFO");
+ static final ArtifactOwner ARTIFACT_OWNER = new BuildInfoKey();
+
+ public WorkspaceStatusValue(Artifact stableArtifact, Artifact volatileArtifact,
+ WorkspaceStatusAction action) {
+ super(action);
+ this.stableArtifact = stableArtifact;
+ this.volatileArtifact = volatileArtifact;
+ }
+
+ public Artifact getStableArtifact() {
+ return stableArtifact;
+ }
+
+ public Artifact getVolatileArtifact() {
+ return volatileArtifact;
+ }
+
+ private static class BuildInfoKey extends ActionLookupKey {
+ @Override
+ SkyFunctionName getType() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ SkyKey getSkyKey() {
+ return SKY_KEY;
+ }
+ }
+}