Implement changes in symlink forest creation related to dont_symlink_directories_in_execroot.

This is a continuation of https://github.com/bazelbuild/bazel/pull/10589.

If dont_symlink_directories_in_execroot is used in WORKSPACE file with some directories (immediate children of the workspace root directory), do not symlink those directories under the execroot during symlink forest creation.
(See SymlinkForest class for more context, and also https://docs.bazel.build/versions/master/output_directories.html)

Normally, source directories are symlinked to the execroot, so that the actions can access the input (source) files.
In the case of Ninja execution (enabled with --experimental_ninja_actions flag), it is typical that the directory with build-related files contains source files for the build, and Ninja prescribes creation of the outputs in that same directory.
Since commands in the Ninja file use relative paths to address source files and directories, we must recreate the exact same directory structure under the execroot.

However, Bazel requires output directories to be separated from input directories: it is Bazel?s concept, not something that is related to this change or Ninja integration.

That is why Ninja build configuration directory (though it is part of the sources in the sense that it contains the source files) can not be directly symlinked to the execroot (if it was, the source tree would be changed by the build).

dont_symlink_directories_in_execroot can be used to specify that Ninja build configuration directories should not be symlinked to the execroot.
It is not expected that there could be other use cases for using this method.

Closes #10600.

PiperOrigin-RevId: 291681522
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/AnalysisResult.java b/src/main/java/com/google/devtools/build/lib/analysis/AnalysisResult.java
index 0675ba9..01a490b 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/AnalysisResult.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/AnalysisResult.java
@@ -16,6 +16,7 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedSet;
 import com.google.devtools.build.lib.actions.ActionGraph;
 import com.google.devtools.build.lib.actions.PackageRoots;
 import com.google.devtools.build.lib.analysis.config.BuildConfigurationCollection;
@@ -41,6 +42,7 @@
   private final PackageRoots packageRoots;
   private final String workspaceName;
   private final Collection<TargetAndConfiguration> topLevelTargetsWithConfigs;
+  private final ImmutableSortedSet<String> nonSymlinkedDirectoriesUnderExecRoot;
 
   AnalysisResult(
       BuildConfigurationCollection configurations,
@@ -56,7 +58,8 @@
       TopLevelArtifactContext topLevelContext,
       PackageRoots packageRoots,
       String workspaceName,
-      Collection<TargetAndConfiguration> topLevelTargetsWithConfigs) {
+      Collection<TargetAndConfiguration> topLevelTargetsWithConfigs,
+      ImmutableSortedSet<String> nonSymlinkedDirectoriesUnderExecRoot) {
     this.configurations = configurations;
     this.targetsToBuild = targetsToBuild;
     this.aspects = aspects;
@@ -71,6 +74,7 @@
     this.packageRoots = packageRoots;
     this.workspaceName = workspaceName;
     this.topLevelTargetsWithConfigs = topLevelTargetsWithConfigs;
+    this.nonSymlinkedDirectoriesUnderExecRoot = nonSymlinkedDirectoriesUnderExecRoot;
   }
 
   public BuildConfigurationCollection getConfigurationCollection() {
@@ -159,4 +163,8 @@
   public Collection<TargetAndConfiguration> getTopLevelTargetsWithConfigs() {
     return topLevelTargetsWithConfigs;
   }
+
+  public ImmutableSortedSet<String> getNonSymlinkedDirectoriesUnderExecRoot() {
+    return nonSymlinkedDirectoriesUnderExecRoot;
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/BuildView.java b/src/main/java/com/google/devtools/build/lib/analysis/BuildView.java
index f8ea726..f2ff198 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/BuildView.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/BuildView.java
@@ -553,7 +553,8 @@
         topLevelOptions,
         skyframeAnalysisResult.getPackageRoots(),
         loadingResult.getWorkspaceName(),
-        topLevelTargetsWithConfigs.getTargetsAndConfigs());
+        topLevelTargetsWithConfigs.getTargetsAndConfigs(),
+        loadingResult.getNotSymlinkedInExecrootDirectories());
   }
 
   /**
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/ExecutionTool.java b/src/main/java/com/google/devtools/build/lib/buildtool/ExecutionTool.java
index fec76b9..fc343e4 100644
--- a/src/main/java/com/google/devtools/build/lib/buildtool/ExecutionTool.java
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/ExecutionTool.java
@@ -22,6 +22,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedSet;
 import com.google.devtools.build.lib.actions.Action;
 import com.google.devtools.build.lib.actions.ActionCacheChecker;
 import com.google.devtools.build.lib.actions.ActionGraph;
@@ -221,7 +222,7 @@
       TopLevelArtifactContext topLevelArtifactContext)
       throws BuildFailedException, InterruptedException, TestExecException, AbruptExitException {
     Stopwatch timer = Stopwatch.createStarted();
-    prepare(packageRoots);
+    prepare(packageRoots, analysisResult.getNonSymlinkedDirectoriesUnderExecRoot());
 
     ActionGraph actionGraph = analysisResult.getActionGraph();
 
@@ -406,8 +407,9 @@
     }
   }
 
-  private void prepare(PackageRoots packageRoots)
-      throws ExecutorInitException, InterruptedException {
+  private void prepare(
+      PackageRoots packageRoots, ImmutableSortedSet<String> nonSymlinkedDirectoriesUnderExecRoot)
+      throws AbruptExitException, InterruptedException {
     Optional<ImmutableMap<PackageIdentifier, Root>> packageRootMap =
         packageRoots.getPackageRootsMap();
     if (packageRootMap.isPresent()) {
@@ -416,8 +418,13 @@
 
       // Plant the symlink forest.
       try (SilentCloseable c = Profiler.instance().profile("plantSymlinkForest")) {
-        new SymlinkForest(packageRootMap.get(), getExecRoot(), runtime.getProductName())
-            .plantSymlinkForest();
+        SymlinkForest symlinkForest =
+            new SymlinkForest(
+                packageRootMap.get(),
+                getExecRoot(),
+                runtime.getProductName(),
+                nonSymlinkedDirectoriesUnderExecRoot);
+        symlinkForest.plantSymlinkForest();
       } catch (IOException e) {
         throw new ExecutorInitException("Source forest creation failed", e);
       }
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/SymlinkForest.java b/src/main/java/com/google/devtools/build/lib/buildtool/SymlinkForest.java
index 8561d67..bfd5d92 100644
--- a/src/main/java/com/google/devtools/build/lib/buildtool/SymlinkForest.java
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/SymlinkForest.java
@@ -16,6 +16,7 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSortedSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
@@ -23,6 +24,11 @@
 import com.google.devtools.build.lib.cmdline.PackageIdentifier;
 import com.google.devtools.build.lib.cmdline.RepositoryName;
 import com.google.devtools.build.lib.concurrent.ThreadSafety;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.syntax.Sequence;
+import com.google.devtools.build.lib.syntax.StarlarkThread;
+import com.google.devtools.build.lib.util.AbruptExitException;
+import com.google.devtools.build.lib.util.ExitCode;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.PathFragment;
 import com.google.devtools.build.lib.vfs.Root;
@@ -44,6 +50,13 @@
   private final Path execroot;
   private final String productName;
   private final String prefix;
+  private final ImmutableSortedSet<String> notSymlinkedInExecrootDirectories;
+
+  /** Constructor for a symlink forest creator without non-symlinked directories parameter. */
+  public SymlinkForest(
+      ImmutableMap<PackageIdentifier, Root> packageRoots, Path execroot, String productName) {
+    this(packageRoots, execroot, productName, ImmutableSortedSet.of());
+  }
 
   /**
    * Constructor for a symlink forest creator; does not perform any i/o.
@@ -53,13 +66,20 @@
    * @param packageRoots source package roots to which to create symlinks
    * @param execroot path where to plant the symlink forest
    * @param productName {@code BlazeRuntime#getProductName()}
+   * @param notSymlinkedInExecrootDirectories directories to not symlink in exec root. {@link
+   *     com.google.devtools.build.lib.packages.WorkspaceGlobals#dontSymlinkDirectoriesInExecroot(Sequence,
+   *     Location, StarlarkThread)}
    */
   public SymlinkForest(
-      ImmutableMap<PackageIdentifier, Root> packageRoots, Path execroot, String productName) {
+      ImmutableMap<PackageIdentifier, Root> packageRoots,
+      Path execroot,
+      String productName,
+      ImmutableSortedSet<String> notSymlinkedInExecrootDirectories) {
     this.packageRoots = packageRoots;
     this.execroot = execroot;
     this.productName = productName;
     this.prefix = productName + "-";
+    this.notSymlinkedInExecrootDirectories = notSymlinkedInExecrootDirectories;
   }
 
   /**
@@ -116,6 +136,9 @@
     // instead of the directory itself.
     for (Path target : mainRepoRoot.getDirectoryEntries()) {
       String baseName = target.getBaseName();
+      if (this.notSymlinkedInExecrootDirectories.contains(baseName)) {
+        continue;
+      }
       Path execPath = execroot.getRelative(baseName);
       // Create any links that don't start with bazel-, and ignore external/ directory if
       // user has it in the source tree because it conflicts with external repository location.
@@ -126,11 +149,18 @@
     }
   }
 
-  private static void plantSymlinkForestWithPartialMainRepository(Map<Path, Path> mainRepoLinks)
-      throws IOException {
+  private void plantSymlinkForestWithPartialMainRepository(Map<Path, Path> mainRepoLinks)
+      throws IOException, AbruptExitException {
     for (Map.Entry<Path, Path> entry : mainRepoLinks.entrySet()) {
       Path link = entry.getKey();
       Path target = entry.getValue();
+      if (this.notSymlinkedInExecrootDirectories.contains(target.getBaseName())) {
+        throw new AbruptExitException(
+            "Directories specified with "
+                + "dont_symlink_directories_in_execroot should be ignored and can not be used"
+                + " as sources.",
+            ExitCode.COMMAND_LINE_ERROR);
+      }
       link.createSymbolicLink(target);
     }
   }
@@ -263,7 +293,7 @@
   }
 
   /** Performs the filesystem operations to plant the symlink forest. */
