Client: convert output_base to Path

(Roll-forward of commit 242acd6c07ccfe380bddf8d5a719c1cdfd6fdfb9 that was rolled back
by commit 505ca7e3036b328908dd57702278d1cf4641aba5 because it broke Google-internal
build targets. Those are now fixed.)

Benefits of a Path abstraction instead of raw
strings:
- type safety
- no need to convert paths on Windows all the time
- no need to check if paths are absolute

Closes #9245.

RELNOTES: none
PiperOrigin-RevId: 265686214
diff --git a/src/main/cpp/bazel_startup_options.cc b/src/main/cpp/bazel_startup_options.cc
index 4866fce..3d702e3 100644
--- a/src/main/cpp/bazel_startup_options.cc
+++ b/src/main/cpp/bazel_startup_options.cc
@@ -87,13 +87,13 @@
         << "Output user root \"" << output_user_root
         << "\" contains a space. This will probably break the build. "
            "You should set a different --output_user_root.";
-  } else if (output_base.find_first_of(' ') != std::string::npos) {
+  } else if (output_base.Contains(' ')) {
     // output_base is computed from output_user_root by default.
     // If output_user_root was bad, don't check output_base: while output_base
     // may also be bad, we already warned about output_user_root so there's no
     // point in another warning.
     BAZEL_LOG(WARNING)
-        << "Output base \"" << output_base
+        << "Output base \"" << output_base.AsPrintablePath()
         << "\" contains a space. This will probably break the build. "
            "You should not set --output_base and let Bazel use the default, or "
            "set --output_base to a path without space.";
diff --git a/src/main/cpp/blaze.cc b/src/main/cpp/blaze.cc
index fbaabec..a514ef8 100644
--- a/src/main/cpp/blaze.cc
+++ b/src/main/cpp/blaze.cc
@@ -237,12 +237,9 @@
 
 class BlazeServer final {
  public:
-  BlazeServer(
-      const int connect_timeout_secs,
-      const bool batch,
-      const bool block_for_lock,
-      const string &output_base,
-      const string &server_jvm_out);
+  BlazeServer(const int connect_timeout_secs, const bool batch,
+              const bool block_for_lock, const blaze_util::Path &output_base,
+              const blaze_util::Path &server_jvm_out);
 
   // Acquire a lock for the server running in this output base. Returns the
   // number of milliseconds spent waiting for the lock.
@@ -309,7 +306,7 @@
   const int connect_timeout_secs_;
   const bool batch_;
   const bool block_for_lock_;
-  const string output_base_;
+  const blaze_util::Path output_base_;
 };
 
 ////////////////////////////////////////////////////////////////////////
@@ -370,7 +367,7 @@
 
   result.push_back("-XX:+HeapDumpOnOutOfMemoryError");
   result.push_back("-XX:HeapDumpPath=" +
-                   blaze_util::PathAsJvmFlag(startup_options.output_base));
+                   startup_options.output_base.AsJvmArgument());
 
   // TODO(b/109998449): only assume JDK >= 9 for embedded JDKs
   if (!startup_options.GetEmbeddedJavabase().empty()) {
@@ -470,13 +467,14 @@
                    blaze_util::ConvertPath(startup_options.install_base));
   result.push_back("--install_md5=" + install_md5);
   result.push_back("--output_base=" +
-                   blaze_util::ConvertPath(startup_options.output_base));
+                   startup_options.output_base.AsCommandLineArgument());
   result.push_back("--workspace_directory=" +
                    blaze_util::ConvertPath(workspace));
   result.push_back("--default_system_javabase=" + GetSystemJavabase());
 
-  if (!startup_options.server_jvm_out.empty()) {
-    result.push_back("--server_jvm_out=" + startup_options.server_jvm_out);
+  if (!startup_options.server_jvm_out.IsEmpty()) {
+    result.push_back("--server_jvm_out=" +
+                     startup_options.server_jvm_out.AsCommandLineArgument());
   }
 
   if (startup_options.deep_execroot) {
@@ -742,18 +740,24 @@
   }
 }
 
-static void WriteFileToStderrOrDie(const char *file_name) {
-  FILE *fp = fopen(file_name, "r");
+static void WriteFileToStderrOrDie(const blaze_util::Path &path) {
+#if defined(_WIN32) || defined(__CYGWIN__)
+  FILE *fp = _wfopen(path.AsNativePath().c_str(), L"r");
+#else
+  FILE *fp = fopen(path.AsNativePath().c_str(), "r");
+#endif
+
   if (fp == NULL) {
     BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
-        << "opening " << file_name << " failed: " << GetLastErrorString();
+        << "opening " << path.AsPrintablePath()
+        << " failed: " << GetLastErrorString();
   }
   char buffer[255];
   int num_read;
   while ((num_read = fread(buffer, 1, sizeof buffer, fp)) > 0) {
     if (ferror(fp)) {
       BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
-          << "failed to read from '" << file_name
+          << "failed to read from '" << path.AsPrintablePath()
           << "': " << GetLastErrorString();
     }
     fwrite(buffer, 1, num_read, stderr);
@@ -816,12 +820,14 @@
       if (server->ProcessInfo().jvm_log_file_append_) {
         // Don't dump the log if we were appending - the user should know where
         // to find it, and who knows how much content they may have accumulated.
-        BAZEL_LOG(USER) << "Server crashed during startup. See "
-                        << server->ProcessInfo().jvm_log_file_;
+        BAZEL_LOG(USER)
+            << "Server crashed during startup. See "
+            << server->ProcessInfo().jvm_log_file_.AsPrintablePath();
       } else {
-        BAZEL_LOG(USER) << "Server crashed during startup. Now printing "
-                        << server->ProcessInfo().jvm_log_file_;
-        WriteFileToStderrOrDie(server->ProcessInfo().jvm_log_file_.c_str());
+        BAZEL_LOG(USER)
+            << "Server crashed during startup. Now printing "
+            << server->ProcessInfo().jvm_log_file_.AsPrintablePath();
+        WriteFileToStderrOrDie(server->ProcessInfo().jvm_log_file_);
       }
       exit(blaze_exit_code::INTERNAL_ERROR);
     }
@@ -1126,8 +1132,8 @@
     return;
   }
 
-  string cmdline_path =
-      blaze_util::JoinPath(startup_options.output_base, "server/cmdline");
+  blaze_util::Path cmdline_path =
+      startup_options.output_base.GetRelative("server/cmdline");
   string old_joined_arguments;
 
   // No, /proc/$PID/cmdline does not work, because it is limited to 4K. Even
@@ -1162,8 +1168,8 @@
   // target dirs don't match, or if the symlink was not present, then kill any
   // running servers. Lastly, symlink to our installation so others know which
   // installation is running.
-  const string installation_path =
-      blaze_util::JoinPath(startup_options.output_base, "install");
+  const blaze_util::Path installation_path =
+      startup_options.output_base.GetRelative("install");
   string prev_installation;
   bool ok =
       blaze_util::ReadDirectorySymlink(installation_path, &prev_installation);
@@ -1180,9 +1186,10 @@
     blaze_util::UnlinkPath(installation_path);
     if (!SymlinkDirectories(startup_options.install_base,
                             installation_path)) {
+      string err = GetLastErrorString();
       BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
-          << "failed to create installation symlink '" << installation_path
-          << "': " << GetLastErrorString();
+          << "failed to create installation symlink '"
+          << installation_path.AsPrintablePath() << "': " << err;
     }
 
     // Update the mtime of the install base so that cleanup tools can
@@ -1310,37 +1317,42 @@
                                                          install_md5);
   }
 
