Windows, build-runfiles: Implement build-runfiles for Windows

Working towards: https://github.com/bazelbuild/bazel/issues/5807

@laszlocsomor

Closes #6024.

Change-Id: I2e1fb56f7686872f5e7d4724c93c1ad2e6e02311
PiperOrigin-RevId: 210905364
diff --git a/src/main/cpp/util/BUILD b/src/main/cpp/util/BUILD
index 9df4e55..f40c00e 100644
--- a/src/main/cpp/util/BUILD
+++ b/src/main/cpp/util/BUILD
@@ -54,6 +54,7 @@
     ],
     visibility = [
         ":ijar",
+        "//src/main/tools:__pkg__",
         "//src/test/cpp/util:__pkg__",
         "//src/tools/launcher:__subpackages__",
         "//src/tools/singlejar:__pkg__",
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java b/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java
index 7590d6a..2b58187 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/BuildConfiguration.java
@@ -1167,10 +1167,6 @@
       fragment.reportInvalidOptions(reporter, this.buildOptions);
     }
 
-    if (OS.getCurrent() == OS.WINDOWS && runfilesEnabled()) {
-      reporter.handle(Event.error("building runfiles is not supported on Windows"));
-    }
-
     if (options.outputDirectoryName != null) {
       reporter.handle(Event.error(
           "The internal '--output directory name' option cannot be used on the command line"));
diff --git a/src/main/tools/BUILD b/src/main/tools/BUILD
index e67d67b..8362c0c 100644
--- a/src/main/tools/BUILD
+++ b/src/main/tools/BUILD
@@ -48,6 +48,7 @@
         "//src/conditions:windows": ["build-runfiles-windows.cc"],
         "//conditions:default": ["build-runfiles.cc"],
     }),