-  public void plantSymlinkForest() throws IOException {
+  public void plantSymlinkForest() throws IOException, AbruptExitException {
     deleteTreesBelowNotPrefixed(execroot, prefix);
 
     boolean shouldLinkAllTopLevelItems = false;
@@ -317,6 +347,12 @@
     // removed in the future, we should remove the plantSymlinkForestMultiPackagePath
     // implementation when --package_path is gone.
     if (mainRepoRoots.size() > 1) {
+      if (!this.notSymlinkedInExecrootDirectories.isEmpty()) {
+        throw new AbruptExitException(
+            "dont_symlink_directories_in_execroot is "
+                + "not supported together with --package_path option.",
+            ExitCode.COMMAND_LINE_ERROR);
+      }
       plantSymlinkForestMultiPackagePath(packageRootsForMainRepo);
     } else if (shouldLinkAllTopLevelItems) {
       Path mainRepoRoot = Iterables.getOnlyElement(mainRepoRoots).asPath();
diff --git a/src/main/java/com/google/devtools/build/lib/repository/ExternalPackageUtil.java b/src/main/java/com/google/devtools/build/lib/repository/ExternalPackageUtil.java
index d308ccb..d5d558d 100644
--- a/src/main/java/com/google/devtools/build/lib/repository/ExternalPackageUtil.java
+++ b/src/main/java/com/google/devtools/build/lib/repository/ExternalPackageUtil.java
@@ -1,4 +1,4 @@
-// Copyright 2017 The Bazel Authors. All rights reserved.
+// Copyright 2019 The Bazel Authors. All rights reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -14,11 +14,7 @@
 
 package com.google.devtools.build.lib.repository;
 
-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.common.collect.Lists;
+import com.google.common.collect.ImmutableSortedSet;
 import com.google.devtools.build.lib.cmdline.LabelConstants;
 import com.google.devtools.build.lib.events.Event;
 import com.google.devtools.build.lib.packages.BuildFileContainsErrorsException;
@@ -30,58 +26,32 @@
 import com.google.devtools.build.skyframe.SkyFunction.Environment;
 import com.google.devtools.build.skyframe.SkyFunctionException.Transience;
 import com.google.devtools.build.skyframe.SkyKey;
-import java.util.List;
 import javax.annotation.Nullable;
 
-/** Utility class to centralize looking up rules from the external package. */
+/** Utility class to centralize looking up data from the external package. */
 public class ExternalPackageUtil {
-
   /**
-   * Loads the external package and then calls the selector to find matching rules.
+   * Returns directories, that should not be symlinked under the execroot.
    *
-   * @param env the environment to use for lookups
-   * @param returnFirst whether to return only the first rule found
-   * @param selector the function to call to load rules
+   * <p>Searches for dont_symlink_directories_in_execroot calls in the WORKSPACE file, and gathers
+   * values of all "paths" attributes.
    */
-  @Nullable
-  private static List<Rule> getRules(
-      Environment env, boolean returnFirst, Function<Package, List<Rule>> selector)
-      throws ExternalPackageException, InterruptedException {
-    SkyKey packageLookupKey = PackageLookupValue.key(LabelConstants.EXTERNAL_PACKAGE_IDENTIFIER);
-    PackageLookupValue packageLookupValue = (PackageLookupValue) env.getValue(packageLookupKey);
-    if (packageLookupValue == null) {
+  public static ImmutableSortedSet<String> getNotSymlinkedInExecrootDirectories(Environment env)
+      throws InterruptedException {
+    ImmutableSortedSet.Builder<String> builder = ImmutableSortedSet.naturalOrder();
+    WorkspaceFileValueProcessor gatherer =
+        workspaceFileValue -> {
+          ImmutableSortedSet<String> paths = workspaceFileValue.getDoNotSymlinkInExecrootPaths();
+          if (paths != null) {
+            builder.addAll(paths);
+          }
+          // Continue to read all the fragments.
+          return true;
+        };
+    if (!iterateWorkspaceFragments(env, gatherer)) {
       return null;
     }
-    RootedPath workspacePath =
-        packageLookupValue.getRootedPath(LabelConstants.EXTERNAL_PACKAGE_IDENTIFIER);
-
-    List<Rule> rules = returnFirst ? ImmutableList.of() : Lists.newArrayList();
-    SkyKey workspaceKey = WorkspaceFileValue.key(workspacePath);
-    do {
-      WorkspaceFileValue value = (WorkspaceFileValue) env.getValue(workspaceKey);
-      if (value == null) {
-        return null;
-      }
-      Package externalPackage = value.getPackage();
-      if (externalPackage.containsErrors()) {
-        Event.replayEventsOn(env.getListener(), externalPackage.getEvents());
-        throw new ExternalPackageException(
-            new BuildFileContainsErrorsException(
-                LabelConstants.EXTERNAL_PACKAGE_IDENTIFIER, "Could not load //external package"),
-            Transience.PERSISTENT);
-      }
-      List<Rule> results = selector.apply(externalPackage);
-      if (results != null && !results.isEmpty()) {
-        if (returnFirst) {
-          // assert expected non null value explicitly for possible future callers
-          return ImmutableList.of(Preconditions.checkNotNull(results.get(0)));
-        }
-        rules.addAll(results);
-      }
-      workspaceKey = value.next();
-    } while (workspaceKey != null);
-
-    return rules;
+    return builder.build();
   }
 
   /** Uses a rule name to fetch the corresponding Rule from the external package. */
@@ -89,28 +59,79 @@
   public static Rule getRuleByName(final String ruleName, Environment env)
       throws ExternalPackageException, InterruptedException {
 
-    List<Rule> rules =
-        getRules(
-            env,
-            true,
-            new Function<Package, List<Rule>>() {
-              @Nullable
-              @Override
-              public List<Rule> apply(Package externalPackage) {
-                Rule rule = externalPackage.getRule(ruleName);
-                if (rule == null) {
-                  return null;
-                }
-                return ImmutableList.of(rule);
-              }
-            });
-
-    if (env.valuesMissing()) {
+    ExternalPackageRuleExtractor extractor = new ExternalPackageRuleExtractor(env, ruleName);
+    if (!iterateWorkspaceFragments(env, extractor)) {
+      // Values missing
       return null;
     }
-    if (rules == null || rules.isEmpty()) {
-      throw new ExternalRuleNotFoundException(ruleName);
+
+    return extractor.getRule();
+  }
+
+  /** Returns false if some SkyValues were missing. */
+  private static boolean iterateWorkspaceFragments(
+      Environment env, WorkspaceFileValueProcessor processor) throws InterruptedException {
+    SkyKey packageLookupKey = PackageLookupValue.key(LabelConstants.EXTERNAL_PACKAGE_IDENTIFIER);
+    PackageLookupValue packageLookupValue = (PackageLookupValue) env.getValue(packageLookupKey);
+    if (packageLookupValue == null) {
+      return false;
     }
-    return Iterables.getFirst(rules, null);
+    RootedPath workspacePath =
+        packageLookupValue.getRootedPath(LabelConstants.EXTERNAL_PACKAGE_IDENTIFIER);
+
+    SkyKey workspaceKey = WorkspaceFileValue.key(workspacePath);
+    WorkspaceFileValue value;
+    do {
+      value = (WorkspaceFileValue) env.getValue(workspaceKey);
+      if (value == null) {
+        return false;
+      }
+    } while (processor.processAndShouldContinue(value) && (workspaceKey = value.next()) != null);
+    return true;
+  }
+
+  private static class ExternalPackageRuleExtractor implements WorkspaceFileValueProcessor {
+    private final Environment env;
+    private final String ruleName;
+    private ExternalPackageException exception;
+    private Rule rule;
+
+    private ExternalPackageRuleExtractor(Environment env, String ruleName) {
+      this.env = env;
+      this.ruleName = ruleName;
+    }
+
+    @Override
+    public boolean processAndShouldContinue(WorkspaceFileValue workspaceFileValue) {
+      Package externalPackage = workspaceFileValue.getPackage();
+      if (externalPackage.containsErrors()) {
+        Event.replayEventsOn(env.getListener(), externalPackage.getEvents());
+        exception =
+            new ExternalPackageException(
+                new BuildFileContainsErrorsException(
+                    LabelConstants.EXTERNAL_PACKAGE_IDENTIFIER,
+                    "Could not load //external package"),
+                Transience.PERSISTENT);
+        // Stop iteration when encountered errors.
+        return false;
+      }
+      rule = externalPackage.getRule(ruleName);
+      // Stop if the rule is found = continue while it is null.
+      return rule == null;
+    }
+
+    public Rule getRule() throws ExternalPackageException {
+      if (exception != null) {
+        throw exception;
+      }
+      if (rule == null) {
+        throw new ExternalRuleNotFoundException(ruleName);
+      }
+      return rule;
+    }
+  }
+
+  private interface WorkspaceFileValueProcessor {
+    boolean processAndShouldContinue(WorkspaceFileValue value);
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/TargetPatternPhaseFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/TargetPatternPhaseFunction.java
index 3ebdae0..32c362e 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/TargetPatternPhaseFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/TargetPatternPhaseFunction.java
@@ -22,6 +22,7 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSetMultimap;
+import com.google.common.collect.ImmutableSortedSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
 import com.google.devtools.build.lib.cmdline.Label;
@@ -45,6 +46,7 @@
 import com.google.devtools.build.lib.pkgcache.ParsingFailedEvent;
 import com.google.devtools.build.lib.pkgcache.TargetParsingCompleteEvent;
 import com.google.devtools.build.lib.pkgcache.TestFilter;
+import com.google.devtools.build.lib.repository.ExternalPackageUtil;
 import com.google.devtools.build.lib.skyframe.TargetPatternPhaseValue.TargetPatternPhaseKey;
 import com.google.devtools.build.lib.skyframe.TargetPatternValue.TargetPatternKey;
 import com.google.devtools.build.lib.skyframe.TargetPatternValue.TargetPatternSkyKeyOrException;
@@ -85,6 +87,8 @@
       env.getListener().handle(Event.error(e.getMessage()));
       workspaceError = true;
     }
+    ImmutableSortedSet<String> notSymlinkedInExecrootDirectories =
+        ExternalPackageUtil.getNotSymlinkedInExecrootDirectories(env);
     if (env.valuesMissing()) {
       return null;
     }
@@ -226,7 +230,8 @@
             testsToRunLabels,
             targets.hasError(),
             expandedTargets.hasError() || workspaceError,
-            workspaceName);
+            workspaceName,
+            notSymlinkedInExecrootDirectories);
 
     env.getListener()
         .post(
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/TargetPatternPhaseValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/TargetPatternPhaseValue.java
index a5998ae..ed9a855 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/TargetPatternPhaseValue.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/TargetPatternPhaseValue.java
@@ -17,6 +17,7 @@
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedSet;
 import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
@@ -51,18 +52,21 @@
   private final boolean hasError;
   private final boolean hasPostExpansionError;
   private final String workspaceName;
+  private final ImmutableSortedSet<String> notSymlinkedInExecrootDirectories;
 
   TargetPatternPhaseValue(
       ImmutableSet<Label> targetLabels,
       ImmutableSet<Label> testsToRunLabels,
       boolean hasError,
       boolean hasPostExpansionError,
-      String workspaceName) {
+      String workspaceName,
+      ImmutableSortedSet<String> notSymlinkedInExecrootDirectories) {
     this.targetLabels = targetLabels;
     this.testsToRunLabels = testsToRunLabels;
     this.hasError = hasError;
     this.hasPostExpansionError = hasPostExpansionError;
     this.workspaceName = workspaceName;
+    this.notSymlinkedInExecrootDirectories = notSymlinkedInExecrootDirectories;
   }
 
   private static ImmutableSet<Target> getTargetsFromLabels(
@@ -116,6 +120,10 @@
     return workspaceName;
   }
 
+  public ImmutableSortedSet<String> getNotSymlinkedInExecrootDirectories() {
+    return notSymlinkedInExecrootDirectories;
+  }
+
   @Override
   public boolean equals(Object obj) {
     if (this == obj) {
@@ -128,6 +136,8 @@
     return Objects.equals(this.targetLabels, that.targetLabels)
         && Objects.equals(this.testsToRunLabels, that.testsToRunLabels)
         && Objects.equals(this.workspaceName, that.workspaceName)
+        && Objects.equals(
+            this.notSymlinkedInExecrootDirectories, that.notSymlinkedInExecrootDirectories)
         && this.hasError == that.hasError
         && this.hasPostExpansionError == that.hasPostExpansionError;
   }
@@ -139,7 +149,8 @@
         this.testsToRunLabels,
         this.workspaceName,
         this.hasError,
-        this.hasPostExpansionError);
+        this.hasPostExpansionError,
+        this.notSymlinkedInExecrootDirectories);
   }
 
   /** Create a target pattern phase value key. */
diff --git a/src/test/java/com/google/devtools/build/lib/buildtool/BUILD b/src/test/java/com/google/devtools/build/lib/buildtool/BUILD
index 37f7f65..c4509ce 100644
--- a/src/test/java/com/google/devtools/build/lib/buildtool/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/buildtool/BUILD
@@ -72,6 +72,7 @@
         "//src/main/java/com/google/devtools/build/lib:build-request-options",
         "//src/main/java/com/google/devtools/build/lib:packages-internal",
         "//src/main/java/com/google/devtools/build/lib:runtime",
+        "//src/main/java/com/google/devtools/build/lib:util",
         "//src/main/java/com/google/devtools/build/lib/actions:localhost_capacity",
         "//src/main/java/com/google/devtools/build/lib/vfs",
         "//src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs",
diff --git a/src/test/java/com/google/devtools/build/lib/buildtool/SymlinkForestTest.java b/src/test/java/com/google/devtools/build/lib/buildtool/SymlinkForestTest.java
index 4be5fa1..9792ccc 100644
--- a/src/test/java/com/google/devtools/build/lib/buildtool/SymlinkForestTest.java
+++ b/src/test/java/com/google/devtools/build/lib/buildtool/SymlinkForestTest.java
@@ -1,4 +1,4 @@
-// Copyright 2016 The Bazel Authors. All rights reserved.
+// Copyright 2019 The Bazel Authors. All rights reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -11,19 +11,23 @@
 // 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.buildtool;
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.devtools.build.lib.testutil.MoreAsserts.assertThrows;
 
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedSet;
 import com.google.devtools.build.lib.cmdline.LabelConstants;
 import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
 import com.google.devtools.build.lib.cmdline.PackageIdentifier;
 import com.google.devtools.build.lib.cmdline.RepositoryName;
 import com.google.devtools.build.lib.testutil.ManualClock;
 import com.google.devtools.build.lib.testutil.TestConstants;
+import com.google.devtools.build.lib.util.AbruptExitException;
 import com.google.devtools.build.lib.vfs.FileSystem;
 import com.google.devtools.build.lib.vfs.FileSystemUtils;
 import com.google.devtools.build.lib.vfs.Path;
@@ -180,7 +184,7 @@
   }
 
   @Test
-  public void testPlantLinkForestWithMultiplePackagePath() throws IOException {
+  public void testPlantLinkForestWithMultiplePackagePath() throws Exception {
     Root rootA = Root.fromPath(fileSystem.getPath("/A"));
     Root rootB = Root.fromPath(fileSystem.getPath("/B"));
 
@@ -200,7 +204,8 @@
 
     Path linkRoot = fileSystem.getPath("/linkRoot");
     linkRoot.createDirectoryAndParents();
-    new SymlinkForest(packageRootMap, linkRoot, TestConstants.PRODUCT_NAME).plantSymlinkForest();
+    new SymlinkForest(packageRootMap, linkRoot, TestConstants.PRODUCT_NAME, ImmutableSortedSet.of())
+        .plantSymlinkForest();
 
     assertLinksTo(linkRoot, rootA, "pkgA");
     assertIsDir(linkRoot, "dir1");
@@ -227,7 +232,8 @@
             .put(createPkg(rootX, rootY, "foo"), rootX)
             .build();
 
-    new SymlinkForest(packageRootMap, linkRoot, TestConstants.PRODUCT_NAME).plantSymlinkForest();
+    new SymlinkForest(packageRootMap, linkRoot, TestConstants.PRODUCT_NAME, ImmutableSortedSet.of())
+        .plantSymlinkForest();
     assertLinksTo(linkRoot, rootX, "file");
   }
 
@@ -254,7 +260,8 @@
             .put(createExternalPkg(outputBase, "Z", ""), outputBase)
             .build();
 
-    new SymlinkForest(packageRootMap, linkRoot, TestConstants.PRODUCT_NAME).plantSymlinkForest();
+    new SymlinkForest(packageRootMap, linkRoot, TestConstants.PRODUCT_NAME, ImmutableSortedSet.of())
+        .plantSymlinkForest();
 
     assertLinksTo(linkRoot, mainRepo, "dir_main");
     assertLinksTo(linkRoot, mainRepo, "dir_lib");
@@ -297,7 +304,8 @@
             .put(createExternalPkg(outputBase, "X", "dir_x/pkg"), outputBase)
             .build();
 
-    new SymlinkForest(packageRootMap, linkRoot, TestConstants.PRODUCT_NAME).plantSymlinkForest();
+    new SymlinkForest(packageRootMap, linkRoot, TestConstants.PRODUCT_NAME, ImmutableSortedSet.of())
+        .plantSymlinkForest();
 
     assertLinksTo(linkRoot, mainRepo, "dir1");
     assertLinksTo(linkRoot, mainRepo, "dir2");
@@ -329,7 +337,8 @@
             .put(createExternalPkg(outputBase, "X", "dir_x/pkg"), outputBase)
             .build();
 
-    new SymlinkForest(packageRootMap, linkRoot, TestConstants.PRODUCT_NAME).plantSymlinkForest();
+    new SymlinkForest(packageRootMap, linkRoot, TestConstants.PRODUCT_NAME, ImmutableSortedSet.of())
+        .plantSymlinkForest();
 
     assertLinksTo(linkRoot, mainRepo, "dir1");
     assertLinksTo(linkRoot, mainRepo, "dir2");
@@ -359,7 +368,8 @@
             .put(createExternalPkg(outputBase, "X", "dir_x/pkg"), outputBase)
             .build();
 
-    new SymlinkForest(packageRootMap, linkRoot, TestConstants.PRODUCT_NAME).plantSymlinkForest();
+    new SymlinkForest(packageRootMap, linkRoot, TestConstants.PRODUCT_NAME, ImmutableSortedSet.of())
+        .plantSymlinkForest();
 
     assertLinksTo(linkRoot, mainRepo, "dir1");
     assertLinksTo(linkRoot, mainRepo, "dir2");
@@ -380,7 +390,134 @@
             .put(LabelConstants.EXTERNAL_PACKAGE_IDENTIFIER, root)
             .build();
 
-    new SymlinkForest(packageRootMap, linkRoot, TestConstants.PRODUCT_NAME).plantSymlinkForest();
+    new SymlinkForest(packageRootMap, linkRoot, TestConstants.PRODUCT_NAME, ImmutableSortedSet.of())
+        .plantSymlinkForest();
     assertThat(linkRoot.getRelative(LabelConstants.EXTERNAL_PATH_PREFIX).exists()).isFalse();
   }
+
+  @Test
+  public void testNotSymlinkedDirectoriesInExecRootAllInMainRepo() throws Exception {
+    Root outputBase = Root.fromPath(fileSystem.getPath("/ob"));
+    Root mainRepo = Root.fromPath(fileSystem.getPath("/my_repo"));
+    Path linkRoot = outputBase.getRelative("execroot/ws_name");
+
+    linkRoot.createDirectoryAndParents();
+    mainRepo.asPath().createDirectoryAndParents();
+    mainRepo.getRelative("dir3").createDirectoryAndParents();
+    mainRepo.getRelative("build").createDirectoryAndParents();
+
+    ImmutableMap<PackageIdentifier, Root> packageRootMap =
+        ImmutableMap.<PackageIdentifier, Root>builder()
+            .put(createMainPkg(mainRepo, "dir1/pkg/foo"), mainRepo)
+            .put(createMainPkg(mainRepo, "dir2/pkg"), mainRepo)
+            // Empty package will cause every top-level files to be linked, except external/
+            .put(createMainPkg(mainRepo, ""), mainRepo)
+            .put(createExternalPkg(outputBase, "X", "dir_x/pkg"), outputBase)
+            .build();
+
+    new SymlinkForest(
+            packageRootMap, linkRoot, TestConstants.PRODUCT_NAME, ImmutableSortedSet.of("build"))
+        .plantSymlinkForest();
+
+    assertLinksTo(linkRoot, mainRepo, "dir1");
+    assertLinksTo(linkRoot, mainRepo, "dir2");
+    assertLinksTo(linkRoot, mainRepo, "dir3");
+    assertLinksTo(linkRoot, outputBase, LabelConstants.EXTERNAL_PATH_PREFIX + "/X");
+    assertThat(linkRoot.getChild("build").exists()).isFalse();
+  }
+
+  @Test
+  public void testNotSymlinkedDirectoriesInExecRootPartialMainRepo1() throws Exception {
+    Root outputBase = Root.fromPath(fileSystem.getPath("/ob"));
+    Root mainRepo = Root.fromPath(fileSystem.getPath("/my_repo"));
+    Path linkRoot = outputBase.getRelative("execroot/ws_name");
+
+    linkRoot.createDirectoryAndParents();
+    mainRepo.asPath().createDirectoryAndParents();
+    mainRepo.getRelative("dir3").createDirectoryAndParents();
+    mainRepo.getRelative("build").createDirectoryAndParents();
+
+    ImmutableMap<PackageIdentifier, Root> packageRootMap =
+        ImmutableMap.<PackageIdentifier, Root>builder()
+            .put(createMainPkg(mainRepo, "dir1/pkg/foo"), mainRepo)
+            .put(createMainPkg(mainRepo, "dir2/pkg"), mainRepo)
+            .put(createExternalPkg(outputBase, "X", "dir_x/pkg"), outputBase)
+            .build();
+
+    new SymlinkForest(
+            packageRootMap, linkRoot, TestConstants.PRODUCT_NAME, ImmutableSortedSet.of("build"))
+        .plantSymlinkForest();
+
+    assertLinksTo(linkRoot, mainRepo, "dir1");
+    assertLinksTo(linkRoot, mainRepo, "dir2");
+    assertLinksTo(linkRoot, outputBase, LabelConstants.EXTERNAL_PATH_PREFIX + "/X");
+    assertThat(linkRoot.getChild("build").exists()).isFalse();
+    // Not part of the package roots.
+    assertThat(linkRoot.getChild("dir3").exists()).isFalse();
+  }
+
+  @Test
+  public void testNotSymlinkedDirectoriesInExecRootPartialMainRepo2() throws Exception {
+    Root outputBase = Root.fromPath(fileSystem.getPath("/ob"));
+    Root mainRepo = Root.fromPath(fileSystem.getPath("/my_repo"));
+    Path linkRoot = outputBase.getRelative("execroot/ws_name");
+
+    linkRoot.createDirectoryAndParents();
+    mainRepo.asPath().createDirectoryAndParents();
+
+    ImmutableMap<PackageIdentifier, Root> packageRootMap =
+        ImmutableMap.of(createMainPkg(mainRepo, "build"), mainRepo);
+
+    AbruptExitException exception =
+        assertThrows(
+            AbruptExitException.class,
+            () ->
+                new SymlinkForest(
+                        packageRootMap,
+                        linkRoot,
+                        TestConstants.PRODUCT_NAME,
+                        ImmutableSortedSet.of("build"))
+                    .plantSymlinkForest());
+    assertThat(exception)
+        .hasMessageThat()
+        .isEqualTo(
+            "Directories specified with dont_symlink_directories_in_execroot should be "
+                + "ignored and can not be used as sources.");
+  }
+
+  @Test
+  public void testNotSymlinkedDirectoriesInExecRootMultiplePackageRoots() throws Exception {
+    Root outputBase = Root.fromPath(fileSystem.getPath("/ob"));
+    Root mainRepo = Root.fromPath(fileSystem.getPath("/my_repo"));
+    Root otherRepo = Root.fromPath(fileSystem.getPath("/other_repo"));
+    Path linkRoot = outputBase.getRelative("execroot/ws_name");
+
+    mainRepo.getRelative("build").createDirectoryAndParents();
+
+    linkRoot.createDirectoryAndParents();
+    mainRepo.asPath().createDirectoryAndParents();
+    otherRepo.asPath().createDirectoryAndParents();
+
+    ImmutableMap<PackageIdentifier, Root> packageRootMap =
+        ImmutableMap.<PackageIdentifier, Root>builder()
+            .put(createMainPkg(mainRepo, "dir1"), mainRepo)
+            .put(createMainPkg(otherRepo, "dir2"), otherRepo)
+            .build();
+
+    AbruptExitException exception =
+        assertThrows(
+            AbruptExitException.class,
+            () ->
+                new SymlinkForest(
+                        packageRootMap,
+                        linkRoot,
+                        TestConstants.PRODUCT_NAME,
+                        ImmutableSortedSet.of("build"))
+                    .plantSymlinkForest());
+    assertThat(exception)
+        .hasMessageThat()
+        .isEqualTo(
+            "dont_symlink_directories_in_execroot is not supported together "
+                + "with --package_path option.");
+  }
 }