-  if (startup_options->output_base.empty()) {
-    startup_options->output_base = blaze::GetHashedBaseDir(
-        startup_options->output_user_root, workspace);
+  if (startup_options->output_base.IsEmpty()) {
+    startup_options->output_base = blaze_util::Path(
+        blaze::GetHashedBaseDir(startup_options->output_user_root, workspace));
   }
 
-  const char *output_base = startup_options->output_base.c_str();
   if (!blaze_util::PathExists(startup_options->output_base)) {
     if (!blaze_util::MakeDirectories(startup_options->output_base, 0777)) {
+      string err = GetLastErrorString();
       BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
-          << "Output base directory '" << output_base
-          << "' could not be created: " << GetLastErrorString();
+          << "Output base directory '"
+          << startup_options->output_base.AsPrintablePath()
+          << "' could not be created: " << err;
     }
   } else {
     if (!blaze_util::IsDirectory(startup_options->output_base)) {
       BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
-          << "Output base directory '" << output_base
+          << "Output base directory '"
+          << startup_options->output_base.AsPrintablePath()
           << "' could not be created. It exists but is not a directory.";
     }
   }
   if (!blaze_util::CanAccessDirectory(startup_options->output_base)) {
     BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
-        << "Output base directory '" << output_base
+        << "Output base directory '"
+        << startup_options->output_base.AsPrintablePath()
         << "' must be readable and writable.";
   }
-  ExcludePathFromBackup(output_base);
+  ExcludePathFromBackup(startup_options->output_base);
 
-  startup_options->output_base = blaze_util::MakeCanonical(output_base);
-  if (startup_options->output_base.empty()) {
+  startup_options->output_base = startup_options->output_base.Canonicalize();
+  if (startup_options->output_base.IsEmpty()) {
+    string err = GetLastErrorString();
     BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
-        << "blaze_util::MakeCanonical('" << output_base
-        << "') failed: " << GetLastErrorString();
+        << "blaze_util::MakeCanonical('"
+        << startup_options->output_base.AsPrintablePath()
+        << "') failed: " << err;
   }
 }
 
@@ -1420,10 +1432,10 @@
   }
 }
 
