Reduce filesystem stats in `SymlinkAction`.

`SymlinkAction` checks whether the symlink target is an executable file. If the filesystem implementation supports returning permission bits in the stat (supported in the common case, i.e. `UnixFileSystem`), then a single stat suffices for both `isFile()` and `isExecutable()`.

Moreover, for a source artifact, we can use the build's `SyscallCache` since it was already statted by `FileStateFunction`.

PiperOrigin-RevId: 690819972
Change-Id: Ibd1f8d7aa9c3bd185a965f5a2ec015513ef315be
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/BUILD b/src/main/java/com/google/devtools/build/lib/analysis/BUILD
index 76d8d05..5537966 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/analysis/BUILD
@@ -1607,6 +1607,7 @@
         "//src/main/java/com/google/devtools/build/lib/analysis/platform",
         "//src/main/java/com/google/devtools/build/lib/collect/nestedset",
         "//src/main/java/com/google/devtools/build/lib/exec:spawn_log_context",
+        "//src/main/java/com/google/devtools/build/lib/unix",
         "//src/main/java/com/google/devtools/build/lib/util",
         "//src/main/java/com/google/devtools/build/lib/util:detailed_exit_code",
         "//src/main/java/com/google/devtools/build/lib/vfs",
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/actions/SymlinkAction.java b/src/main/java/com/google/devtools/build/lib/analysis/actions/SymlinkAction.java
index 76f0df3..f6ca7b9 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/actions/SymlinkAction.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/actions/SymlinkAction.java
@@ -14,6 +14,8 @@
 
 package com.google.devtools.build.lib.analysis.actions;
 
+import static com.google.devtools.build.lib.unix.FileStatus.S_IXUSR;
+
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
@@ -34,8 +36,11 @@
 import com.google.devtools.build.lib.server.FailureDetails.SymlinkAction.Code;
 import com.google.devtools.build.lib.util.DetailedExitCode;
 import com.google.devtools.build.lib.util.Fingerprint;
+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.devtools.build.lib.vfs.SyscallCache;
 import java.io.IOException;
 import javax.annotation.Nullable;
 
@@ -246,19 +251,32 @@
       return;
     }
 
-    Path inputPath = actionExecutionContext.getInputPath(getPrimaryInput());
+    Artifact primaryInput = getPrimaryInput();
+    Path inputPath = actionExecutionContext.getInputPath(primaryInput);
+
+    // Source artifacts are probably in the syscall cache. Generated artifacts are probably not.
+    SyscallCache syscallCache =
+        primaryInput.isSourceArtifact()
+            ? actionExecutionContext.getSyscallCache()
+            : SyscallCache.NO_CACHE;
     try {
-      // Validate that input path is a file with the executable bit set.
-      if (!inputPath.isFile()) {
-        String message = String.format("'%s' is not a file", getPrimaryInput().getExecPathString());
+      FileStatus stat = syscallCache.statIfFound(inputPath, Symlinks.FOLLOW);
+      if (stat == null || !stat.isFile()) {
+        String message = String.format("'%s' is not a file", primaryInput.getExecPathString());
         throw new ActionExecutionException(
             message, this, false, createDetailedExitCode(message, Code.EXECUTABLE_INPUT_NOT_FILE));
       }
-      if (!inputPath.isExecutable()) {
+      boolean isExecutable;
+      if (stat.getPermissions() != -1) {
+        isExecutable = (stat.getPermissions() & S_IXUSR) != 0;
+      } else {
+        isExecutable = inputPath.isExecutable();
+      }
+      if (!isExecutable) {
         String message =
             String.format(
                 "failed to create symbolic link '%s': file '%s' is not executable",
-                getPrimaryOutput().getExecPathString(), getPrimaryInput().getExecPathString());
+                getPrimaryOutput().getExecPathString(), primaryInput.getExecPathString());
         throw new ActionExecutionException(
             message, this, false, createDetailedExitCode(message, Code.EXECUTABLE_INPUT_IS_NOT));
       }
@@ -267,7 +285,7 @@
           String.format(
               "failed to create symbolic link '%s' to the '%s' due to I/O error: %s",
               getPrimaryOutput().getExecPathString(),
-              getPrimaryInput().getExecPathString(),
+              primaryInput.getExecPathString(),
               e.getMessage());
       DetailedExitCode detailedExitCode =
           createDetailedExitCode(message, Code.EXECUTABLE_INPUT_CHECK_IO_EXCEPTION);