Use ctime in file digest cache key (#18101)

File digests are now additionally keyed by ctime for supported file system implementations. Since Bazel has a non-zero default for `--cache_computed_file_digests`, this may be required for correctness in cases where different files have identical mtime and inode number. For example, this can happen on Linux when files are extracted from a tar file with fixed mtime and are then replaced with `mv`, which preserves inodes.

Since Java (N)IO doesn't have support for reading file ctimes on Windows, a new method backed by a native implementation is added to `WindowsFileOperation`. Adding a call to this function to `stat` uncovered previously silent bugs where Unix-style `PathFragment`s were created on Windows:

1. Bzlmod's `createLocalRepoSpec` did not correctly extract the path from a registry's `file://` URI on Windows.
2. `--package_path` isn't usable with absolute paths on Windows as it splits on `:`. Since the flag is deprecated, this commit fixes the tests rather than the implementation.

Fixes #14723

Closes #18003.

PiperOrigin-RevId: 524297459
Change-Id: I96bfc0210e2f71bf8603c7b7cc5eb06a04048c85

Co-authored-by: Fabian Meumertzheim <fabian@meumertzhe.im>
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BUILD b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BUILD
index e6a4c7d..a2f01a4 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BUILD
@@ -74,6 +74,7 @@
         "//src/main/java/com/google/devtools/build/lib/bazel/repository/downloader",
         "//src/main/java/com/google/devtools/build/lib/cmdline",
         "//src/main/java/com/google/devtools/build/lib/events",
+        "//src/main/java/com/google/devtools/build/lib/util:os",
         "//src/main/java/com/google/devtools/build/lib/vfs:pathfragment",
         "//third_party:gson",
         "//third_party:guava",
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/IndexRegistry.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/IndexRegistry.java
index 869ae3b..83266c3 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/IndexRegistry.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/IndexRegistry.java
@@ -24,6 +24,7 @@
 import com.google.devtools.build.lib.bazel.repository.downloader.DownloadManager;
 import com.google.devtools.build.lib.cmdline.RepositoryName;
 import com.google.devtools.build.lib.events.ExtendedEventHandler;
+import com.google.devtools.build.lib.util.OS;
 import com.google.devtools.build.lib.vfs.PathFragment;
 import com.google.gson.FieldNamingPolicy;
 import com.google.gson.Gson;
@@ -175,7 +176,15 @@
       path = moduleBase + "/" + path;
       if (!PathFragment.isAbsolute(moduleBase)) {
         if (uri.getScheme().equals("file")) {
-          path = uri.getPath() + "/" + path;
+          if (uri.getPath().isEmpty() || !uri.getPath().startsWith("/")) {
+            throw new IOException(
+                String.format(
+                    "Provided non absolute local registry path for module %s: %s",
+                    key, uri.getPath()));
+          }
+          // Unix:    file:///tmp --> /tmp
+          // Windows: file:///C:/tmp --> C:/tmp
+          path = uri.getPath().substring(OS.getCurrent() == OS.WINDOWS ? 1 : 0) + "/" + path;
         } else {
           throw new IOException(String.format("Provided non local registry for module %s", key));
         }
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ActionMetadataHandler.java b/src/main/java/com/google/devtools/build/lib/skyframe/ActionMetadataHandler.java
index af848e3..f0efff1 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/ActionMetadataHandler.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ActionMetadataHandler.java
@@ -719,7 +719,10 @@
   }
 
   private void setPathPermissionsIfFile(Path path) throws IOException {
-    if (path.isFile(Symlinks.NOFOLLOW)) {
+    FileStatus stat = path.statIfFound(Symlinks.NOFOLLOW);
+    if (stat != null
+        && stat.isFile()
+        && stat.getPermissions() != outputPermissions.getPermissionsMode()) {
       setPathPermissions(path);
     }
   }
diff --git a/src/main/java/com/google/devtools/build/lib/unix/UnixFileSystem.java b/src/main/java/com/google/devtools/build/lib/unix/UnixFileSystem.java
index c92fba6..0e12321 100644
--- a/src/main/java/com/google/devtools/build/lib/unix/UnixFileSystem.java
+++ b/src/main/java/com/google/devtools/build/lib/unix/UnixFileSystem.java
@@ -120,7 +120,8 @@
       return status.getInodeNumber();
     }
 
-    int getPermissions() {
+    @Override
+    public int getPermissions() {
       return status.getPermissions();
     }
 
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/DigestUtils.java b/src/main/java/com/google/devtools/build/lib/vfs/DigestUtils.java
index 6c2f2b5..77bd98d 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/DigestUtils.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/DigestUtils.java
@@ -46,6 +46,9 @@
     /** File system identifier of the file (typically the inode number). */
     private final long nodeId;
 
+    /** Last change time of the file. */
+    private final long changeTime;
+
     /** Last modification time of the file. */
     private final long modifiedTime;
 
@@ -62,6 +65,7 @@
     public CacheKey(Path path, FileStatus status) throws IOException {
       this.path = path.asFragment();
       this.nodeId = status.getNodeId();
+      this.changeTime = status.getLastChangeTime();
       this.modifiedTime = status.getLastModifiedTime();
       this.size = status.getSize();
     }
@@ -76,6 +80,7 @@
         CacheKey key = (CacheKey) object;
         return path.equals(key.path)
             && nodeId == key.nodeId
+            && changeTime == key.changeTime
             && modifiedTime == key.modifiedTime
             && size == key.size;
       }
@@ -86,6 +91,7 @@
       int result = 17;
       result = 31 * result + path.hashCode();
       result = 31 * result + Longs.hashCode(nodeId);
+      result = 31 * result + Longs.hashCode(changeTime);
       result = 31 * result + Longs.hashCode(modifiedTime);
       result = 31 * result + Longs.hashCode(size);
       return result;
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/FileStatus.java b/src/main/java/com/google/devtools/build/lib/vfs/FileStatus.java
index f6112be..5827576 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/FileStatus.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/FileStatus.java
@@ -85,4 +85,16 @@
    * ought to cause the node ID of b to change, but appending / modifying b should not.
    */
   long getNodeId() throws IOException;
+
+  /**
+   * Returns the file's permissions in POSIX format (e.g. 0755) if possible without performing
+   * additional IO, otherwise (or if unsupported by the file system) returns -1.
+   *
+   * <p>If accurate group and other permissions aren't available, the returned value should attempt
+   * to mimic a umask of 022 (i.e. read and execute permissions extend to group and other, write
+   * does not).
+   */
+  default int getPermissions() {
+    return -1;
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryContentInfo.java b/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryContentInfo.java
index 5686308..51b9b65 100644
--- a/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryContentInfo.java
+++ b/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryContentInfo.java
@@ -121,6 +121,22 @@
   }
 
   @Override
+  public final int getPermissions() {
+    int permissions = 0;
+    // Emulate the default umask of 022.
+    if (isReadable) {
+      permissions |= 0444;
+    }
+    if (isWritable) {
+      permissions |= 0200;
+    }
+    if (isExecutable) {
+      permissions |= 0111;
+    }
+    return permissions;
+  }
+
+  @Override
   public final InMemoryContentInfo inode() {
     return this;
   }
diff --git a/src/main/java/com/google/devtools/build/lib/windows/WindowsFileOperations.java b/src/main/java/com/google/devtools/build/lib/windows/WindowsFileOperations.java
index 25cba40..7b8e691 100644
--- a/src/main/java/com/google/devtools/build/lib/windows/WindowsFileOperations.java
+++ b/src/main/java/com/google/devtools/build/lib/windows/WindowsFileOperations.java
@@ -15,7 +15,9 @@
 package com.google.devtools.build.lib.windows;
 
 import com.google.devtools.build.lib.jni.JniLoader;
+import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.nio.file.AccessDeniedException;
 
 /** File operations on Windows. */
 public class WindowsFileOperations {
@@ -82,6 +84,12 @@
   // IS_SYMLINK_OR_JUNCTION_ERROR = 1;
   private static final int IS_SYMLINK_OR_JUNCTION_DOES_NOT_EXIST = 2;
 
+  // Keep GET_CHANGE_TIME_* values in sync with src/main/native/windows/file.cc.
+  private static final int GET_CHANGE_TIME_SUCCESS = 0;
+  //  private static final int GET_CHANGE_TIME_ERROR = 1;
+  private static final int GET_CHANGE_TIME_DOES_NOT_EXIST = 2;
+  private static final int GET_CHANGE_TIME_ACCESS_DENIED = 3;
+
   // Keep CREATE_JUNCTION_* values in sync with src/main/native/windows/file.h.
   private static final int CREATE_JUNCTION_SUCCESS = 0;
   // CREATE_JUNCTION_ERROR = 1;
@@ -114,6 +122,9 @@
   private static native int nativeIsSymlinkOrJunction(
       String path, boolean[] result, String[] error);
 
+  private static native int nativeGetChangeTime(
+      String path, boolean followReparsePoints, long[] result, String[] error);
+
   private static native boolean nativeGetLongPath(String path, String[] result, String[] error);
 
   private static native int nativeCreateJunction(String name, String target, String[] error);
@@ -143,6 +154,25 @@
     throw new IOException(String.format("Cannot tell if '%s' is link: %s", path, error[0]));
   }
 
+  /** Returns the time at which the file was last changed, including metadata changes. */
+  public static long getLastChangeTime(String path, boolean followReparsePoints)
+      throws IOException {
+    long[] result = new long[] {0};
+    String[] error = new String[] {null};
+    switch (nativeGetChangeTime(asLongPath(path), followReparsePoints, result, error)) {
+      case GET_CHANGE_TIME_SUCCESS:
+        return result[0];
+      case GET_CHANGE_TIME_DOES_NOT_EXIST:
+        throw new FileNotFoundException(path);
+      case GET_CHANGE_TIME_ACCESS_DENIED:
+        throw new AccessDeniedException(path);
+      default:
+        // This is GET_CHANGE_TIME_ERROR (1). The JNI code puts a custom message in 'error[0]'.
+        break;
+    }
+    throw new IOException(String.format("Cannot get last change time of '%s': %s", path, error[0]));
+  }
+
   /**
    * Returns the long path associated with the input `path`.
    *
diff --git a/src/main/java/com/google/devtools/build/lib/windows/WindowsFileSystem.java b/src/main/java/com/google/devtools/build/lib/windows/WindowsFileSystem.java
index 30afecc..2c4eda7 100644
--- a/src/main/java/com/google/devtools/build/lib/windows/WindowsFileSystem.java
+++ b/src/main/java/com/google/devtools/build/lib/windows/WindowsFileSystem.java
@@ -156,6 +156,8 @@
     }
 
     final boolean isSymbolicLink = !followSymlinks && fileIsSymbolicLink(file);
+    final long lastChangeTime =
+        WindowsFileOperations.getLastChangeTime(getNioPath(path).toString(), followSymlinks);
     FileStatus status =
         new FileStatus() {
           @Override
@@ -193,8 +195,7 @@
 
           @Override
           public long getLastChangeTime() {
-            // This is the best we can do with Java NIO...
-            return attributes.lastModifiedTime().toMillis();
+            return lastChangeTime;
           }
 
           @Override
@@ -202,6 +203,12 @@
             // TODO(bazel-team): Consider making use of attributes.fileKey().
             return -1;
           }
+
+          @Override
+          public int getPermissions() {
+            // Files on Windows are implicitly readable and executable.
+            return 0555 | (attributes.isReadOnly() ? 0 : 0200);
+          }
         };
 
     return status;
diff --git a/src/main/native/windows/file-jni.cc b/src/main/native/windows/file-jni.cc
index d920b08..4ed2470 100644
--- a/src/main/native/windows/file-jni.cc
+++ b/src/main/native/windows/file-jni.cc
@@ -62,6 +62,29 @@
   return static_cast<jint>(result);
 }
 
+extern "C" JNIEXPORT jint JNICALL
+Java_com_google_devtools_build_lib_windows_WindowsFileOperations_nativeGetChangeTime(
+    JNIEnv* env, jclass clazz, jstring path, jboolean follow_reparse_points,
+    jlongArray result_holder, jobjectArray error_msg_holder) {
+  std::wstring wpath(bazel::windows::GetJavaWstring(env, path));
+  std::wstring error;
+  jlong ctime = 0;
+  int result =
+      bazel::windows::GetChangeTime(wpath.c_str(), follow_reparse_points,
+                                    reinterpret_cast<int64_t*>(&ctime), &error);
+  if (result == bazel::windows::GetChangeTimeResult::kSuccess) {
+    env->SetLongArrayRegion(result_holder, 0, 1, &ctime);
+  } else {
+    if (!error.empty() && CanReportError(env, error_msg_holder)) {
+      ReportLastError(
+          bazel::windows::MakeErrorMessage(
+              WSTR(__FILE__), __LINE__, L"nativeGetChangeTime", wpath, error),
+          env, error_msg_holder);
+    }
+  }
+  return static_cast<jint>(result);
+}
+
 extern "C" JNIEXPORT jboolean JNICALL
 Java_com_google_devtools_build_lib_windows_WindowsFileOperations_nativeGetLongPath(
     JNIEnv* env, jclass clazz, jstring path, jobjectArray result_holder,
diff --git a/src/main/native/windows/file.cc b/src/main/native/windows/file.cc
index 72d3e4a..f20831e 100644
--- a/src/main/native/windows/file.cc
+++ b/src/main/native/windows/file.cc
@@ -21,6 +21,7 @@
 #include <WinIoCtl.h>
 #include <stdint.h>  // uint8_t
 #include <versionhelpers.h>
+#include <winbase.h>
 #include <windows.h>
 
 #include <memory>
@@ -119,6 +120,54 @@
   }
 }
 
+int GetChangeTime(const WCHAR* path, bool follow_reparse_points,
+                  int64_t* result, wstring* error) {
+  if (!IsAbsoluteNormalizedWindowsPath(path)) {
+    if (error) {
+      *error = MakeErrorMessage(WSTR(__FILE__), __LINE__, L"GetChangeTime",
+                                path, L"expected an absolute Windows path");
+    }
+    return GetChangeTimeResult::kError;
+  }
+
+  AutoHandle handle;
+  DWORD flags = FILE_FLAG_BACKUP_SEMANTICS;
+  if (!follow_reparse_points) {
+    flags |= FILE_FLAG_OPEN_REPARSE_POINT;
+  }
+  handle = CreateFileW(path, 0,
+                       FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
+                       nullptr, OPEN_EXISTING, flags, nullptr);
+
+  if (!handle.IsValid()) {
+    DWORD err = GetLastError();
+    if (err == ERROR_FILE_NOT_FOUND || err == ERROR_PATH_NOT_FOUND) {
+      return GetChangeTimeResult::kDoesNotExist;
+    } else if (err == ERROR_ACCESS_DENIED) {
+      return GetChangeTimeResult::kAccessDenied;
+    }
+    if (error) {
+      *error =
+          MakeErrorMessage(WSTR(__FILE__), __LINE__, L"CreateFileW", path, err);
+    }
+    return GetChangeTimeResult::kError;
+  }
+
+  FILE_BASIC_INFO info;
+  if (!GetFileInformationByHandleEx(handle, FileBasicInfo, (LPVOID)&info,
+                                    sizeof(FILE_BASIC_INFO))) {
+    DWORD err = GetLastError();
+    if (error) {
+      *error = MakeErrorMessage(WSTR(__FILE__), __LINE__,
+                                L"GetFileInformationByHandleEx", path, err);
+    }
+    return GetChangeTimeResult::kError;
+  }
+
+  *result = info.ChangeTime.QuadPart;
+  return GetChangeTimeResult::kSuccess;
+}
+
 wstring GetLongPath(const WCHAR* path, unique_ptr<WCHAR[]>* result) {
   if (!IsAbsoluteNormalizedWindowsPath(path)) {
     return MakeErrorMessage(WSTR(__FILE__), __LINE__, L"GetLongPath", path,
diff --git a/src/main/native/windows/file.h b/src/main/native/windows/file.h
index 590137f..2850357 100644
--- a/src/main/native/windows/file.h
+++ b/src/main/native/windows/file.h
@@ -83,6 +83,16 @@
 };
 
 // Keep in sync with j.c.g.devtools.build.lib.windows.WindowsFileOperations
+struct GetChangeTimeResult {
+  enum {
+    kSuccess = 0,
+    kError = 1,
+    kDoesNotExist = 2,
+    kAccessDenied = 3,
+  };
+};
+
+// Keep in sync with j.c.g.devtools.build.lib.windows.WindowsFileOperations
 struct DeletePathResult {
   enum {
     kSuccess = 0,
@@ -136,6 +146,13 @@
 // see http://superuser.com/a/343079. In Bazel we only ever create junctions.
 int IsSymlinkOrJunction(const WCHAR* path, bool* result, wstring* error);
 
+// Retrieves the FILETIME at which `path` was last changed, including metadata.
+//
+// `path` should be an absolute, normalized, Windows-style path, with "\\?\"
+// prefix if it's longer than MAX_PATH.
+int GetChangeTime(const WCHAR* path, bool follow_reparse_points,
+                  int64_t* result, wstring* error);
+
 // Computes the long version of `path` if it has any 8dot3 style components.
 // Returns the empty string upon success, or a human-readable error message upon
 // failure.
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/ActionMetadataHandlerTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/ActionMetadataHandlerTest.java
index ebc04c3..b780e03 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/ActionMetadataHandlerTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/ActionMetadataHandlerTest.java
@@ -305,9 +305,10 @@
 
     // Reset this output, which will make the handler stat the file again.
     handler.resetOutputs(ImmutableList.of(artifact));
-    chmodCalls.clear(); // Permit a second chmod call for the artifact.
+    chmodCalls.clear();
     assertThat(handler.getMetadata(artifact).getSize()).isEqualTo(10);
-    assertThat(chmodCalls).containsExactly(outputPath, 0555);
+    // The handler should not have chmodded the file as it already has the correct permission.
+    assertThat(chmodCalls).isEmpty();
   }
 
   @Test
diff --git a/src/test/shell/integration/execution_phase_tests.sh b/src/test/shell/integration/execution_phase_tests.sh
index 3458210..73ff18e 100755
--- a/src/test/shell/integration/execution_phase_tests.sh
+++ b/src/test/shell/integration/execution_phase_tests.sh
@@ -392,4 +392,102 @@
       >& "$TEST_log" || fail "Expected success"
   expect_log "WARNING: Directory artifact foo/dir crosses package boundary into"
 }
+
+# Regression test for https://github.com/bazelbuild/bazel/issues/14723
+function test_fixed_mtime_move_detected_as_change() {
+  mkdir -p pkg
+  cat > pkg/BUILD <<'EOF'
+load("rules.bzl", "my_expand")
+
+genrule(
+    name = "my_templates",
+    srcs = ["template_archive.tar"],
+    outs = ["template1"],
+    cmd = "tar -C $(RULEDIR) -xf $<",
+)
+
+my_expand(
+    name = "expand1",
+    input = "template1",
+    output = "expanded1",
+    to_sub = {"test":"foo"}
+)
+EOF
+  cat > pkg/rules.bzl <<'EOF'
+def _my_expand_impl(ctx):
+    ctx.actions.expand_template(
+        template = ctx.file.input,
+        output = ctx.outputs.output,
+        substitutions = ctx.attr.to_sub
+    )
+
+my_expand = rule(
+    implementation = _my_expand_impl,
+    attrs = {
+        "input": attr.label(allow_single_file=True),
+        "output": attr.output(),
+        "to_sub" : attr.string_dict(),
+    }
+)
+EOF
+
+  echo "test : alpha" > template1
+  touch -t 197001010000 template1
+  tar -cf pkg/template_archive_alpha.tar template1
+
+  echo "test : delta" > template1
+  touch -t 197001010000 template1
+  tar -cf pkg/template_archive_delta.tar template1
+
+  mv pkg/template_archive_alpha.tar pkg/template_archive.tar
+  bazel build //pkg:expand1 || fail "Expected success"
+  assert_equals "foo : alpha" "$(cat bazel-bin/pkg/expanded1)"
+
+  mv pkg/template_archive_delta.tar pkg/template_archive.tar
+  bazel build //pkg:expand1 || fail "Expected success"
+  assert_equals "foo : delta" "$(cat bazel-bin/pkg/expanded1)"
+}
+
+# Regression test for https://github.com/bazelbuild/bazel/issues/14723
+function test_fixed_mtime_source_file() {
+  mkdir -p pkg
+  cat > pkg/BUILD <<'EOF'
+load("rules.bzl", "my_expand")
+
+my_expand(
+    name = "expand1",
+    input = "template1",
+    output = "expanded1",
+    to_sub = {"test":"foo"}
+)
+EOF
+  cat > pkg/rules.bzl <<'EOF'
+def _my_expand_impl(ctx):
+    ctx.actions.expand_template(
+        template = ctx.file.input,
+        output = ctx.outputs.output,
+        substitutions = ctx.attr.to_sub
+    )
+
+my_expand = rule(
+    implementation = _my_expand_impl,
+    attrs = {
+        "input": attr.label(allow_single_file=True),
+        "output": attr.output(),
+        "to_sub" : attr.string_dict(),
+    }
+)
+EOF
+
+  echo "test : alpha" > pkg/template1
+  touch -t 197001010000 pkg/template1
+  bazel build //pkg:expand1 || fail "Expected success"
+  assert_equals "foo : alpha" "$(cat bazel-bin/pkg/expanded1)"
+
+  echo "test : delta" > pkg/template1
+  touch -t 197001010000 pkg/template1
+  bazel build //pkg:expand1 || fail "Expected success"
+  assert_equals "foo : delta" "$(cat bazel-bin/pkg/expanded1)"
+}
+
 run_suite "Integration tests of ${PRODUCT_NAME} using the execution phase."
diff --git a/src/test/shell/integration/loading_phase_test.sh b/src/test/shell/integration/loading_phase_test.sh
index 478410c..d997109 100755
--- a/src/test/shell/integration/loading_phase_test.sh
+++ b/src/test/shell/integration/loading_phase_test.sh
@@ -289,24 +289,24 @@
   local -r pkg="${FUNCNAME}"
   mkdir -p "$pkg" || fail "could not create \"$pkg\""
 
-  local other_root=$TEST_TMPDIR/other_root/${WORKSPACE_NAME}
+  local other_root=other_root/${WORKSPACE_NAME}
   mkdir -p $other_root/$pkg/a
   touch $other_root/WORKSPACE
   echo 'sh_library(name="external")' > $other_root/$pkg/a/BUILD
   mkdir -p $pkg/a
   echo 'sh_library(name="internal")' > $pkg/a/BUILD
 
-  bazel query --package_path=$other_root:. $pkg/a:all >& $TEST_log \
+  bazel query --package_path=%workspace%/$other_root:. $pkg/a:all >& $TEST_log \
       || fail "Expected success"
   expect_log "//$pkg/a:external"
   expect_not_log "//$pkg/a:internal"
   rm -r $other_root
-  bazel query --package_path=$other_root:. $pkg/a:all >& $TEST_log \
+  bazel query --package_path=%workspace%/$other_root:. $pkg/a:all >& $TEST_log \
       || fail "Expected success"
   expect_log "//$pkg/a:internal"
   expect_not_log "//$pkg/a:external"
   mkdir -p $other_root
-  bazel query --package_path=$other_root:. $pkg/a:all >& $TEST_log \
+  bazel query --package_path=%workspace%/$other_root:. $pkg/a:all >& $TEST_log \
       || fail "Expected success"
   expect_log "//$pkg/a:internal"
   expect_not_log "//$pkg/a:external"