-static int GetExitCodeForAbruptExit(const string &output_base) {
+static int GetExitCodeForAbruptExit(const blaze_util::Path &output_base) {
   BAZEL_LOG(INFO) << "Looking for a custom exit-code.";
-  std::string filename = blaze_util::JoinPath(
-      output_base, "exit_code_to_use_on_abrupt_exit");
+  blaze_util::Path filename =
+      output_base.GetRelative("exit_code_to_use_on_abrupt_exit");
   std::string content;
   if (!blaze_util::ReadFile(filename, &content)) {
     BAZEL_LOG(INFO) << "Unable to read the custom exit-code file. "
@@ -1572,7 +1584,8 @@
     UnlimitCoredumps();
   }
 
-  blaze::CreateSecureOutputRoot(startup_options->output_user_root);
+  blaze::CreateSecureOutputRoot(
+      blaze_util::Path(startup_options->output_user_root));
 
   // Only start a server when in a workspace because otherwise we won't do more
   // than emit a help message.
@@ -1610,18 +1623,16 @@
   return memcmp(cookieA.c_str(), cookieB.c_str(), cookie_length) == 0;
 }
 
-BlazeServer::BlazeServer(
-    const int connect_timeout_secs,
-    const bool batch,
-    const bool block_for_lock,
-    const string &output_base,
-    const string &server_jvm_out)
-  : connected_(false),
-    process_info_(output_base, server_jvm_out),
-    connect_timeout_secs_(connect_timeout_secs),
-    batch_(batch),
-    block_for_lock_(block_for_lock),
-    output_base_(output_base) {
+BlazeServer::BlazeServer(const int connect_timeout_secs, const bool batch,
+                         const bool block_for_lock,
+                         const blaze_util::Path &output_base,
+                         const blaze_util::Path &server_jvm_out)
+    : connected_(false),
+      process_info_(output_base, server_jvm_out),
+      connect_timeout_secs_(connect_timeout_secs),
+      batch_(batch),
+      block_for_lock_(block_for_lock),
+      output_base_(output_base) {
   gpr_set_log_function(null_grpc_log_function);
 
   pipe_.reset(blaze_util::CreatePipe());
@@ -1658,14 +1669,13 @@
 bool BlazeServer::Connect() {
   assert(!connected_);
 
-  std::string server_dir = blaze_util::JoinPath(output_base_, "server");
+  blaze_util::Path server_dir = output_base_.GetRelative("server");
   std::string port;
   std::string ipv4_prefix = "127.0.0.1:";
   std::string ipv6_prefix_1 = "[0:0:0:0:0:0:0:1]:";
   std::string ipv6_prefix_2 = "[::1]:";
 
-  if (!blaze_util::ReadFile(blaze_util::JoinPath(server_dir, "command_port"),
-                            &port)) {
+  if (!blaze_util::ReadFile(server_dir.GetRelative("command_port"), &port)) {
     return false;
   }
 
@@ -1676,12 +1686,12 @@
     return false;
   }
 
-  if (!blaze_util::ReadFile(blaze_util::JoinPath(server_dir, "request_cookie"),
+  if (!blaze_util::ReadFile(server_dir.GetRelative("request_cookie"),
                             &request_cookie_)) {
     return false;
   }
 
-  if (!blaze_util::ReadFile(blaze_util::JoinPath(server_dir, "response_cookie"),
+  if (!blaze_util::ReadFile(server_dir.GetRelative("response_cookie"),
                             &response_cookie_)) {
     return false;
   }
@@ -1864,7 +1874,7 @@
           << "Shutdown request failed, server still alive: (error code: "
           << status.error_code() << ", error message: '"
           << status.error_message() << "', log file: '"
-          << process_info_.jvm_log_file_ << "')";
+          << process_info_.jvm_log_file_.AsPrintablePath() << "')";
     }
     KillServerProcess(process_info_.server_pid_, output_base_);
   }
@@ -2002,12 +2012,12 @@
     BAZEL_LOG(USER) << "\nServer terminated abruptly (error code: "
                     << status.error_code() << ", error message: '"
                     << status.error_message() << "', log file: '"
-                    << process_info_.jvm_log_file_ << "')\n";
+                    << process_info_.jvm_log_file_.AsPrintablePath() << "')\n";
     return GetExitCodeForAbruptExit(output_base_);
   } else if (!finished) {
     BAZEL_LOG(USER)
         << "\nServer finished RPC without an explicit exit code (log file: '"
-        << process_info_.jvm_log_file_ << "')\n";
+        << process_info_.jvm_log_file_.AsPrintablePath() << "')\n";
     return GetExitCodeForAbruptExit(output_base_);
   } else if (final_response.has_exec_request()) {
     const command_server::ExecRequest& request = final_response.exec_request();
diff --git a/src/main/cpp/blaze_util.cc b/src/main/cpp/blaze_util.cc
index ca25bba..2c3f7a2 100644
--- a/src/main/cpp/blaze_util.cc
+++ b/src/main/cpp/blaze_util.cc
@@ -175,7 +175,7 @@
             elapsed_seconds, wait_seconds);
 }
 
-bool AwaitServerProcessTermination(int pid, const string& output_base,
+bool AwaitServerProcessTermination(int pid, const blaze_util::Path& output_base,
                                    unsigned int wait_seconds) {
   uint64_t st = GetMillisecondsMonotonic();
   const unsigned int first_seconds = 5;
diff --git a/src/main/cpp/blaze_util.h b/src/main/cpp/blaze_util.h
index 123ead9..f91ee50 100644
--- a/src/main/cpp/blaze_util.h
+++ b/src/main/cpp/blaze_util.h
@@ -26,6 +26,8 @@
 #include <string>
 #include <vector>
 
+#include "src/main/cpp/util/path.h"
+
 namespace blaze {
 
 extern const char kServerPidFile[];
@@ -74,7 +76,7 @@
 // wait_seconds elapses or the server process terminates. Returns true if a
 // check sees that the server process terminated. Logs to stderr after 5, 10,
 // and 30 seconds if the wait lasts that long.
-bool AwaitServerProcessTermination(int pid, const std::string& output_base,
+bool AwaitServerProcessTermination(int pid, const blaze_util::Path& output_base,
                                    unsigned int wait_seconds);
 
 // The number of seconds the client will wait for the server process to
diff --git a/src/main/cpp/blaze_util_darwin.cc b/src/main/cpp/blaze_util_darwin.cc
index fbeb0a1..bcb3bcd 100644
--- a/src/main/cpp/blaze_util_darwin.cc
+++ b/src/main/cpp/blaze_util_darwin.cc
@@ -99,11 +99,12 @@
   return "/var/tmp";
 }
 
-void WarnFilesystemType(const string& output_base) {
+void WarnFilesystemType(const blaze_util::Path &output_base) {
   // Check to see if we are on a non-local drive.
   CFScopedReleaser<CFURLRef> cf_url(CFURLCreateFromFileSystemRepresentation(
-      kCFAllocatorDefault, reinterpret_cast<const UInt8 *>(output_base.c_str()),
-      output_base.length(), true));
+      kCFAllocatorDefault,
+      reinterpret_cast<const UInt8 *>(output_base.AsNativePath().c_str()),
+      output_base.AsNativePath().length(), true));
   CFBooleanRef cf_local = NULL;
   CFErrorRef cf_error = NULL;
   if (!cf_url.isValid() ||
@@ -111,13 +112,13 @@
                                        &cf_local, &cf_error)) {
     CFScopedReleaser<CFErrorRef> cf_error_releaser(cf_error);
     BAZEL_LOG(WARNING) << "couldn't get file system type information for '"
-                       << output_base
+                       << output_base.AsPrintablePath()
                        << "': " << DescriptionFromCFError(cf_error_releaser);
     return;
   }
   CFScopedReleaser<CFBooleanRef> cf_local_releaser(cf_local);
   if (!CFBooleanGetValue(cf_local_releaser)) {
-    BAZEL_LOG(WARNING) << "Output base '" << output_base
+    BAZEL_LOG(WARNING) << "Output base '" << output_base.AsPrintablePath()
                        << "' is on a non-local drive. This may lead to "
                           "surprising failures and undetermined behavior.";
   }
@@ -204,7 +205,7 @@
 void WriteSystemSpecificProcessIdentifier(const blaze_util::Path &server_dir,
                                           pid_t server_pid) {}
 
-bool VerifyServerProcess(int pid, const string &output_base) {
+bool VerifyServerProcess(int pid, const blaze_util::Path &output_base) {
   // TODO(lberki): This only checks for the process's existence, not whether
   // its start time matches. Therefore this might accidentally kill an
   // unrelated process if the server died and the PID got reused.
@@ -213,19 +214,22 @@
 
 // Sets a flag on path to exclude the path from Apple's automatic backup service
 // (Time Machine)
-void ExcludePathFromBackup(const string &path) {
+void ExcludePathFromBackup(const blaze_util::Path &path) {
   CFScopedReleaser<CFURLRef> cf_url(CFURLCreateFromFileSystemRepresentation(
-      kCFAllocatorDefault, reinterpret_cast<const UInt8 *>(path.c_str()),
-      path.length(), true));
+      kCFAllocatorDefault,
+      reinterpret_cast<const UInt8 *>(path.AsNativePath().c_str()),
+      path.AsNativePath().length(), true));
   if (!cf_url.isValid()) {
-    BAZEL_LOG(WARNING) << "unable to exclude '" << path << "' from backups";
+    BAZEL_LOG(WARNING) << "unable to exclude '" << path.AsPrintablePath()
+                       << "' from backups";
     return;
   }
   CFErrorRef cf_error = NULL;
   if (!CFURLSetResourcePropertyForKey(cf_url, kCFURLIsExcludedFromBackupKey,
                                       kCFBooleanTrue, &cf_error)) {
     CFScopedReleaser<CFErrorRef> cf_error_releaser(cf_error);
-    BAZEL_LOG(WARNING) << "unable to exclude '" << path << "' from backups: "
+    BAZEL_LOG(WARNING) << "unable to exclude '" << path.AsPrintablePath()
+                       << "' from backups: "
                        << DescriptionFromCFError(cf_error_releaser);
     return;
   }
diff --git a/src/main/cpp/blaze_util_freebsd.cc b/src/main/cpp/blaze_util_freebsd.cc
index b731d38..067dfae 100644
--- a/src/main/cpp/blaze_util_freebsd.cc
+++ b/src/main/cpp/blaze_util_freebsd.cc
@@ -56,16 +56,17 @@
   }
 }
 
-void WarnFilesystemType(const string &output_base) {
+void WarnFilesystemType(const blaze_util::Path &output_base) {
   struct statfs buf = {};
-  if (statfs(output_base.c_str(), &buf) < 0) {
+  if (statfs(output_base.AsNativePath().c_str(), &buf) < 0) {
     BAZEL_LOG(WARNING) << "couldn't get file system type information for '"
-                       << output_base << "': " << strerror(errno);
+                       << output_base.AsPrintablePath()
+                       << "': " << strerror(errno);
     return;
   }
 
   if (strcmp(buf.f_fstypename, "nfs") == 0) {
-    BAZEL_LOG(WARNING) << "Output base '" << output_base
+    BAZEL_LOG(WARNING) << "Output base '" << output_base.AsPrintablePath()
                        << "' is on NFS. This may lead to surprising failures "
                           "and undetermined behavior.";
   }
@@ -164,7 +165,7 @@
 void WriteSystemSpecificProcessIdentifier(const blaze_util::Path &server_dir,
                                           pid_t server_pid) {}
 
-bool VerifyServerProcess(int pid, const string &output_base) {
+bool VerifyServerProcess(int pid, const blaze_util::Path &output_base) {
   // TODO(lberki): This only checks for the process's existence, not whether
   // its start time matches. Therefore this might accidentally kill an
   // unrelated process if the server died and the PID got reused.
@@ -172,8 +173,7 @@
 }
 
 // Not supported.
-void ExcludePathFromBackup(const string &path) {
-}
+void ExcludePathFromBackup(const blaze_util::Path &path) {}
 
 int32_t GetExplicitSystemLimit(const int resource) {
   return -1;
diff --git a/src/main/cpp/blaze_util_linux.cc b/src/main/cpp/blaze_util_linux.cc
index b23898e..91d65ea 100644
--- a/src/main/cpp/blaze_util_linux.cc
+++ b/src/main/cpp/blaze_util_linux.cc
@@ -66,16 +66,17 @@
   return "/tmp";
 }
 
-void WarnFilesystemType(const string& output_base) {
+void WarnFilesystemType(const blaze_util::Path &output_base) {
   struct statfs buf = {};
-  if (statfs(output_base.c_str(), &buf) < 0) {
+  if (statfs(output_base.AsNativePath().c_str(), &buf) < 0) {
     BAZEL_LOG(WARNING) << "couldn't get file system type information for '"
-                       << output_base << "': " << strerror(errno);
+                       << output_base.AsPrintablePath()
+                       << "': " << strerror(errno);
     return;
   }
 
   if (buf.f_type == NFS_SUPER_MAGIC) {
-    BAZEL_LOG(WARNING) << "Output base '" << output_base
+    BAZEL_LOG(WARNING) << "Output base '" << output_base.AsPrintablePath()
                        << "' is on NFS. This may lead to surprising failures "
                           "and undetermined behavior.";
   }
@@ -232,7 +233,7 @@
 // On Linux we use a combination of PID and start time to identify the server
 // process. That is supposed to be unique unless one can start more processes
 // than there are PIDs available within a single jiffy.
-bool VerifyServerProcess(int pid, const string& output_base) {
+bool VerifyServerProcess(int pid, const blaze_util::Path &output_base) {
   string start_time;
   if (!GetStartTime(ToString(pid), &start_time)) {
     // Cannot read PID file from /proc . Process died meantime, all is good. No
@@ -242,8 +243,7 @@
 
   string recorded_start_time;
   bool file_present = blaze_util::ReadFile(
-      blaze_util::JoinPath(output_base, "server/server.starttime"),
-      &recorded_start_time);
+      output_base.GetRelative("server/server.starttime"), &recorded_start_time);
 
   // If start time file got deleted, but PID file didn't, assume that this is an
   // old Blaze process that doesn't know how to write start time files yet.
@@ -251,8 +251,7 @@
 }
 
 // Not supported.
-void ExcludePathFromBackup(const string &path) {
-}
+void ExcludePathFromBackup(const blaze_util::Path &path) {}
 
 int32_t GetExplicitSystemLimit(const int resource) {
   return -1;
diff --git a/src/main/cpp/blaze_util_platform.h b/src/main/cpp/blaze_util_platform.h
index d001306..1474bce 100644
--- a/src/main/cpp/blaze_util_platform.h
+++ b/src/main/cpp/blaze_util_platform.h
@@ -82,18 +82,19 @@
     return server_process_info_;
   }
   const std::string& GetProductName() const { return product_name_; }
-  const std::string& GetOutputBase() const { return output_base_; }
+  const blaze_util::Path& GetOutputBase() const { return output_base_; }
   void CancelServer() { cancel_server_(); }
-  void Install(
-      const std::string &product_name, const std::string &output_base,
-      const ServerProcessInfo* server_process_info, Callback cancel_server);
+  void Install(const std::string& product_name,
+               const blaze_util::Path& output_base,
+               const ServerProcessInfo* server_process_info,
+               Callback cancel_server);
   ATTRIBUTE_NORETURN void PropagateSignalOrExit(int exit_code);
 
  private:
   static SignalHandler INSTANCE;
 
   std::string product_name_;
-  std::string output_base_;
+  blaze_util::Path output_base_;
   const ServerProcessInfo* server_process_info_;
   Callback cancel_server_;
 
@@ -117,7 +118,7 @@
 std::string GetHomeDir();
 
 // Warn about dubious filesystem types, such as NFS, case-insensitive (?).
-void WarnFilesystemType(const std::string& output_base);
+void WarnFilesystemType(const blaze_util::Path& output_base);
 
 // Returns elapsed milliseconds since some unspecified start of time.
 // The results are monotonic, i.e. subsequent calls to this method never return
@@ -175,7 +176,7 @@
 int ExecuteDaemon(
     const std::string& exe, const std::vector<std::string>& args_vector,
     const std::map<std::string, EnvVarValue>& env,
-    const std::string& daemon_output, const bool daemon_output_append,
+    const blaze_util::Path& daemon_output, const bool daemon_output_append,
     const std::string& binaries_dir, const blaze_util::Path& server_dir,
     const StartupOptions& options, BlazeServerStartup** server_startup);
 
@@ -185,7 +186,8 @@
 // Create a symlink to directory ``target`` at location ``link``.
 // Returns true on success, false on failure. The target must be absolute.
 // Implemented via junctions on Windows.
-bool SymlinkDirectories(const std::string& target, const std::string& link);
+bool SymlinkDirectories(const std::string& target,
+                        const blaze_util::Path& link);
 
 struct BlazeLock {
 #if defined(_WIN32) || defined(__CYGWIN__)
@@ -198,7 +200,7 @@
 // Acquires a lock on the output base. Exits if the lock cannot be acquired.
 // Sets ``lock`` to a value that can subsequently be passed to ReleaseLock().
 // Returns the number of milliseconds spent with waiting for the lock.
-uint64_t AcquireLock(const std::string& output_base, bool batch_mode,
+uint64_t AcquireLock(const blaze_util::Path& output_base, bool batch_mode,
                      bool block, BlazeLock* blaze_lock);
 
 // Releases the lock on the output base. In case of an error, continues as
@@ -206,12 +208,12 @@
 void ReleaseLock(BlazeLock* blaze_lock);
 
 // Verifies whether the server process still exists. Returns true if it does.
-bool VerifyServerProcess(int pid, const std::string& output_base);
+bool VerifyServerProcess(int pid, const blaze_util::Path& output_base);
 
 // Kills a server process based on its PID.
 // Returns true if the server process was found and killed.
 // WARNING! This function can be called from a signal handler!
-bool KillServerProcess(int pid, const std::string& output_base);
+bool KillServerProcess(int pid, const blaze_util::Path& output_base);
 
 // Wait for approximately the specified number of milliseconds. The actual
 // amount of time waited may be more or less because of interrupts or system
@@ -219,7 +221,7 @@
 void TrySleep(unsigned int milliseconds);
 
 // Mark path as being excluded from backups (if supported by operating system).
-void ExcludePathFromBackup(const std::string& path);
+void ExcludePathFromBackup(const blaze_util::Path& path);
 
 // Returns the canonical form of the base dir given a root and a hashable
 // string. The resulting dir is composed of the root + md5(hashable)
@@ -229,7 +231,7 @@
 // Create a safe installation directory where we keep state, installations etc.
 // This method ensures that the directory is created, is owned by the current
 // user, and not accessible to anyone else.
-void CreateSecureOutputRoot(const std::string& path);
+void CreateSecureOutputRoot(const blaze_util::Path& path);
 
 std::string GetEnv(const std::string& name);
 
diff --git a/src/main/cpp/blaze_util_posix.cc b/src/main/cpp/blaze_util_posix.cc
index 23c2c8c..3efbbdf 100644
--- a/src/main/cpp/blaze_util_posix.cc
+++ b/src/main/cpp/blaze_util_posix.cc
@@ -163,10 +163,12 @@
       signal_handler_received_signal = SIGPIPE;
       break;
     case SIGQUIT:
-      SigPrintf(
-          "\nSending SIGQUIT to JVM process %d (see %s).\n\n",
-          SignalHandler::Get().GetServerProcessInfo()->server_pid_,
-          SignalHandler::Get().GetServerProcessInfo()->jvm_log_file_.c_str());
+      SigPrintf("\nSending SIGQUIT to JVM process %d (see %s).\n\n",
+                SignalHandler::Get().GetServerProcessInfo()->server_pid_,
+                SignalHandler::Get()
+                    .GetServerProcessInfo()
+                    ->jvm_log_file_.AsNativePath()
+                    .c_str());
       kill(SignalHandler::Get().GetServerProcessInfo()->server_pid_, SIGQUIT);
       break;
   }
@@ -174,9 +176,9 @@
   errno = saved_errno;
 }
 
-void SignalHandler::Install(const string &product_name,
-                            const string &output_base,
-                            const ServerProcessInfo *server_process_info,
+void SignalHandler::Install(const string& product_name,
+                            const blaze_util::Path& output_base,
+                            const ServerProcessInfo* server_process_info,
                             SignalHandler::Callback cancel_server) {
   product_name_ = product_name;
   output_base_ = output_base;
@@ -302,8 +304,8 @@
 
 const char kListSeparator = ':';
 
-bool SymlinkDirectories(const string &target, const string &link) {
-  return symlink(target.c_str(), link.c_str()) == 0;
+bool SymlinkDirectories(const string& target, const blaze_util::Path& link) {
+  return symlink(target.c_str(), link.AsNativePath().c_str()) == 0;
 }
 
 // Notifies the client about the death of the server process by keeping a socket
@@ -357,15 +359,16 @@
 
 int ExecuteDaemon(const string& exe, const std::vector<string>& args_vector,
                   const std::map<string, EnvVarValue>& env,
-                  const string& daemon_output, const bool daemon_output_append,
-                  const string& binaries_dir,
+                  const blaze_util::Path& daemon_output,
+                  const bool daemon_output_append, const string& binaries_dir,
                   const blaze_util::Path& server_dir,
                   const StartupOptions& options,
                   BlazeServerStartup** server_startup) {
   const blaze_util::Path pid_file = server_dir.GetRelative(kServerPidFile);
   const string daemonize = blaze_util::JoinPath(binaries_dir, "daemonize");
 
-  std::vector<string> daemonize_args = {"daemonize", "-l", daemon_output, "-p",
+  std::vector<string> daemonize_args = {"daemonize", "-l",
+                                        daemon_output.AsNativePath(), "-p",
                                         pid_file.AsNativePath()};
   if (daemon_output_append) {
     daemonize_args.push_back("-a");
@@ -448,48 +451,51 @@
   return blaze_util::JoinPath(root, digest.String());
 }
 
-void CreateSecureOutputRoot(const string& path) {
-  const char* root = path.c_str();
+void CreateSecureOutputRoot(const blaze_util::Path& path) {
   struct stat fileinfo = {};
 
-  if (!blaze_util::MakeDirectories(root, 0755)) {
+  if (!blaze_util::MakeDirectories(path, 0755)) {
+    string err = GetLastErrorString();
     BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
-        << "mkdir('" << root << "'): " << GetLastErrorString();
+        << "mkdir('" << path.AsPrintablePath() << "'): " << err;
   }
 
   // The path already exists.
   // Check ownership and mode, and verify that it is a directory.
 
-  if (lstat(root, &fileinfo) < 0) {
+  if (lstat(path.AsNativePath().c_str(), &fileinfo) < 0) {
+    string err = GetLastErrorString();
     BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
-        << "lstat('" << root << "'): " << GetLastErrorString();
+        << "lstat('" << path.AsPrintablePath() << "'): " << err;
   }
 
   if (fileinfo.st_uid != geteuid()) {
     BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
-        << "'" << root << "' is not owned by me";
+        << "'" << path.AsPrintablePath() << "' is not owned by me";
   }
 
   if ((fileinfo.st_mode & 022) != 0) {
     int new_mode = fileinfo.st_mode & (~022);
-    if (chmod(root, new_mode) < 0) {
+    if (chmod(path.AsNativePath().c_str(), new_mode) < 0) {
       BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
-          << "'" << root << "' has mode " << (fileinfo.st_mode & 07777)
-          << ", chmod to " << new_mode << " failed";
+          << "'" << path.AsPrintablePath() << "' has mode "
+          << (fileinfo.st_mode & 07777) << ", chmod to " << new_mode
+          << " failed";
     }
   }
 
-  if (stat(root, &fileinfo) < 0) {
+  if (stat(path.AsNativePath().c_str(), &fileinfo) < 0) {
+    string err = GetLastErrorString();
     BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
-        << "stat('" << root << "'): " << GetLastErrorString();
+        << "stat('" << path.AsPrintablePath() << "'): " << err;
   }
 
   if (!S_ISDIR(fileinfo.st_mode)) {
     BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
-        << "'" << root << "' is not a directory";
+        << "'" << path.AsPrintablePath() << "' is not a directory";
   }
 
-  ExcludePathFromBackup(root);
+  ExcludePathFromBackup(path);
 }
 
 string GetEnv(const string& name) {
@@ -583,22 +589,24 @@
   return -1;
 }
 
-uint64_t AcquireLock(const string& output_base, bool batch_mode, bool block,
-                     BlazeLock* blaze_lock) {
-  string lockfile = blaze_util::JoinPath(output_base, "lock");
-  int lockfd = open(lockfile.c_str(), O_CREAT|O_RDWR, 0644);
+uint64_t AcquireLock(const blaze_util::Path& output_base, bool batch_mode,
+                     bool block, BlazeLock* blaze_lock) {
+  blaze_util::Path lockfile = output_base.GetRelative("lock");
+  int lockfd = open(lockfile.AsNativePath().c_str(), O_CREAT | O_RDWR, 0644);
 
   if (lockfd < 0) {
+    string err = GetLastErrorString();
     BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
-        << "cannot open lockfile '" << lockfile
-        << "' for writing: " << GetLastErrorString();
+        << "cannot open lockfile '" << lockfile.AsPrintablePath()
+        << "' for writing: " << err;
   }
 
   // Keep server from inheriting a useless fd if we are not in batch mode
   if (!batch_mode) {
+    string err = GetLastErrorString();
     if (fcntl(lockfd, F_SETFD, FD_CLOEXEC) == -1) {
       BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
-          << "fcntl(F_SETFD) failed for lockfile: " << GetLastErrorString();
+          << "fcntl(F_SETFD) failed for lockfile: " << err;
     }
   }
 
@@ -680,7 +688,7 @@
   close(blaze_lock->lockfd);
 }
 
-bool KillServerProcess(int pid, const string& output_base) {
+bool KillServerProcess(int pid, const blaze_util::Path& output_base) {
   // Kill the process and make sure it's dead before proceeding.
   killpg(pid, SIGKILL);
   if (!AwaitServerProcessTermination(pid, output_base,
diff --git a/src/main/cpp/blaze_util_windows.cc b/src/main/cpp/blaze_util_windows.cc
index dae4855..df6c4c5 100644
--- a/src/main/cpp/blaze_util_windows.cc
+++ b/src/main/cpp/blaze_util_windows.cc
@@ -317,9 +317,9 @@
   return false;
 }
 
-void SignalHandler::Install(const string &product_name,
-                            const string &output_base,
-                            const ServerProcessInfo *server_process_info_,
+void SignalHandler::Install(const string& product_name,
+                            const blaze_util::Path& output_base,
+                            const ServerProcessInfo* server_process_info_,
                             SignalHandler::Callback cancel_server) {
   product_name_ = product_name;
   output_base_ = output_base;
@@ -377,8 +377,7 @@
   LocalFree(message_buffer);
 }
 
-void WarnFilesystemType(const string& output_base) {
-}
+void WarnFilesystemType(const blaze_util::Path& output_base) {}
 
 string GetProcessIdAsString() {
   return ToString(GetCurrentProcessId());
@@ -555,7 +554,8 @@
   }
 }
 
-static HANDLE CreateJvmOutputFile(const wstring& path, LPSECURITY_ATTRIBUTES sa,
+static HANDLE CreateJvmOutputFile(const blaze_util::Path& path,
+                                  LPSECURITY_ATTRIBUTES sa,
                                   bool daemon_out_append) {
   // If the previous server process was asked to be shut down (but not killed),
   // it takes a while for it to comply, so wait until the JVM output file that
@@ -564,18 +564,19 @@
   static const unsigned int timeout_sec = 60;
   for (unsigned int waited = 0; waited < timeout_sec; ++waited) {
     HANDLE handle = ::CreateFileW(
-        /* lpFileName */ path.c_str(),
+        /* lpFileName */ path.AsNativePath().c_str(),
         /* dwDesiredAccess */ GENERIC_READ | GENERIC_WRITE,
         /* dwShareMode */ FILE_SHARE_READ,
         /* lpSecurityAttributes */ sa,
         /* dwCreationDisposition */
-            daemon_out_append ? OPEN_ALWAYS : CREATE_ALWAYS,
+        daemon_out_append ? OPEN_ALWAYS : CREATE_ALWAYS,
         /* dwFlagsAndAttributes */ FILE_ATTRIBUTE_NORMAL,
         /* hTemplateFile */ NULL);
     if (handle != INVALID_HANDLE_VALUE) {
       if (daemon_out_append
           && !SetFilePointerEx(handle, {0}, NULL, FILE_END)) {
-        fprintf(stderr, "Could not seek to end of file (%ls)\n", path.c_str());
+        fprintf(stderr, "Could not seek to end of file (%s)\n",
+                path.AsPrintablePath().c_str());
         return INVALID_HANDLE_VALUE;
       }
       return handle;
@@ -616,20 +617,11 @@
 
 int ExecuteDaemon(const string& exe, const std::vector<string>& args_vector,
                   const std::map<string, EnvVarValue>& env,
-                  const string& daemon_output, const bool daemon_out_append,
-                  const string& binaries_dir,
+                  const blaze_util::Path& daemon_output,
+                  const bool daemon_out_append, const string& binaries_dir,
                   const blaze_util::Path& server_dir,
                   const StartupOptions& options,
                   BlazeServerStartup** server_startup) {
-  wstring wdaemon_output;
-  string error;
-  if (!blaze_util::AsAbsoluteWindowsPath(daemon_output, &wdaemon_output,
-                                         &error)) {
-    BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
-        << "ExecuteDaemon(" << exe << "): AsAbsoluteWindowsPath("
-        << daemon_output << ") failed: " << error;
-  }
-
   SECURITY_ATTRIBUTES inheritable_handle_sa = {sizeof(SECURITY_ATTRIBUTES),
                                                NULL, TRUE};
 
@@ -637,18 +629,18 @@
       L"NUL", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE,
       &inheritable_handle_sa, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL));
   if (!devnull.IsValid()) {
-    error = GetLastErrorString();
+    std::string error = GetLastErrorString();
     BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
         << "ExecuteDaemon(" << exe << "): CreateFileA(NUL) failed: " << error;
   }
 
   AutoHandle stdout_file(CreateJvmOutputFile(
-      wdaemon_output.c_str(), &inheritable_handle_sa, daemon_out_append));
+      daemon_output, &inheritable_handle_sa, daemon_out_append));
   if (!stdout_file.IsValid()) {
-    error = GetLastErrorString();
+    std::string error = GetLastErrorString();
     BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
         << "ExecuteDaemon(" << exe << "): CreateJvmOutputFile("
-        << blaze_util::WstringToString(wdaemon_output) << ") failed: " << error;
+        << daemon_output.AsPrintablePath() << ") failed: " << error;
   }
   HANDLE stderr_handle;
   // We must duplicate the handle to stdout, otherwise "bazel clean --expunge"
@@ -663,10 +655,10 @@
           /* dwDesiredAccess */ 0,
           /* bInheritHandle */ TRUE,
           /* dwOptions */ DUPLICATE_SAME_ACCESS)) {
+    std::string error = GetLastErrorString();
     BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
         << "ExecuteDaemon(" << exe << "): DuplicateHandle("
-        << blaze_util::WstringToString(wdaemon_output)
-        << ") failed: " << GetLastErrorString();
+        << daemon_output.AsPrintablePath() << ") failed: " << error;
   }
   AutoHandle stderr_file(stderr_handle);
 
@@ -799,33 +791,29 @@
 
 const char kListSeparator = ';';
 
-bool SymlinkDirectories(const string &posix_target, const string &posix_name) {
-  wstring name;
+bool SymlinkDirectories(const string& posix_target,
+                        const blaze_util::Path& name) {
   wstring target;
   string error;
-  if (!blaze_util::AsAbsoluteWindowsPath(posix_name, &name, &error)) {
-    BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
-        << "SymlinkDirectories(" << posix_target << ", " << posix_name
-        << "): AsAbsoluteWindowsPath(" << posix_target << ") failed: " << error;
-    return false;
-  }
   if (!blaze_util::AsAbsoluteWindowsPath(posix_target, &target, &error)) {
     BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
-        << "SymlinkDirectories(" << posix_target << ", " << posix_name
-        << "): AsAbsoluteWindowsPath(" << posix_name << ") failed: " << error;
+        << "SymlinkDirectories(" << posix_target << ", "
+        << name.AsPrintablePath() << "): AsAbsoluteWindowsPath(" << posix_target
+        << ") failed: " << error;
     return false;
   }
   wstring werror;
-  if (CreateJunction(name, target, &werror) != CreateJunctionResult::kSuccess) {
+  if (CreateJunction(name.AsNativePath(), target, &werror) !=
+      CreateJunctionResult::kSuccess) {
     string error(blaze_util::WstringToCstring(werror.c_str()).get());
     BAZEL_LOG(ERROR) << "SymlinkDirectories(" << posix_target << ", "
-                     << posix_name << "): CreateJunction: " << error;
+                     << name.AsPrintablePath()
+                     << "): CreateJunction: " << error;
     return false;
   }
   return true;
 }
 
-
 #ifndef STILL_ACTIVE
 #define STILL_ACTIVE (259)  // From MSDN about GetExitCodeProcess.
 #endif
@@ -833,7 +821,7 @@
 // On Windows (and Linux) we use a combination of PID and start time to identify
 // the server process. That is supposed to be unique unless one can start more
 // processes than there are PIDs available within a single jiffy.
-bool VerifyServerProcess(int pid, const string& output_base) {
+bool VerifyServerProcess(int pid, const blaze_util::Path& output_base) {
   AutoHandle process(
       ::OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid));
   if (!process.IsValid()) {
@@ -852,15 +840,14 @@
 
   string recorded_start_time;
   bool file_present = blaze_util::ReadFile(
-      blaze_util::JoinPath(output_base, "server/server.starttime"),
-      &recorded_start_time);
+      output_base.GetRelative("server/server.starttime"), &recorded_start_time);
 
   // If start time file got deleted, but PID file didn't, assume that this is an
   // old Bazel process that doesn't know how to write start time files yet.
   return !file_present || recorded_start_time == ToString(start_time);
 }
 
-bool KillServerProcess(int pid, const string& output_base) {
+bool KillServerProcess(int pid, const blaze_util::Path& output_base) {
   AutoHandle process(::OpenProcess(
       PROCESS_TERMINATE | PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid));
   DWORD exitcode = 0;
@@ -874,9 +861,10 @@
   BOOL result = TerminateProcess(process, /*uExitCode*/ 0);
   if (!result || !AwaitServerProcessTermination(pid, output_base,
                                                 kPostKillGracePeriodSeconds)) {
+    string err = GetLastErrorString();
     BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
         << "Cannot terminate server process with PID " << pid
-        << ", output_base=(" << output_base << "): " << GetLastErrorString();
+        << ", output_base=(" << output_base.AsPrintablePath() << "): " << err;
   }
   return result;
 }
@@ -886,8 +874,7 @@
 }
 
 // Not supported.
-void ExcludePathFromBackup(const string &path) {
-}
+void ExcludePathFromBackup(const blaze_util::Path& path) {}
 
 string GetHashedBaseDir(const string& root, const string& hashable) {
   // Builds a shorter output base dir name for Windows.
@@ -920,21 +907,21 @@
   return blaze_util::JoinPath(root, string(coded_name));
 }
 
-void CreateSecureOutputRoot(const string& path) {
+void CreateSecureOutputRoot(const blaze_util::Path& path) {
   // TODO(bazel-team): implement this properly, by mimicing whatever the POSIX
   // implementation does.
-  const char* root = path.c_str();
   if (!blaze_util::MakeDirectories(path, 0755)) {
+    string err = GetLastErrorString();
     BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
-        << "MakeDirectories(" << root << ") failed: " << GetLastErrorString();
+        << "MakeDirectories(" << path.AsPrintablePath() << ") failed: " << err;
   }
 
   if (!blaze_util::IsDirectory(path)) {
     BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
-        << "'" << root << "' is not a directory";
+        << "'" << path.AsPrintablePath() << "' is not a directory";
   }
 
-  ExcludePathFromBackup(root);
+  ExcludePathFromBackup(path);
 }
 
 string GetEnv(const string& name) {
@@ -1086,23 +1073,15 @@
   return GetMillisecondsAsLargeInt(kFrequency).QuadPart;
 }
 
-uint64_t AcquireLock(const string& output_base, bool batch_mode, bool block,
-                     BlazeLock* blaze_lock) {
-  string lockfile = blaze_util::JoinPath(output_base, "lock");
-  wstring wlockfile;
-  string error;
-  if (!blaze_util::AsAbsoluteWindowsPath(lockfile, &wlockfile, &error)) {
-    BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
-        << "AcquireLock(" << output_base << "): AsAbsoluteWindowsPath("
-        << lockfile << ") failed: " << error;
-  }
-
+uint64_t AcquireLock(const blaze_util::Path& output_base, bool batch_mode,
+                     bool block, BlazeLock* blaze_lock) {
+  blaze_util::Path lockfile = output_base.GetRelative("lock");
   blaze_lock->handle = INVALID_HANDLE_VALUE;
   bool first_lock_attempt = true;
   uint64_t st = GetMillisecondsMonotonic();
   while (true) {
     blaze_lock->handle = ::CreateFileW(
-        /* lpFileName */ wlockfile.c_str(),
+        /* lpFileName */ lockfile.AsNativePath().c_str(),
         /* dwDesiredAccess */ GENERIC_READ | GENERIC_WRITE,
         /* dwShareMode */ FILE_SHARE_READ,
         /* lpSecurityAttributes */ NULL,
@@ -1128,10 +1107,10 @@
       }
       Sleep(/* dwMilliseconds */ 200);
     } else {
+      string err = GetLastErrorString();
       BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
-          << "AcquireLock(" << lockfile << "): CreateFileW("
-          << blaze_util::WstringToString(wlockfile)
-          << ") failed: " << GetLastErrorString();
+          << "AcquireLock(" << lockfile.AsPrintablePath()
+          << "): CreateFile failed: " << err;
     }
   }
   uint64_t wait_time = GetMillisecondsMonotonic() - st;
@@ -1145,10 +1124,10 @@
           /* nNumberOfBytesToLockLow */ 1,
           /* nNumberOfBytesToLockHigh */ 0,
           /* lpOverlapped */ &overlapped)) {
+    string err = GetLastErrorString();
     BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
-        << "AcquireLock(" << lockfile << "): LockFileEx("
-        << blaze_util::WstringToString(wlockfile)
-        << ") failed: " << GetLastErrorString();
+        << "AcquireLock(" << lockfile.AsPrintablePath()
+        << "): LockFileEx failed: " << err;
   }
   // On other platforms we write some info about this process into the lock file
   // such as the server PID. On Windows we don't do that because the file is
diff --git a/src/main/cpp/server_process_info.cc b/src/main/cpp/server_process_info.cc
index ed8fa41..c854e58 100644
--- a/src/main/cpp/server_process_info.cc
+++ b/src/main/cpp/server_process_info.cc
@@ -18,19 +18,19 @@
 
 namespace blaze {
 
-static std::string GetJvmOutFile(
-    const std::string &output_base, const std::string &server_jvm_out) {
-  if (!server_jvm_out.empty()) {
+static blaze_util::Path GetJvmOutFile(const blaze_util::Path &output_base,
+                                      const blaze_util::Path &server_jvm_out) {
+  if (!server_jvm_out.IsEmpty()) {
     return server_jvm_out;
   } else {
-    return blaze_util::JoinPath(output_base, "server/jvm.out");
+    return output_base.GetRelative("server/jvm.out");
   }
 }
 
-ServerProcessInfo::ServerProcessInfo(
-    const std::string &output_base, const std::string &server_jvm_out)
+ServerProcessInfo::ServerProcessInfo(const blaze_util::Path &output_base,
+                                     const blaze_util::Path &server_jvm_out)
     : jvm_log_file_(GetJvmOutFile(output_base, server_jvm_out)),
-      jvm_log_file_append_(!server_jvm_out.empty()),
+      jvm_log_file_append_(!server_jvm_out.IsEmpty()),
       // TODO(b/69972303): Formalize the "no server" magic value or rm it.
       server_pid_(-1) {}
 
diff --git a/src/main/cpp/server_process_info.h b/src/main/cpp/server_process_info.h
index 1a1813a..9fa69de 100644
--- a/src/main/cpp/server_process_info.h
+++ b/src/main/cpp/server_process_info.h
@@ -16,9 +16,11 @@
 #define BAZEL_SRC_MAIN_CPP_SERVER_PROCESS_INFO_H_
 
 #include <sys/types.h>
+
 #include <string>
 #include <vector>
 
+#include "src/main/cpp/util/path.h"
 #include "src/main/cpp/util/port.h"  // pid_t on Windows/MSVC
 
 namespace blaze {
@@ -27,12 +29,12 @@
 // configuration.
 class ServerProcessInfo final {
  public:
-  ServerProcessInfo(
-      const std::string &output_base, const std::string &server_jvm_out);
+  ServerProcessInfo(const blaze_util::Path &output_base,
+                    const blaze_util::Path &server_jvm_out);
 
   // When running as a daemon, where the deamonized server's stdout and stderr
   // should be written.
-  const std::string jvm_log_file_;
+  const blaze_util::Path jvm_log_file_;
 
   // Whether or not the jvm_log_file should be opened with O_APPEND.
   const bool jvm_log_file_append_;
diff --git a/src/main/cpp/startup_options.cc b/src/main/cpp/startup_options.cc
index 884feee..ce7dfd7 100644
--- a/src/main/cpp/startup_options.cc
+++ b/src/main/cpp/startup_options.cc
@@ -252,7 +252,7 @@
   }
 
   if ((value = GetUnaryOption(arg, next_arg, "--output_base")) != NULL) {
-    output_base = blaze::AbsolutePathFromFlag(value);
+    output_base = blaze_util::Path(blaze::AbsolutePathFromFlag(value));
     option_sources["output_base"] = rcfile;
   } else if ((value = GetUnaryOption(arg, next_arg,
                                      "--install_base")) != NULL) {
@@ -264,7 +264,7 @@
     option_sources["output_user_root"] = rcfile;
   } else if ((value = GetUnaryOption(arg, next_arg,
                                      "--server_jvm_out")) != NULL) {
-    server_jvm_out = blaze::AbsolutePathFromFlag(value);
+    server_jvm_out = blaze_util::Path(blaze::AbsolutePathFromFlag(value));
     option_sources["server_jvm_out"] = rcfile;
   } else if ((value = GetUnaryOption(arg, next_arg, "--host_jvm_profile")) !=
              NULL) {
@@ -594,12 +594,13 @@
 }
 
 static std::string GetSimpleLogHandlerProps(
-    const std::string &java_log, const std::string &java_logging_formatter) {
+    const blaze_util::Path &java_log,
+    const std::string &java_logging_formatter) {
   return "handlers=com.google.devtools.build.lib.util.SimpleLogHandler\n"
          ".level=INFO\n"
          "com.google.devtools.build.lib.util.SimpleLogHandler.level=INFO\n"
          "com.google.devtools.build.lib.util.SimpleLogHandler.prefix=" +
-         java_log +
+         java_log.AsJvmArgument() +
          "\n"
          "com.google.devtools.build.lib.util.SimpleLogHandler.limit=1024000\n"
          "com.google.devtools.build.lib.util.SimpleLogHandler.total_limit="
@@ -610,17 +611,18 @@
 
 void StartupOptions::AddJVMLoggingArguments(std::vector<string> *result) const {
   // Configure logging
-  const string propFile = blaze_util::PathAsJvmFlag(
-      blaze_util::JoinPath(output_base, "javalog.properties"));
-  const string java_log(
-      blaze_util::PathAsJvmFlag(blaze_util::JoinPath(output_base, "java.log")));
+  const blaze_util::Path propFile =
+      output_base.GetRelative("javalog.properties");
+  const blaze_util::Path java_log = output_base.GetRelative("java.log");
   const std::string loggingProps =
       GetSimpleLogHandlerProps(java_log, java_logging_formatter);
 
   if (!blaze_util::WriteFile(loggingProps, propFile)) {
-    perror(("Couldn't write logging file " + propFile).c_str());
+    perror(
+        ("Couldn't write logging file " + propFile.AsPrintablePath()).c_str());
   } else {
-    result->push_back("-Djava.util.logging.config.file=" + propFile);
+    result->push_back("-Djava.util.logging.config.file=" +
+                      propFile.AsJvmArgument());
     result->push_back(
         "-Dcom.google.devtools.build.lib.util.LogHandlerQuerier.class="
         "com.google.devtools.build.lib.util.SimpleLogHandler$HandlerQuerier");
diff --git a/src/main/cpp/startup_options.h b/src/main/cpp/startup_options.h
index 1af639e..36fd000 100644
--- a/src/main/cpp/startup_options.h
+++ b/src/main/cpp/startup_options.h
@@ -27,6 +27,7 @@
 #include <vector>
 
 #include "src/main/cpp/util/exit_code.h"
+#include "src/main/cpp/util/path.h"
 
 namespace blaze {
 
@@ -120,11 +121,11 @@
 
   // If supplied, alternate location to write the blaze server's jvm's stdout.
   // Otherwise a default path in the output base is used.
-  std::string server_jvm_out;
+  blaze_util::Path server_jvm_out;
 
   // Blaze's output base.  Everything is relative to this.  See
   // the BlazeDirectories Java class for details.
-  std::string output_base;
+  blaze_util::Path output_base;
 
   // Installation base for a specific release installation.
   std::string install_base;
diff --git a/src/main/cpp/util/file_platform.h b/src/main/cpp/util/file_platform.h
index 25dc856..bdf8d07 100644
--- a/src/main/cpp/util/file_platform.h
+++ b/src/main/cpp/util/file_platform.h
@@ -143,7 +143,7 @@
 // Reads which directory a symlink points to. Puts the target of the symlink
 // in ``result`` and returns if the operation was successful. Will not work on
 // symlinks that don't point to directories on Windows.
-bool ReadDirectorySymlink(const std::string &symlink, std::string *result);
+bool ReadDirectorySymlink(const blaze_util::Path &symlink, std::string *result);
 
 // Unlinks the file given by 'file_path'.
 // Returns true on success. In case of failure sets errno.
@@ -152,6 +152,7 @@
 
 // Returns true if this path exists, following symlinks.
 bool PathExists(const std::string& path);
+bool PathExists(const Path &path);
 
 // Returns the real, absolute path corresponding to `path`.
 // The method resolves all symlink components of `path`.
@@ -172,9 +173,11 @@
 // is both readable and writable.
 // Follows symlinks/junctions.
 bool CanAccessDirectory(const std::string &path);
+bool CanAccessDirectory(const Path &path);
 
 // Returns true if `path` refers to a directory or a symlink/junction to one.
 bool IsDirectory(const std::string& path);
+bool IsDirectory(const Path &path);
 
 // Calls fsync() on the file (or directory) specified in 'file_path'.
 // pdie() if syncing fails.
diff --git a/src/main/cpp/util/file_posix.cc b/src/main/cpp/util/file_posix.cc
index 3133345..4340aa7 100644
--- a/src/main/cpp/util/file_posix.cc
+++ b/src/main/cpp/util/file_posix.cc
@@ -261,9 +261,9 @@
   }
 }
 
-bool ReadDirectorySymlink(const string &name, string *result) {
+bool ReadDirectorySymlink(const blaze_util::Path &name, string *result) {
   char buf[PATH_MAX + 1];
-  int len = readlink(name.c_str(), buf, PATH_MAX);
+  int len = readlink(name.AsNativePath().c_str(), buf, PATH_MAX);
   if (len < 0) {
     return false;
   }
@@ -285,6 +285,8 @@
   return access(path.c_str(), F_OK) == 0;
 }
 
+bool PathExists(const Path &path) { return PathExists(path.AsNativePath()); }
+
 string MakeCanonical(const char *path) {
   char *resolved_path = realpath(path, NULL);
   if (resolved_path == NULL) {
@@ -322,11 +324,17 @@
   return IsDirectory(path) && CanAccess(path, true, true, true);
 }
 
+bool CanAccessDirectory(const Path &path) {
+  return CanAccessDirectory(path.AsNativePath());
+}
+
 bool IsDirectory(const string& path) {
   struct stat buf;
   return stat(path.c_str(), &buf) == 0 && S_ISDIR(buf.st_mode);
 }
 
+bool IsDirectory(const Path &path) { return IsDirectory(path.AsNativePath()); }
+
 void SyncFile(const string& path) {
   const char* file_path = path.c_str();
   int fd = open(file_path, O_RDONLY);
diff --git a/src/main/cpp/util/file_windows.cc b/src/main/cpp/util/file_windows.cc
index 9a387bd..a2fc9e2 100644
--- a/src/main/cpp/util/file_windows.cc
+++ b/src/main/cpp/util/file_windows.cc
@@ -458,39 +458,25 @@
   }
 }
 
-bool ReadDirectorySymlink(const string& name, string* result) {
-  wstring wname;
-  string error;
-  if (!AsAbsoluteWindowsPath(name, &wname, &error)) {
-    BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
-        << "ReadDirectorySymlink(" << name
-        << "): AsAbsoluteWindowsPath failed: " << error;
-    return false;
-  }
+bool ReadDirectorySymlink(const blaze_util::Path& name, string* result) {
   unique_ptr<WCHAR[]> result_ptr;
-  if (!RealPath(wname.c_str(), &result_ptr)) {
+  if (!RealPath(name.AsNativePath().c_str(), &result_ptr)) {
     return false;
   }
   *result = WstringToCstring(RemoveUncPrefixMaybe(result_ptr.get())).get();
   return true;
 }
 
-bool PathExists(const string& path) {
-  if (path.empty()) {
+bool PathExists(const string& path) { return PathExists(Path(path)); }
+
+bool PathExists(const Path& path) {
+  if (path.IsEmpty()) {
     return false;
   }
-  if (IsDevNull(path.c_str())) {
+  if (path.IsNull()) {
     return true;
   }
-  wstring wpath;
-  string error;
-  if (!AsAbsoluteWindowsPath(path, &wpath, &error)) {
-    BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
-        << "PathExists(" << path
-        << "): AsAbsoluteWindowsPath failed: " << error;
-    return false;
-  }
-  return RealPath(wpath.c_str(), nullptr);
+  return RealPath(path.AsNativePath().c_str(), nullptr);
 }
 
 string MakeCanonical(const char* path) {
@@ -561,15 +547,11 @@
 }
 
 bool CanAccessDirectory(const std::string& path) {
-  wstring wpath;
-  string error;
-  if (!AsAbsoluteWindowsPath(path, &wpath, &error)) {
-    BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
-        << "CanAccessDirectory(" << path
-        << "): AsAbsoluteWindowsPath failed: " << error;
-    return false;
-  }
-  DWORD attr = ::GetFileAttributesW(wpath.c_str());
+  return CanAccessDirectory(Path(path));
+}
+
+bool CanAccessDirectory(const Path& path) {
+  DWORD attr = ::GetFileAttributesW(path.AsNativePath().c_str());
   if ((attr == INVALID_FILE_ATTRIBUTES) || !(attr & FILE_ATTRIBUTE_DIRECTORY)) {
     // The path doesn't exist or is not a directory.
     return false;
@@ -577,17 +559,13 @@
 
   // The only easy way to know if a directory is writable is by attempting to
   // open a file for writing in it.
-  wstring dummy_path = wpath + L"\\bazel_directory_access_test";
-
-  // The path may have just became too long for MAX_PATH, so add the UNC prefix
-  // if necessary.
-  AddUncPrefixMaybe(&dummy_path);
+  Path dummy_path = path.GetRelative("bazel_directory_access_test");
 
   // Attempt to open the dummy file for read/write access.
   // If the file happens to exist, no big deal, we won't overwrite it thanks to
   // OPEN_ALWAYS.
   HANDLE handle = ::CreateFileW(
-      /* lpFileName */ dummy_path.c_str(),
+      /* lpFileName */ dummy_path.AsNativePath().c_str(),
       /* dwDesiredAccess */ GENERIC_WRITE | GENERIC_READ,
       /* dwShareMode */ kAllShare,
       /* lpSecurityAttributes */ NULL,
@@ -606,7 +584,7 @@
   if (err != ERROR_ALREADY_EXISTS) {
     // The file didn't exist before, but due to OPEN_ALWAYS we created it just
     // now, so do delete it.
-    ::DeleteFileW(dummy_path.c_str());
+    ::DeleteFileW(dummy_path.AsNativePath().c_str());
   }  // Otherwise the file existed before, leave it alone.
   return true;
 }
@@ -623,19 +601,13 @@
          (info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY);
 }
 
-bool IsDirectory(const string& path) {
-  if (path.empty() || IsDevNull(path.c_str())) {
+bool IsDirectory(const string& path) { return IsDirectory(Path(path)); }
+
+bool IsDirectory(const Path& path) {
+  if (path.IsEmpty() || path.IsNull()) {
     return false;
   }
-  wstring wpath;
-  string error;
-  if (!AsAbsoluteWindowsPath(path, &wpath, &error)) {
-    BAZEL_DIE(blaze_exit_code::LOCAL_ENVIRONMENTAL_ERROR)
-        << "IsDirectory(" << path
-        << "): AsAbsoluteWindowsPath failed: " << error;
-    return false;
-  }
-  return IsDirectoryW(wpath);
+  return IsDirectoryW(path.AsNativePath());
 }
 
 void SyncFile(const string& path) {
diff --git a/src/main/cpp/util/path_platform.h b/src/main/cpp/util/path_platform.h
index 27656f7..d1637a7 100644
--- a/src/main/cpp/util/path_platform.h
+++ b/src/main/cpp/util/path_platform.h
@@ -19,19 +19,25 @@
 namespace blaze_util {
 
 // Platform-native, absolute, normalized path.
+// It can be converted to a printable path (for error messages) or to a native
+// path (for API calls).
 class Path {
  public:
   Path() {}
   explicit Path(const std::string &path);
   bool IsEmpty() const { return path_.empty(); }
   bool IsNull() const;
+  bool Contains(const char c) const;
   Path GetRelative(const std::string &r) const;
+  Path Canonicalize() const;
   std::string AsPrintablePath() const;
+  std::string AsJvmArgument() const;
+  std::string AsCommandLineArgument() const;
 
 #if defined(_WIN32) || defined(__CYGWIN__)
-  const std::wstring &AsNativePath() const { return path_; }
+  const std::wstring AsNativePath() const { return path_; }
 #else
-  const std::string &AsNativePath() const { return path_; }
+  const std::string AsNativePath() const { return path_; }
 #endif
 
  private:
diff --git a/src/main/cpp/util/path_posix.cc b/src/main/cpp/util/path_posix.cc
index eed7f64..720a74b 100644
--- a/src/main/cpp/util/path_posix.cc
+++ b/src/main/cpp/util/path_posix.cc
@@ -139,10 +139,20 @@
 
 bool Path::IsNull() const { return path_ == "/dev/null"; }
 
+bool Path::Contains(const char c) const {
+  return path_.find_first_of(c) != std::string::npos;
+}
+
 Path Path::GetRelative(const std::string &r) const {
   return Path(JoinPath(path_, r));
 }
 
+Path Path::Canonicalize() const { return Path(MakeCanonical(path_.c_str())); }
+
 std::string Path::AsPrintablePath() const { return path_; }
 
+std::string Path::AsJvmArgument() const { return path_; }
+
+std::string Path::AsCommandLineArgument() const { return path_; }
+
 }  // namespace blaze_util
diff --git a/src/main/cpp/util/path_windows.cc b/src/main/cpp/util/path_windows.cc
index 8621dff..464ccf9 100644
--- a/src/main/cpp/util/path_windows.cc
+++ b/src/main/cpp/util/path_windows.cc
@@ -490,10 +490,24 @@
   }
 }
 
+Path Path::Canonicalize() const {
+  return Path(MakeCanonical(WstringToString(path_).c_str()));
+}
+
 bool Path::IsNull() const { return path_ == L"NUL"; }
 
+bool Path::Contains(const char c) const {
+  return path_.find_first_of(c) != std::wstring::npos;
+}
+
 std::string Path::AsPrintablePath() const {
   return WstringToCstring(RemoveUncPrefixMaybe(path_.c_str())).get();
 }
 
+std::string Path::AsJvmArgument() const {
+  return PathAsJvmFlag(AsPrintablePath());
+}
+
+std::string Path::AsCommandLineArgument() const { return AsPrintablePath(); }
+
 }  // namespace blaze_util