+    deps = ["//src/main/cpp/util:filesystem"],
 )
 
 cc_binary(
diff --git a/src/main/tools/build-runfiles-windows.cc b/src/main/tools/build-runfiles-windows.cc
index ea4eed1..36091ff 100644
--- a/src/main/tools/build-runfiles-windows.cc
+++ b/src/main/tools/build-runfiles-windows.cc
@@ -12,16 +12,400 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include <string.h>
+#include <windows.h>
+#include <fstream>
 #include <iostream>
+#include <sstream>
+#include <string>
+#include <unordered_map>
 
-int main(int argc, char** argv) {
-  // TODO(bazel-team): decide whether we need build-runfiles at all on Windows.
-  // Implement this program if so; make sure we don't run it on Windows if not.
-  std::cout << "ERROR: build-runfiles is not (yet?) implemented on Windows."
-            << std::endl
-            << "Called with args:" << std::endl;
-  for (int i = 0; i < argc; ++i) {
-    std::cout << "argv[" << i << "]=(" << argv[i] << ")" << std::endl;
+#include "src/main/cpp/util/file_platform.h"
+#include "src/main/cpp/util/path_platform.h"
+#include "src/main/cpp/util/strings.h"
+
+using std::ifstream;
+using std::string;
+using std::stringstream;
+using std::unordered_map;
+using std::wstring;
+
+#ifndef SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE
+#define SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE 0x2
+#endif
+
+#ifndef SYMBOLIC_LINK_FLAG_DIRECTORY
+#define SYMBOLIC_LINK_FLAG_DIRECTORY 0x1
+#endif
+
+namespace {
+
+const wchar_t* manifest_filename;
+const wchar_t* runfiles_base_dir;
+
+string GetLastErrorString() {
+  DWORD last_error = GetLastError();
+  if (last_error == 0) {
+    return string();
   }
-  return 1;
+
+  char* message_buffer;
+  size_t size = FormatMessageA(
+      FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
+          FORMAT_MESSAGE_IGNORE_INSERTS,
+      NULL, last_error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
+      (LPSTR)&message_buffer, 0, NULL);
+
+  stringstream result;
+  result << "(error: " << last_error << "): " << message_buffer;
+  LocalFree(message_buffer);
+  return result.str();
+}
+
+void die(const wchar_t* format, ...) {
+  va_list ap;
+  va_start(ap, format);
+  fputws(L"build-runfiles error: ", stderr);
+  vfwprintf(stderr, format, ap);
+  va_end(ap);
+  fputwc(L'\n', stderr);
+  fputws(L"manifest file name: ", stderr);
+  fputws(manifest_filename, stderr);
+  fputwc(L'\n', stderr);
+  fputws(L"runfiles base directory: ", stderr);
+  fputws(runfiles_base_dir, stderr);
+  fputwc(L'\n', stderr);
+  exit(1);
+}
+
+wstring AsAbsoluteWindowsPath(const wchar_t* path) {
+  wstring wpath;
+  string error;
+  if (!blaze_util::AsAbsoluteWindowsPath(path, &wpath, &error)) {
+    die(L"Couldn't convert %s to absolute Windows path: %hs", path,
+        error.c_str());
+  }
+  return wpath;
+}
+
+bool DoesDirectoryPathExist(const wchar_t* path) {
+  DWORD dwAttrib = GetFileAttributesW(AsAbsoluteWindowsPath(path).c_str());
+
+  return (dwAttrib != INVALID_FILE_ATTRIBUTES &&
+          (dwAttrib & FILE_ATTRIBUTE_DIRECTORY));
+}
+
+wstring GetParentDirFromPath(const wstring& path) {
+  return path.substr(0, path.find_last_of(L"\\/"));
+}
+
+inline void Trim(wstring& str) {
+  str.erase(0, str.find_first_not_of(' '));
+  str.erase(str.find_last_not_of(' ') + 1);
+}
+
+}  // namespace
+
+class RunfilesCreator {
+  typedef std::unordered_map<std::wstring, std::wstring> ManifestFileMap;
+
+ public:
+  RunfilesCreator(const wstring& manifest_path,
+                  const wstring& runfiles_output_base)
+      : manifest_path_(manifest_path),
+        runfiles_output_base_(runfiles_output_base) {
+    SetupOutputBase();
+    if (!SetCurrentDirectoryW(runfiles_output_base_.c_str())) {
+      die(L"SetCurrentDirectoryW failed (%s): %hs",
+          runfiles_output_base_.c_str(), GetLastErrorString().c_str());
+    }
+  }
+
+  void ReadManifest(bool allow_relative, bool ignore_metadata) {
+    ifstream manifest_file(
+        AsAbsoluteWindowsPath(manifest_path_.c_str()).c_str());
+
+    if (!manifest_file) {
+      die(L"Couldn't open MANIFEST file: %s", manifest_path_.c_str());
+    }
+
+    string line;
+    int lineno = 0;
+    while (getline(manifest_file, line)) {
+      lineno++;
+      // Skip metadata lines. They are used solely for
+      // dependency checking.
+      if (ignore_metadata && lineno % 2 == 0) {
+        continue;
+      }
+
+      size_t space_pos = line.find_first_of(' ');
+      wstring wline = blaze_util::CstringToWstring(line);
+      wstring link, target;
+      if (space_pos == string::npos) {
+        link = wline;
+        target = wstring();
+      } else {
+        link = wline.substr(0, space_pos);
+        target = wline.substr(space_pos + 1);
+      }
+
+      // Removing leading and trailing spaces
+      Trim(link);
+      Trim(target);
+
+      // We sometimes need to create empty files under the runfiles tree.
+      // For example, for python binary, __init__.py is needed under every
+      // directory. Storing an entry with an empty target indicates we need to
+      // create such a file when creating the runfiles tree.
+      if (!allow_relative && !target.empty() &&
+          !blaze_util::IsAbsolute(target)) {
+        die(L"Target cannot be relative path: %hs", line.c_str());
+      }
+
+      link = AsAbsoluteWindowsPath(link.c_str());
+
+      manifest_file_map.insert(make_pair(link, target));
+    }
+  }
+
+  void CreateRunfiles() {
+    bool symlink_needs_privilege =
+        DoesCreatingSymlinkNeedAdminPrivilege(runfiles_output_base_);
+    ScanTreeAndPrune(runfiles_output_base_);
+    CreateFiles(symlink_needs_privilege);
+    CopyManifestFile();
+  }
+
+ private:
+  void SetupOutputBase() {
+    if (!DoesDirectoryPathExist(runfiles_output_base_.c_str())) {
+      MakeDirectoriesOrDie(runfiles_output_base_);
+    }
+  }
+
+  void MakeDirectoriesOrDie(const wstring& path) {
+    if (!blaze_util::MakeDirectoriesW(path, 0755)) {
+      die(L"MakeDirectoriesW failed (%s): %hs", path.c_str(),
+          GetLastErrorString().c_str());
+    }
+  }
+
+  void RemoveDirectoryOrDie(const wstring& path) {
+    if (!RemoveDirectoryW(path.c_str())) {
+      die(L"RemoveDirectoryW failed (%s): %hs", GetLastErrorString().c_str());
+    }
+  }
+
+  void DeleteFileOrDie(const wstring& path) {
+    SetFileAttributesW(path.c_str(), GetFileAttributesW(path.c_str()) &
+                                         ~FILE_ATTRIBUTE_READONLY);
+    if (!DeleteFileW(path.c_str())) {
+      die(L"DeleteFileW failed (%s): %hs", path.c_str(),
+          GetLastErrorString().c_str());
+    }
+  }
+
+  bool DoesCreatingSymlinkNeedAdminPrivilege(const wstring& runfiles_base_dir) {
+    wstring dummy_link = runfiles_base_dir + L"\\dummy_link";
+    wstring dummy_target = runfiles_base_dir + L"\\dummy_target";
+
+    // Try creating symlink without admin privilege.
+    if (CreateSymbolicLinkW(dummy_link.c_str(), dummy_target.c_str(),
+                            SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE)) {
+      DeleteFileOrDie(dummy_link);
+      return false;
+    }
+
+    // Try creating symlink with admin privilege
+    if (CreateSymbolicLinkW(dummy_link.c_str(), dummy_target.c_str(), 0)) {
+      DeleteFileOrDie(dummy_link);
+      return true;
+    }
+
+    // If we couldn't create symlink, print out an error message and exit.
+    if (GetLastError() == ERROR_PRIVILEGE_NOT_HELD) {
+      die(L"CreateSymbolicLinkW failed:\n%hs\n",
+          "Bazel needs to create symlink for building runfiles tree.\n"
+          "Creating symlink on Windows requires either of the following:\n"
+          "    1. Program is running with elevated privileges (Admin rights).\n"
+          "    2. The system version is Windows 10 Creators Update (1703) or "
+          "later and "
+          "developer mode is enabled.",
+          GetLastErrorString().c_str());
+    } else {
+      die(L"CreateSymbolicLinkW failed: %hs", GetLastErrorString().c_str());
+    }
+
+    return true;
+  }
+
+  // This function scan the current directory, remove all
+  // files/symlinks/directories that are not presented in manifest file. If a
+  // symlink already exists and points to the correct target, this function
+  // erases its entry from manifest_file_map, so that we won't recreate it.
+  void ScanTreeAndPrune(const wstring& path) {
+    static const wstring kDot(L".");
+    static const wstring kDotDot(L"..");
+
+    WIN32_FIND_DATAW metadata;
+    HANDLE handle = ::FindFirstFileW((path + L"\\*").c_str(), &metadata);
+    if (handle == INVALID_HANDLE_VALUE) {
+      return;  // directory does not exist or is empty
+    }
+
+    do {
+      if (kDot != metadata.cFileName && kDotDot != metadata.cFileName) {
+        wstring subpath = path + L"\\" + metadata.cFileName;
+        bool is_dir =
+            (metadata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
+        bool is_symlink =
+            (metadata.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0;
+        if (is_symlink) {
+          wstring target;
+          if (!blaze_util::ReadSymlinkW(subpath, &target)) {
+            die(L"ReadSymlinkW failed (%s): %hs", subpath.c_str(),
+                GetLastErrorString().c_str());
+          }
+
+          subpath = AsAbsoluteWindowsPath(subpath.c_str());
+          target = AsAbsoluteWindowsPath(target.c_str());
+          ManifestFileMap::iterator expected_target =
+              manifest_file_map.find(subpath);
+
+          if (expected_target == manifest_file_map.end() ||
+              expected_target->second.empty()
+              // Both paths are normalized paths in lower case, we can compare
+              // them directly.
+              || target !=
+                     AsAbsoluteWindowsPath(expected_target->second.c_str()) ||
+              blaze_util::IsDirectoryW(target) != is_dir) {
+            if (is_dir) {
+              RemoveDirectoryOrDie(subpath);
+            } else {
+              DeleteFileOrDie(subpath);
+            }
+          } else {
+            manifest_file_map.erase(expected_target);
+          }
+        } else {
+          if (is_dir) {
+            ScanTreeAndPrune(subpath);
+            // If the directory is empty, then we remove the directory.
+            // Otherwise RemoveDirectory will fail with ERROR_DIR_NOT_EMPTY,
+            // which we can just ignore.
+            // Because if the directory is not empty, it means it contains some
+            // symlinks already pointing to the correct targets (we just called
+            // ScanTreeAndPrune). Then this directory shouldn't be removed in
+            // the first place.
+            if (!RemoveDirectoryW(subpath.c_str()) &&
+                GetLastError() != ERROR_DIR_NOT_EMPTY) {
+              die(L"RemoveDirectoryW failed (%s): %hs", subpath.c_str(),
+                  GetLastErrorString().c_str());
+            }
+          } else {
+            DeleteFileOrDie(subpath);
+          }
+        }
+      }
+    } while (::FindNextFileW(handle, &metadata));
+    ::FindClose(handle);
+  }
+
+  void CreateFiles(bool creating_symlink_needs_admin_privilege) {
+    DWORD privilege_flag = creating_symlink_needs_admin_privilege
+                               ? 0
+                               : SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE;
+
+    for (const auto& it : manifest_file_map) {
+      // Ensure the parent directory exists
+      wstring parent_dir = GetParentDirFromPath(it.first);
+      if (!DoesDirectoryPathExist(parent_dir.c_str())) {
+        MakeDirectoriesOrDie(parent_dir);
+      }
+
+      if (it.second.empty()) {
+        // Create an empty file
+        HANDLE h = CreateFileW(it.first.c_str(),  // name of the file
+                               GENERIC_WRITE,     // open for writing
+                               0,  // sharing mode, none in this case
+                               0,  // use default security descriptor
+                               CREATE_ALWAYS,  // overwrite if exists
+                               FILE_ATTRIBUTE_NORMAL, 0);
+        if (h != INVALID_HANDLE_VALUE) {
+          CloseHandle(h);
+        } else {
+          die(L"CreateFileW failed (%s): %hs", it.first.c_str(),
+              GetLastErrorString().c_str());
+        }
+      } else {
+        DWORD create_dir = 0;
+        if (blaze_util::IsDirectoryW(it.second.c_str())) {
+          create_dir = SYMBOLIC_LINK_FLAG_DIRECTORY;
+        }
+        if (!CreateSymbolicLinkW(it.first.c_str(), it.second.c_str(),
+                                 privilege_flag | create_dir)) {
+          die(L"CreateSymbolicLinkW failed (%s -> %s): %hs", it.first.c_str(),
+              it.second.c_str(), GetLastErrorString().c_str());
+        }
+      }
+    }
+  }
+
+  void CopyManifestFile() {
+    wstring new_manifest_file = runfiles_output_base_ + L"\\MANIFEST";
+    if (!CopyFileW(manifest_path_.c_str(), new_manifest_file.c_str(),
+                   /*bFailIfExists=*/FALSE)) {
+      die(L"CopyFileW failed (%s -> %s): %hs", manifest_path_.c_str(),
+          new_manifest_file.c_str(), GetLastErrorString().c_str());
+    }
+  }
+
+ private:
+  wstring manifest_path_;
+  wstring runfiles_output_base_;
+  ManifestFileMap manifest_file_map;
+};
+
+int wmain(int argc, wchar_t** argv) {
+  argc--;
+  argv++;
+  bool allow_relative = false;
+  bool ignore_metadata = false;
+
+  while (argc >= 1) {
+    if (wcscmp(argv[0], L"--allow_relative") == 0) {
+      allow_relative = true;
+      argc--;
+      argv++;
+    } else if (wcscmp(argv[0], L"--use_metadata") == 0) {
+      // If --use_metadata is passed, it means manifest file contains metadata
+      // lines, which we should ignore when reading manifest file.
+      ignore_metadata = true;
+      argc--;
+      argv++;
+    } else {
+      break;
+    }
+  }
+
+  if (argc != 2) {
+    fprintf(stderr,
+            "usage: [--allow_relative] [--use_metadata] "
+            "<manifest_file> <runfiles_base_dir>\n");
+    return 1;
+  }
+
+  manifest_filename = argv[0];
+  runfiles_base_dir = argv[1];
+
+  wstring manifest_absolute_path = AsAbsoluteWindowsPath(manifest_filename);
+  wstring output_base_absolute_path = AsAbsoluteWindowsPath(runfiles_base_dir);
+
+  RunfilesCreator runfiles_creator(manifest_absolute_path,
+                                   output_base_absolute_path);
+  runfiles_creator.ReadManifest(allow_relative, ignore_metadata);
+  runfiles_creator.CreateRunfiles();
+
+  return 0;
 }
diff --git a/src/test/py/bazel/runfiles_test.py b/src/test/py/bazel/runfiles_test.py
index 50506f6..36aba0d 100644
--- a/src/test/py/bazel/runfiles_test.py
+++ b/src/test/py/bazel/runfiles_test.py
@@ -21,16 +21,6 @@
 
 class RunfilesTest(test_base.TestBase):
 
-  def testAttemptToBuildRunfilesOnWindows(self):
-    if not self.IsWindows():
-      self.skipTest("only applicable to Windows")
-    self.ScratchFile("WORKSPACE")
-    exit_code, _, stderr = self.RunBazel(
-        ["build", "--experimental_enable_runfiles"])
-    self.assertNotEqual(exit_code, 0)
-    self.assertIn("building runfiles is not supported on Windows",
-                  "\n".join(stderr))
-
   def _AssertRunfilesLibraryInBazelToolsRepo(self, family, lang_name):
     for s, t, exe in [("WORKSPACE.mock", "WORKSPACE",
                        False), ("foo/BUILD.mock", "foo/BUILD",
diff --git a/src/test/shell/integration/BUILD b/src/test/shell/integration/BUILD
index 5501058..efdde9e 100644
--- a/src/test/shell/integration/BUILD
+++ b/src/test/shell/integration/BUILD
@@ -29,11 +29,12 @@
     name = "runfiles_test",
     size = "medium",
     srcs = ["runfiles_test.sh"],
-    data = [":test-deps"],
+    data = [
+        ":test-deps",
+        "@bazel_tools//tools/bash/runfiles",
+    ],
     tags = [
         "local",
-        # no_windows: this test exercises the symlink-based runfiles tree.
-        "no_windows",
     ],
 )
 
diff --git a/src/test/shell/integration/runfiles_test.sh b/src/test/shell/integration/runfiles_test.sh
index cb2e886..fd85be9 100755
--- a/src/test/shell/integration/runfiles_test.sh
+++ b/src/test/shell/integration/runfiles_test.sh
@@ -16,18 +16,59 @@
 #
 # An end-to-end test that Bazel produces runfiles trees as expected.
 
-# Load the test setup defined in the parent directory
-CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-source "${CURRENT_DIR}/../integration_test_setup.sh" \
+# --- begin runfiles.bash initialization ---
+# Copy-pasted from Bazel's Bash runfiles library (tools/bash/runfiles/runfiles.bash).
+set -euo pipefail
+if [[ ! -d "${RUNFILES_DIR:-/dev/null}" && ! -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]]; then
+  if [[ -f "$0.runfiles_manifest" ]]; then
+    export RUNFILES_MANIFEST_FILE="$0.runfiles_manifest"
+  elif [[ -f "$0.runfiles/MANIFEST" ]]; then
+    export RUNFILES_MANIFEST_FILE="$0.runfiles/MANIFEST"
+  elif [[ -f "$0.runfiles/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then
+    export RUNFILES_DIR="$0.runfiles"
+  fi
+fi
+if [[ -f "${RUNFILES_DIR:-/dev/null}/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then
+  source "${RUNFILES_DIR}/bazel_tools/tools/bash/runfiles/runfiles.bash"
+elif [[ -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]]; then
+  source "$(grep -m1 "^bazel_tools/tools/bash/runfiles/runfiles.bash " \
+            "$RUNFILES_MANIFEST_FILE" | cut -d ' ' -f 2-)"
+else
+  echo >&2 "ERROR: cannot find @bazel_tools//tools/bash/runfiles:runfiles.bash"
+  exit 1
+fi
+# --- end runfiles.bash initialization ---
+
+source "$(rlocation "io_bazel/src/test/shell/integration_test_setup.sh")" \
   || { echo "integration_test_setup.sh not found!" >&2; exit 1; }
 
+case "$(uname -s | tr [:upper:] [:lower:])" in
+msys*|mingw*|cygwin*)
+  declare -r is_windows=true
+  ;;
+*)
+  declare -r is_windows=false
+  ;;
+esac
+
+if "$is_windows"; then
+  export MSYS_NO_PATHCONV=1
+  export MSYS2_ARG_CONV_EXCL="*"
+  export EXT=".exe"
+  export EXTRA_BUILD_FLAGS="--experimental_enable_runfiles --build_python_zip=0"
+else
+  export EXT=""
+  export EXTRA_BUILD_FLAGS=""
+fi
+
 #### SETUP #############################################################
 
 set -e
 
-function set_up() {
-  mkdir -p pkg
-  cd pkg
+function create_pkg() {
+  local -r pkg=$1
+  mkdir -p $pkg
+  cd $pkg
 
   mkdir -p a/b c/d e/f/g x/y
   touch py.py a/b/no_module.py c/d/one_module.py c/__init__.py e/f/g/ignored.py x/y/z.sh
@@ -40,7 +81,9 @@
 #### TESTS #############################################################
 
 function test_hidden() {
-  cat > pkg/BUILD << EOF
+  local -r pkg=$FUNCNAME
+  create_pkg $pkg
+  cat > $pkg/BUILD << EOF
 py_binary(name = "py",
           srcs = [ "py.py" ],
           data = [ "e/f",
@@ -49,19 +92,21 @@
         outs = [ "e/f/g/hidden.py" ],
         cmd = "touch \$@")
 EOF
-  bazel build pkg:py >&$TEST_log 2>&1 || fail "build failed"
+  bazel build $pkg:py $EXTRA_BUILD_FLAGS >&$TEST_log 2>&1 || fail "build failed"
 
   # we get a warning that hidden.py is inaccessible
-  expect_log_once "pkg/e/f/g/hidden.py obscured by pkg/e/f "
+  expect_log_once "${pkg}/e/f/g/hidden.py obscured by ${pkg}/e/f "
 }
 
 function test_foo_runfiles() {
+  local -r pkg=$FUNCNAME
+  create_pkg $pkg
 cat > BUILD << EOF
 py_library(name = "root",
            srcs = ["__init__.py"],
            visibility = ["//visibility:public"])
 EOF
-cat > pkg/BUILD << EOF
+cat > $pkg/BUILD << EOF
 sh_binary(name = "foo",
           srcs = [ "x/y/z.sh" ],
           data = [ ":py",
@@ -74,9 +119,10 @@
                    "e/f/g/ignored.py" ],
           deps = ["//:root"])
 EOF
-  bazel build pkg:foo >&$TEST_log || fail "build failed"
+  bazel build $pkg:foo $EXTRA_BUILD_FLAGS >&$TEST_log || fail "build failed"
+  workspace_root=$PWD
 
-  cd ${PRODUCT_NAME}-bin/pkg/foo.runfiles
+  cd ${PRODUCT_NAME}-bin/$pkg/foo${EXT}.runfiles
 
   # workaround until we use assert/fail macros in the tests below
   touch $TEST_TMPDIR/__fail
@@ -88,10 +134,10 @@
   cd ${WORKSPACE_NAME}
 
   # these are real directories
-  test \! -L pkg
-  test    -d pkg
+  test \! -L $pkg
+  test    -d $pkg
 
-  cd pkg
+  cd $pkg
   test \! -L a
   test    -d a
   test \! -L a/b
@@ -133,17 +179,110 @@
 
   # that accounts for everything
   cd ../..
-  assert_equals  9 $(find ${WORKSPACE_NAME} -type l | wc -l)
-  assert_equals  4 $(find ${WORKSPACE_NAME} -type f | wc -l)
-  assert_equals  9 $(find ${WORKSPACE_NAME} -type d | wc -l)
-  assert_equals 22 $(find ${WORKSPACE_NAME} | wc -l)
-  assert_equals 13 $(wc -l < MANIFEST)
+  # For shell binary, we build both `bin` and `bin.exe`, but on Linux we only build `bin`
+  # That's why we have one more symlink on Windows.
+  if "$is_windows"; then
+    assert_equals 10 $(find ${WORKSPACE_NAME} -type l | wc -l)
+    assert_equals  4 $(find ${WORKSPACE_NAME} -type f | wc -l)
+    assert_equals  9 $(find ${WORKSPACE_NAME} -type d | wc -l)
+    assert_equals 23 $(find ${WORKSPACE_NAME} | wc -l)
+    assert_equals 14 $(wc -l < MANIFEST)
+  else
+    assert_equals  9 $(find ${WORKSPACE_NAME} -type l | wc -l)
+    assert_equals  4 $(find ${WORKSPACE_NAME} -type f | wc -l)
+    assert_equals  9 $(find ${WORKSPACE_NAME} -type d | wc -l)
+    assert_equals 22 $(find ${WORKSPACE_NAME} | wc -l)
+    assert_equals 13 $(wc -l < MANIFEST)
+  fi
 
   for i in $(find ${WORKSPACE_NAME} \! -type d); do
-    if readlink "$i" > /dev/null; then
-      echo "$i $(readlink "$i")" >> ${TEST_TMPDIR}/MANIFEST2
-    else
+    target="$(readlink "$i" || true)"
+    if [[ -z "$target" ]]; then
       echo "$i " >> ${TEST_TMPDIR}/MANIFEST2
+    else
+      if "$is_windows"; then
+        echo "$i $(cygpath -m $target)" >> ${TEST_TMPDIR}/MANIFEST2
+      else
+        echo "$i $target" >> ${TEST_TMPDIR}/MANIFEST2
+      fi
+    fi
+  done
+  sort MANIFEST > ${TEST_TMPDIR}/MANIFEST_sorted
+  sort ${TEST_TMPDIR}/MANIFEST2 > ${TEST_TMPDIR}/MANIFEST2_sorted
+  diff -u ${TEST_TMPDIR}/MANIFEST_sorted ${TEST_TMPDIR}/MANIFEST2_sorted
+
+  # Rebuild the same target with a new dependency.
+  cd "$workspace_root"
+cat > $pkg/BUILD << EOF
+sh_binary(name = "foo",
+          srcs = [ "x/y/z.sh" ],
+          data = [ "e/f" ])
+EOF
+  bazel build $pkg:foo $EXTRA_BUILD_FLAGS >&$TEST_log || fail "build failed"
+
+  cd ${PRODUCT_NAME}-bin/$pkg/foo${EXT}.runfiles
+
+  # workaround until we use assert/fail macros in the tests below
+  touch $TEST_TMPDIR/__fail
+
+  # output manifest exists and is non-empty
+  test    -f MANIFEST
+  test    -s MANIFEST
+
+  cd ${WORKSPACE_NAME}
+
+  # these are real directories
+  test \! -L $pkg
+  test    -d $pkg
+
+  # these directory should not exist anymore
+  test \! -e a
+  test \! -e c
+
+  cd $pkg
+  test \! -L e
+  test    -d e
+  test \! -L x
+  test    -d x
+  test \! -L x/y
+  test    -d x/y
+
+  # these are symlinks to the source tree
+  test    -L foo
+  test    -L x/y/z.sh
+  test    -L e/f
+  test    -d e/f
+
+  # that accounts for everything
+  cd ../..
+  # For shell binary, we build both `bin` and `bin.exe`, but on Linux we only build `bin`
+  # That's why we have one more symlink on Windows.
+  if "$is_windows"; then
+    assert_equals  4 $(find ${WORKSPACE_NAME} -type l | wc -l)
+    assert_equals  0 $(find ${WORKSPACE_NAME} -type f | wc -l)
+    assert_equals  5 $(find ${WORKSPACE_NAME} -type d | wc -l)
+    assert_equals  9 $(find ${WORKSPACE_NAME} | wc -l)
+    assert_equals  4 $(wc -l < MANIFEST)
+  else
+    assert_equals  3 $(find ${WORKSPACE_NAME} -type l | wc -l)
+    assert_equals  0 $(find ${WORKSPACE_NAME} -type f | wc -l)
+    assert_equals  5 $(find ${WORKSPACE_NAME} -type d | wc -l)
+    assert_equals  8 $(find ${WORKSPACE_NAME} | wc -l)
+    assert_equals  3 $(wc -l < MANIFEST)
+  fi
+
+  rm -f ${TEST_TMPDIR}/MANIFEST
+  rm -f ${TEST_TMPDIR}/MANIFEST2
+  for i in $(find ${WORKSPACE_NAME} \! -type d); do
+    target="$(readlink "$i" || true)"
+    if [[ -z "$target" ]]; then
+      echo "$i " >> ${TEST_TMPDIR}/MANIFEST2
+    else
+      if "$is_windows"; then
+        echo "$i $(cygpath -m $target)" >> ${TEST_TMPDIR}/MANIFEST2
+      else
+        echo "$i $target" >> ${TEST_TMPDIR}/MANIFEST2
+      fi
     fi
   done
   sort MANIFEST > ${TEST_TMPDIR}/MANIFEST_sorted
@@ -166,15 +305,15 @@
   cat > thing.cc <<EOF
 int main() { return 0; }
 EOF
-  bazel build //:thing &> $TEST_log || fail "Build failed"
-  [[ -d ${PRODUCT_NAME}-bin/thing.runfiles/foo ]] || fail "foo not found"
+  bazel build //:thing $EXTRA_BUILD_FLAGS &> $TEST_log || fail "Build failed"
+  [[ -d ${PRODUCT_NAME}-bin/thing${EXT}.runfiles/foo ]] || fail "foo not found"
 
   cat > WORKSPACE <<EOF
 workspace(name = "bar")
 EOF
-  bazel build //:thing &> $TEST_log || fail "Build failed"
-  [[ -d ${PRODUCT_NAME}-bin/thing.runfiles/bar ]] || fail "bar not found"
-  [[ ! -d ${PRODUCT_NAME}-bin/thing.runfiles/foo ]] \
+  bazel build //:thing $EXTRA_BUILD_FLAGS &> $TEST_log || fail "Build failed"
+  [[ -d ${PRODUCT_NAME}-bin/thing${EXT}.runfiles/bar ]] || fail "bar not found"
+  [[ ! -d ${PRODUCT_NAME}-bin/thing${EXT}.runfiles/foo ]] \
     || fail "Old foo still found"
 }