Remove absolute paths from build command lines

The GN convention is to specify paths in command lines relative to the
build directory. Unfortunately and contrary to pigweed's expectations
this is not what rebase_path(path) does; that outputs an absolute path.

Absolute paths are not desirable in most circumstances as they contain
sources of nondeterminism such as the developer's home directory. Using
them can for example reduce hit rate in build caches.

Replace rebase_path(path) with rebase_path(path, root_build_dir) which
is the correct idiom and matches GN's builtin behavior (e.g. for
sources, include_dirs, etc).

This also removes the --directory argument to python_action(). Changing
the directory during the build while using relative paths is likely to
result in confusion and should be discouraged.

There's a couple more things to do on top of this for identical
binaries between build directories / machines / developers:

- pass options to avoid embedding the working directory
- pass options to use relative paths for the vendored clang & libc++

See [1]-[2] for how to do that.

[1] https://source.chromium.org/chromium/chromium/src/+/main:build/config/compiler/BUILD.gn;l=1170-1239;drc=ab531c265c533cba1c2f6d8240cc0bf7679f605a
[2] https://cs.opensource.google/fuchsia/fuchsia/+/main:build/config/BUILD.gn;l=145-216;drc=f6d705f0937c778d5d5f807a4580113612b02f5a

Change-Id: I17708102c03d6488d68c8571b6e9343191fd47de
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/47461
Reviewed-by: Wyatt Hepler <hepler@google.com>
Commit-Queue: Michael Spang <spang@google.com>
diff --git a/docs/module_structure.rst b/docs/module_structure.rst
index dc2e4b0..e22bf4e 100644
--- a/docs/module_structure.rst
+++ b/docs/module_structure.rst
@@ -320,7 +320,7 @@
   config("set_options_in_header_file") {
     cflags = [
       "-include",
-      rebase_path("my_config_overrides.h"),
+      rebase_path("my_config_overrides.h", root_build_dir),
     ]
   }
 
diff --git a/pw_arduino_build/arduino.gni b/pw_arduino_build/arduino.gni
index 0d1704c..f35836d 100644
--- a/pw_arduino_build/arduino.gni
+++ b/pw_arduino_build/arduino.gni
@@ -46,7 +46,8 @@
              _required_args_message)
 
   _arduino_selected_core_path =
-      rebase_path("$pw_arduino_build_CORE_PATH/$pw_arduino_build_CORE_NAME")
+      rebase_path("$pw_arduino_build_CORE_PATH/$pw_arduino_build_CORE_NAME",
+                  root_build_dir)
 
   arduino_builder_script =
       get_path_info("py/pw_arduino_build/__main__.py", "abspath")
@@ -60,7 +61,8 @@
              pw_arduino_build_PACKAGE_NAME + " list-boards")
 
   _compiler_path_override =
-      rebase_path(getenv("_PW_ACTUAL_ENVIRONMENT_ROOT") + "/cipd/pigweed/bin")
+      rebase_path(getenv("_PW_ACTUAL_ENVIRONMENT_ROOT") + "/cipd/pigweed/bin",
+                  root_build_dir)
 
   arduino_core_library_path = "$_arduino_selected_core_path/hardware/" +
                               "$pw_arduino_build_PACKAGE_NAME/libraries"
@@ -75,13 +77,13 @@
 
     # Save config files to "out/arduino_debug/gen/arduino_builder_config.json"
     "--config-file",
-    rebase_path(root_gen_dir) + "/arduino_builder_config.json",
+    rebase_path(root_gen_dir, root_build_dir) + "/arduino_builder_config.json",
     "--save-config",
   ]
 
   arduino_board_args = [
     "--build-path",
-    rebase_path(root_build_dir),
+    ".",
     "--board",
     pw_arduino_build_BOARD,
   ]
diff --git a/pw_bloat/bloat.gni b/pw_bloat/bloat.gni
index bb22417..6d79cfb 100644
--- a/pw_bloat/bloat.gni
+++ b/pw_bloat/bloat.gni
@@ -116,9 +116,10 @@
 
       # Allow each binary to override the global bloaty config.
       if (defined(binary.bloaty_config)) {
-        _bloaty_configs += [ rebase_path(binary.bloaty_config) ]
+        _bloaty_configs += [ rebase_path(binary.bloaty_config, root_build_dir) ]
       } else {
-        _bloaty_configs += [ rebase_path(pw_bloat_BLOATY_CONFIG) ]
+        _bloaty_configs +=
+            [ rebase_path(pw_bloat_BLOATY_CONFIG, root_build_dir) ]
       }
 
       _binary_path += ";" + "<TARGET_FILE($_binary_base)>"
@@ -131,7 +132,7 @@
       "--bloaty-config",
       string_join(";", _bloaty_configs),
       "--out-dir",
-      rebase_path(target_gen_dir),
+      rebase_path(target_gen_dir, root_build_dir),
       "--target",
       target_name,
       "--title",
@@ -165,7 +166,7 @@
         }
         script = "$dir_pw_bloat/py/pw_bloat/no_bloaty.py"
         python_deps = [ "$dir_pw_bloat/py" ]
-        args = [ rebase_path(_doc_rst_output) ]
+        args = [ rebase_path(_doc_rst_output, root_build_dir) ]
         outputs = [ _doc_rst_output ]
       }
 
@@ -250,7 +251,8 @@
     _linker_script_target_name = "${_prefix}_linker_script"
     config(_linker_script_target_name) {
       if (defined(_toolchain.linker_script)) {
-        ldflags = [ "-T" + rebase_path(_toolchain.linker_script) ]
+        ldflags =
+            [ "-T" + rebase_path(_toolchain.linker_script, root_build_dir) ]
         inputs = [ _toolchain.linker_script ]
       } else {
         ldflags = []
@@ -327,7 +329,7 @@
       }
       script = "$dir_pw_bloat/py/pw_bloat/no_toolchains.py"
       python_deps = [ "$dir_pw_bloat/py" ]
-      args = [ rebase_path(_doc_rst_output) ]
+      args = [ rebase_path(_doc_rst_output, root_build_dir) ]
       outputs = [ _doc_rst_output ]
     }
   }
diff --git a/pw_build/docs.rst b/pw_build/docs.rst
index db3725d..7bf075b 100644
--- a/pw_build/docs.rst
+++ b/pw_build/docs.rst
@@ -162,8 +162,6 @@
   specifying ``outputs``. If ``stamp`` is true, a generic output file is
   used. If ``stamp`` is a file path, that file is used as a stamp file. Like any
   output file, ``stamp`` must be in the build directory. Defaults to false.
-* ``directory``: Optional path. Change to this directory before executing the
-  command. Paths in arguments may need to be adjusted.
 * ``environment``: Optional list of strings. Environment variables to set,
   passed as NAME=VALUE strings.
 
@@ -208,7 +206,7 @@
 
   ``TARGET_FILE`` only resolves GN target labels to their outputs. To resolve
   paths generally, use the standard GN approach of applying the
-  ``rebase_path(path)`` function. With default arguments, ``rebase_path``
+  ``rebase_path(path, root_build_dir)`` function. This function
   converts the provided GN path or list of paths to be relative to the build
   directory, from which all build commands and scripts are executed.
 
@@ -266,7 +264,7 @@
     script = "py/postprocess_binary.py"
     args = [
       "--database",
-      rebase_path("my/database.csv"),
+      rebase_path("my/database.csv", root_build_dir),
       "--binary=<TARGET_FILE(//firmware/images:main)>",
     ]
     stamp = true
diff --git a/pw_build/error.gni b/pw_build/error.gni
index a7c0e69..653b4a9 100644
--- a/pw_build/error.gni
+++ b/pw_build/error.gni
@@ -41,9 +41,9 @@
       "--message",
       _message,
       "--root",
-      rebase_path("//"),
+      rebase_path("//", root_build_dir),
       "--out",
-      rebase_path(root_build_dir),
+      ".",
     ]
 
     # This output file is never created.
diff --git a/pw_build/exec.gni b/pw_build/exec.gni
index d2b4e69..fc6a5f9 100644
--- a/pw_build/exec.gni
+++ b/pw_build/exec.gni
@@ -72,14 +72,14 @@
   if (defined(invoker.env_file)) {
     _script_args += [
       "--env-file",
-      rebase_path(invoker.env_file),
+      rebase_path(invoker.env_file, root_build_dir),
     ]
   }
 
   if (defined(invoker.args_file)) {
     _script_args += [
       "--args-file",
-      rebase_path(invoker.args_file),
+      rebase_path(invoker.args_file, root_build_dir),
     ]
 
     if (defined(invoker.skip_empty_args) && invoker.skip_empty_args) {
diff --git a/pw_build/go.gni b/pw_build/go.gni
index 49230f2..eadb93b 100644
--- a/pw_build/go.gni
+++ b/pw_build/go.gni
@@ -149,7 +149,7 @@
     args = [
       "build",
       "-o",
-      rebase_path(target_out_dir),
+      rebase_path(target_out_dir, root_build_dir),
       invoker.package,
     ]
     deps = [
diff --git a/pw_build/host_tool.gni b/pw_build/host_tool.gni
index cac3f23..b5a1ffc 100644
--- a/pw_build/host_tool.gni
+++ b/pw_build/host_tool.gni
@@ -34,9 +34,9 @@
       "--src",
       "<TARGET_FILE(${invoker.tool})>",
       "--dst",
-      rebase_path("$root_out_dir/host_tools"),
+      rebase_path("$root_out_dir/host_tools", root_build_dir),
       "--out-root",
-      rebase_path(root_out_dir),
+      rebase_path(root_out_dir, root_build_dir),
     ]
 
     if (defined(invoker.name) && invoker.name != "") {
diff --git a/pw_build/linker_script.gni b/pw_build/linker_script.gni
index 7ba64ac..0cf681b 100644
--- a/pw_build/linker_script.gni
+++ b/pw_build/linker_script.gni
@@ -73,7 +73,7 @@
       # Treat the following file as a C file.
       "-x",
       "c",
-      rebase_path(invoker.linker_script),
+      rebase_path(invoker.linker_script, root_build_dir),
     ]
 
     # Include any explicitly listed c flags.
@@ -91,7 +91,7 @@
     # Set output file.
     args += [
       "-o",
-      rebase_path(_final_linker_script),
+      rebase_path(_final_linker_script, root_build_dir),
     ]
     outputs = [ _final_linker_script ]
   }
@@ -103,7 +103,7 @@
     if (!defined(invoker.ldflags)) {
       ldflags = []
     }
-    ldflags += [ "-T" + rebase_path(_final_linker_script) ]
+    ldflags += [ "-T" + rebase_path(_final_linker_script, root_build_dir) ]
   }
 
   # The target that adds the linker script config to this library and everything
diff --git a/pw_build/mirror_tree.gni b/pw_build/mirror_tree.gni
index 5e78de8..6091738 100644
--- a/pw_build/mirror_tree.gni
+++ b/pw_build/mirror_tree.gni
@@ -47,9 +47,9 @@
 
   _args = [
     "--source-root",
-    rebase_path(_root),
+    rebase_path(_root, root_build_dir),
     "--directory",
-    rebase_path(invoker.directory),
+    rebase_path(invoker.directory, root_build_dir),
   ]
 
   _deps = []
@@ -75,7 +75,8 @@
 
     _deps += [ ":$target_name._path_list" ]
     _args += [ "--path-file" ] +
-             rebase_path(get_target_outputs(":$target_name._path_list"))
+             rebase_path(get_target_outputs(":$target_name._path_list"),
+                         root_build_dir)
   }
 
   pw_python_action(target_name) {
@@ -85,7 +86,7 @@
     outputs = []
 
     if (defined(invoker.sources)) {
-      args += rebase_path(invoker.sources)
+      args += rebase_path(invoker.sources, root_build_dir)
 
       foreach(path, rebase_path(invoker.sources, _root)) {
         outputs += [ "${invoker.directory}/$path" ]
diff --git a/pw_build/py/pw_build/generate_python_package.py b/pw_build/py/pw_build/generate_python_package.py
index e70ce71..2ce4d62 100644
--- a/pw_build/py/pw_build/generate_python_package.py
+++ b/pw_build/py/pw_build/generate_python_package.py
@@ -105,7 +105,8 @@
 
     # Add all non-source files to package data.
     for file in (f for f in files if f.suffix != '.py'):
-        pkg = root / file.parent
+        pkg = file.parent
+
         package_name = pkg.relative_to(root).as_posix().replace('/', '.')
         pkg_data[package_name].add(file.name)
 
diff --git a/pw_build/py/pw_build/python_runner.py b/pw_build/py/pw_build/python_runner.py
index 15ce00e..5c38717 100755
--- a/pw_build/py/pw_build/python_runner.py
+++ b/pw_build/py/pw_build/python_runner.py
@@ -41,20 +41,17 @@
                         type=Path,
                         required=True,
                         help=('Path to the root of the GN tree; '
-                              'value of rebase_path("//")'))
+                              'value of rebase_path("//", root_build_dir)'))
     parser.add_argument('--current-path',
                         type=Path,
                         required=True,
-                        help='Value of rebase_path(".")')
+                        help='Value of rebase_path(".", root_build_dir)')
     parser.add_argument('--default-toolchain',
                         required=True,
                         help='Value of default_toolchain')
     parser.add_argument('--current-toolchain',
                         required=True,
                         help='Value of current_toolchain')
-    parser.add_argument('--directory',
-                        type=Path,
-                        help='Execute the command from this directory')
     parser.add_argument('--module', help='Run this module instead of a script')
     parser.add_argument('--env',
                         action='append',
@@ -164,7 +161,7 @@
 _MAIN_ARTIFACTS = '', '.elf', '.a', '.so', '.dylib', '.exe', '.lib', '.dll'
 
 
-def _get_artifact(build_dir: Path, entries: List[str]) -> _Artifact:
+def _get_artifact(entries: List[str]) -> _Artifact:
     """Attempts to resolve which artifact to use if there are multiple.
 
     Selects artifacts based on extension. This will not work if a toolchain
@@ -173,19 +170,19 @@
     assert entries, "There should be at least one entry here!"
 
     if len(entries) == 1:
-        return _Artifact(build_dir / entries[0], {})
+        return _Artifact(Path(entries[0]), {})
 
     filtered = [p for p in entries if Path(p).suffix in _MAIN_ARTIFACTS]
 
     if len(filtered) == 1:
-        return _Artifact(build_dir / filtered[0], {})
+        return _Artifact(Path(filtered[0]), {})
 
     raise ExpressionError(
         f'Expected 1, but found {len(filtered)} artifacts, after filtering for '
         f'extensions {", ".join(repr(e) for e in _MAIN_ARTIFACTS)}: {entries}')
 
 
-def _parse_build_artifacts(build_dir: Path, fd) -> Iterator[_Artifact]:
+def _parse_build_artifacts(fd) -> Iterator[_Artifact]:
     """Partially parses the build statements in a Ninja file."""
     lines = iter(fd)
 
@@ -212,7 +209,7 @@
         else:
             match = _GN_NINJA_BUILD_STATEMENT.match(line)
             if match:
-                artifact = _get_artifact(build_dir, match.group(1).split())
+                artifact = _get_artifact(match.group(1).split())
 
             line = next_line()
 
@@ -220,7 +217,7 @@
         yield artifact
 
 
-def _search_target_ninja(ninja_file: Path, paths: GnPaths,
+def _search_target_ninja(ninja_file: Path,
                          target: Label) -> Tuple[Optional[Path], List[Path]]:
     """Parses the main output file and object files from <target>.ninja."""
 
@@ -230,16 +227,16 @@
     _LOG.debug('Parsing target Ninja file %s for %s', ninja_file, target)
 
     with ninja_file.open() as fd:
-        for path, variables in _parse_build_artifacts(paths.build, fd):
+        for path, variables in _parse_build_artifacts(fd):
             # Older GN used .stamp files when there is no build artifact.
             if path.suffix == '.stamp':
                 continue
 
             if variables:
                 assert not artifact, f'Multiple artifacts for {target}!'
-                artifact = path
+                artifact = Path(path)
             else:
-                objects.append(path)
+                objects.append(Path(path))
 
     return artifact, objects
 
@@ -271,7 +268,7 @@
                 if line.startswith(statement):
                     output_files = line[len(statement):].strip().split()
                     if len(output_files) == 1:
-                        return paths.build / output_files[0]
+                        return Path(output_files[0])
 
                     break
 
@@ -283,7 +280,7 @@
         target: Label) -> Tuple[bool, Optional[Path], List[Path]]:
     ninja_file = target.out_dir / f'{target.name}.ninja'
     if ninja_file.exists():
-        return (True, *_search_target_ninja(ninja_file, paths, target))
+        return (True, *_search_target_ninja(ninja_file, target))
 
     ninja_file = paths.build / target.toolchain_name() / 'toolchain.ninja'
     if ninja_file.exists():
@@ -366,7 +363,7 @@
         if target.artifact is None:
             raise ExpressionError(f'Target {target} has no output file!')
 
-        if Path(target.artifact).exists():
+        if paths.build.joinpath(target.artifact).exists():
             yield _ArgAction.APPEND, str(target.artifact)
             return
 
@@ -441,7 +438,6 @@
 def main(
     gn_root: Path,
     current_path: Path,
-    directory: Optional[Path],
     original_cmd: List[str],
     default_toolchain: str,
     current_toolchain: str,
@@ -470,7 +466,7 @@
     if module is not None:
         command += ['-m', module]
 
-    run_args: dict = dict(cwd=directory)
+    run_args: dict = dict()
 
     if env is not None:
         environment = os.environ.copy()
diff --git a/pw_build/py/python_runner_test.py b/pw_build/py/python_runner_test.py
index 1ea69b0..f9e188c 100755
--- a/pw_build/py/python_runner_test.py
+++ b/pw_build/py/python_runner_test.py
@@ -194,6 +194,8 @@
         self._tempdir, self._outdir, self._paths = _create_ninja_files(
             NINJA_SOURCE_SET)
 
+        self._rel_outdir = self._outdir.relative_to(self._paths.build)
+
     def tearDown(self):
         self._tempdir.cleanup()
 
@@ -207,22 +209,22 @@
         self.assertTrue(target.generated)
         self.assertEqual(
             set(target.object_files), {
-                self._outdir / 'fake_source_set.file_a.cc.o',
-                self._outdir / 'fake_source_set.file_b.c.o',
+                self._rel_outdir / 'fake_source_set.file_a.cc.o',
+                self._rel_outdir / 'fake_source_set.file_b.c.o',
             })
 
     def test_executable_object_files(self):
         target = TargetInfo(self._paths, '//fake_module:fake_test')
         self.assertEqual(
             set(target.object_files), {
-                self._outdir / 'fake_test.fake_test.cc.o',
-                self._outdir / 'fake_test.fake_test_c.c.o',
+                self._rel_outdir / 'fake_test.fake_test.cc.o',
+                self._rel_outdir / 'fake_test.fake_test_c.c.o',
             })
 
     def test_executable_artifact(self):
         target = TargetInfo(self._paths, '//fake_module:fake_test')
         self.assertEqual(target.artifact,
-                         self._outdir / 'test' / 'fake_test.elf')
+                         self._rel_outdir / 'test' / 'fake_test.elf')
 
     def test_non_existent_target(self):
         target = TargetInfo(self._paths,
@@ -243,6 +245,8 @@
         self._tempdir, self._outdir, self._paths = _create_ninja_files(
             NINJA_SOURCE_SET_STAMP)
 
+        self._rel_outdir = self._outdir.relative_to(self._paths.build)
+
 
 class ExpandExpressionsTest(unittest.TestCase):
     """Tests expansion of expressions like <TARGET_FILE(//foo)>."""
@@ -260,7 +264,7 @@
             path.touch()
         else:
             assert not path.exists()
-        return str(path)
+        return str(path.relative_to(self._paths.build))
 
     def test_empty(self):
         self.assertEqual(list(expand_expressions(self._paths, '')), [''])
@@ -389,6 +393,8 @@
         self._tempdir, self._outdir, self._paths = _create_ninja_files(
             NINJA_SOURCE_SET_STAMP)
 
+        self._rel_outdir = self._outdir.relative_to(self._paths.build)
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/pw_build/python.gni b/pw_build/python.gni
index b7bf71a..217a7f8 100644
--- a/pw_build/python.gni
+++ b/pw_build/python.gni
@@ -64,17 +64,17 @@
     ]
 
     if (defined(invoker.mypy_ini)) {
-      args += [ "--config-file=" + rebase_path(invoker.mypy_ini) ]
+      args +=
+          [ "--config-file=" + rebase_path(invoker.mypy_ini, root_build_dir) ]
       inputs = [ invoker.mypy_ini ]
     }
 
-    args += rebase_path(invoker.sources)
+    args += rebase_path(invoker.sources, root_build_dir)
 
     # Use this environment variable to force mypy to colorize output.
     # See https://github.com/python/mypy/issues/7771
     environment = [ "MYPY_FORCE_COLOR=1" ]
 
-    directory = invoker.directory
     stamp = true
 
     deps = invoker.deps
@@ -92,13 +92,13 @@
   pw_python_action_foreach(target_name) {
     module = "pylint"
     args = [
-      rebase_path(".") + "/{{source_target_relative}}",
+      rebase_path(".", root_build_dir) + "/{{source_target_relative}}",
       "--jobs=1",
       "--output-format=colorized",
     ]
 
     if (defined(invoker.pylintrc)) {
-      args += [ "--rcfile=" + rebase_path(invoker.pylintrc) ]
+      args += [ "--rcfile=" + rebase_path(invoker.pylintrc, root_build_dir) ]
       inputs = [ invoker.pylintrc ]
     }
 
@@ -108,7 +108,6 @@
     }
 
     sources = invoker.sources
-    directory = invoker.directory
 
     stamp = "$target_gen_dir/{{source_target_relative}}.pylint.passed"
 
@@ -151,10 +150,10 @@
 #       generate_setup is required in place of setup if proto_library is used.
 #   static_analysis: List of static analysis tools to run; "*" (default) runs
 #       all tools. The supported tools are "mypy" and "pylint".
-#   pylintrc: Optional path to a pylintrc configuration file to use. If not
-#       provided, Pylint's default rcfile search is used. Pylint is executed
-#       from the package's setup directory, so pylintrc files in that directory
-#       will take precedence over others.
+#   pylintrc: Path to a pylintrc configuration file to use. If not
+#       provided, Pylint's default rcfile search is used. As this may
+#       use the the local user's configuration file, it is highly
+#       recommended to pass this option to specify the rcfile explicitly.
 #   mypy_ini: Optional path to a mypy configuration file to use. If not
 #       provided, mypy's default configuration file search is used. mypy is
 #       executed from the package's setup directory, so mypy.ini files in that
@@ -346,10 +345,10 @@
                  "--label",
                  get_label_info(":$target_name", "label_no_toolchain"),
                  "--generated-root",
-                 rebase_path(_setup_dir),
+                 rebase_path(_setup_dir, root_build_dir),
                  "--setup-json",
-                 rebase_path("$_setup_dir/setup.json"),
-               ] + rebase_path(_sources)
+                 rebase_path("$_setup_dir/setup.json", root_build_dir),
+               ] + rebase_path(_sources, root_build_dir)
 
         # Pass in the .json information files for the imported proto libraries.
         foreach(proto, _import_protos) {
@@ -359,7 +358,7 @@
                   get_label_info(_label, "name") + ".json"
           args += [
             "--proto-library",
-            rebase_path(_file),
+            rebase_path(_file, root_build_dir),
           ]
         }
 
@@ -406,7 +405,7 @@
           args += [ "--editable" ]
         }
 
-        args += [ rebase_path(_setup_dir) ]
+        args += [ rebase_path(_setup_dir, root_build_dir) ]
 
         stamp = true
 
@@ -431,12 +430,13 @@
 
         module = "build"
 
-        args = [
-                 rebase_path(_setup_dir),
-                 "--wheel",
-                 "--no-isolation",
-                 "--outdir",
-               ] + rebase_path(metadata.pw_python_package_wheels)
+        args =
+            [
+              rebase_path(_setup_dir, root_build_dir),
+              "--wheel",
+              "--no-isolation",
+              "--outdir",
+            ] + rebase_path(metadata.pw_python_package_wheels, root_build_dir)
 
         deps = [ ":${invoker.target_name}" ]
         foreach(dep, _python_deps) {
@@ -509,12 +509,6 @@
         deps = _test_install_deps
         python_deps = _python_deps
 
-        if (defined(_setup_dir)) {
-          directory = rebase_path(_setup_dir)
-        } else {
-          directory = rebase_path(".")
-        }
-
         _optional_variables = [
           "mypy_ini",
           "pylintrc",
@@ -729,7 +723,7 @@
     foreach(_requirements_file, inputs) {
       args += [
         "--requirement",
-        rebase_path(_requirements_file),
+        rebase_path(_requirements_file, root_build_dir),
       ]
     }
 
diff --git a/pw_build/python_action.gni b/pw_build/python_action.gni
index fe31801..6ef48a4 100644
--- a/pw_build/python_action.gni
+++ b/pw_build/python_action.gni
@@ -32,10 +32,6 @@
 #                   creating their own placeholder file. If true, a generic file
 #                   is used. If false or not set, no file is touched.
 #
-#   directory       The directory from which to execute the Python script. Paths
-#                   in args may need to be adjusted to be relative to this
-#                   directory.
-#
 #   environment     Environment variables to set, passed as a list of NAME=VALUE
 #                   strings.
 #
@@ -60,23 +56,16 @@
     # GN root directory relative to the build directory (in which the runner
     # script is invoked).
     "--gn-root",
-    rebase_path("//"),
+    rebase_path("//", root_build_dir),
 
     # Current directory, used to resolve relative paths.
     "--current-path",
-    rebase_path("."),
+    rebase_path(".", root_build_dir),
 
     "--default-toolchain=$default_toolchain",
     "--current-toolchain=$current_toolchain",
   ]
 
-  if (defined(invoker.directory)) {
-    _script_args += [
-      "--directory",
-      rebase_path(invoker.directory),
-    ]
-  }
-
   if (defined(invoker.environment)) {
     foreach(variable, invoker.environment) {
       _script_args += [ "--env=$variable" ]
@@ -112,7 +101,7 @@
     _outputs += [ _stamp_file ]
     _script_args += [
       "--touch",
-      rebase_path(_stamp_file),
+      rebase_path(_stamp_file, root_build_dir),
     ]
   }
 
@@ -134,7 +123,7 @@
   _script_args += [ "--" ]
 
   if (defined(invoker.script)) {
-    _script_args += [ rebase_path(invoker.script) ]
+    _script_args += [ rebase_path(invoker.script, root_build_dir) ]
   }
 
   if (defined(invoker.args)) {
diff --git a/pw_build/python_dist.gni b/pw_build/python_dist.gni
index db41d1e..9514b94 100644
--- a/pw_build/python_dist.gni
+++ b/pw_build/python_dist.gni
@@ -58,11 +58,11 @@
 
     args = [
       "--prefix",
-      rebase_path(root_build_dir),
+      rebase_path(root_build_dir, root_build_dir),
       "--suffix",
-      rebase_path(_wheel_paths_path),
+      rebase_path(_wheel_paths_path, root_build_dir),
       "--out_dir",
-      rebase_path("${target_out_dir}/python_wheels"),
+      rebase_path("${target_out_dir}/python_wheels", root_build_dir),
     ]
 
     stamp = true
diff --git a/pw_build/zip.gni b/pw_build/zip.gni
index 653b0cd..60b3aa0 100644
--- a/pw_build/zip.gni
+++ b/pw_build/zip.gni
@@ -94,7 +94,7 @@
     script = "$dir_pw_build/py/pw_build/zip.py"
 
     args = [ "--out_filename" ]
-    args += [ rebase_path(invoker.output) ]
+    args += [ rebase_path(invoker.output, root_build_dir) ]
 
     inputs = []
     args += [ "--input_list" ]
@@ -107,8 +107,8 @@
 
         input_list = []
         input_list = string_split(input, _delimiter)
-        input_list[0] = rebase_path(input_list[0])
         inputs += [ input_list[0] ]
+        input_list[0] = rebase_path(input_list[0], root_build_dir)
 
         # Pass rebased and delimited path to script.
         args += [ string_join(_delimiter, input_list) ]
@@ -122,7 +122,7 @@
         dir = string_replace(dir, " $_delimiter", _delimiter)
         dir = string_replace(dir, "$_delimiter ", _delimiter)
 
-        args += [ rebase_path(dir) ]
+        args += [ rebase_path(dir, root_build_dir) ]
       }
     }
 
diff --git a/pw_docgen/docs.gni b/pw_docgen/docs.gni
index 50bf8d6..5759946 100644
--- a/pw_docgen/docs.gni
+++ b/pw_docgen/docs.gni
@@ -97,18 +97,19 @@
     "--gn-gen-root",
     rebase_path(root_gen_dir, root_build_dir) + "/",
     "--sphinx-build-dir",
-    rebase_path("$target_gen_dir/pw_docgen_tree"),
+    rebase_path("$target_gen_dir/pw_docgen_tree", root_build_dir),
     "--conf",
-    rebase_path(invoker.conf),
+    rebase_path(invoker.conf, root_build_dir),
     "--out-dir",
-    rebase_path(invoker.output_directory),
+    rebase_path(invoker.output_directory, root_build_dir),
     "--metadata",
   ]
 
   # Metadata JSON file path.
-  _script_args += rebase_path(get_target_outputs(":$_metadata_file_target"))
+  _script_args +=
+      rebase_path(get_target_outputs(":$_metadata_file_target"), root_build_dir)
 
-  _script_args += rebase_path(invoker.sources)
+  _script_args += rebase_path(invoker.sources, root_build_dir)
 
   if (pw_docgen_BUILD_DOCS) {
     pw_python_action(target_name) {
diff --git a/pw_hdlc/BUILD.gn b/pw_hdlc/BUILD.gn
index 41e5e4b..7ee98c1 100644
--- a/pw_hdlc/BUILD.gn
+++ b/pw_hdlc/BUILD.gn
@@ -125,7 +125,7 @@
 pw_python_action("generate_decoder_test") {
   outputs = [ "$target_gen_dir/generated_decoder_test.cc" ]
   script = "py/decode_test.py"
-  args = [ "--generate-cc-test" ] + rebase_path(outputs)
+  args = [ "--generate-cc-test" ] + rebase_path(outputs, root_build_dir)
   python_deps = [
     "$dir_pw_build/py",
     "py",
diff --git a/pw_polyfill/BUILD.gn b/pw_polyfill/BUILD.gn
index b173e06..6292b2d 100644
--- a/pw_polyfill/BUILD.gn
+++ b/pw_polyfill/BUILD.gn
@@ -40,7 +40,7 @@
     # without requiring a #include. This allows the use of newer C++ language
     # features in older C++ versions without an explicit include.
     "-include",
-    rebase_path("language_features.h"),
+    rebase_path("language_features.h", root_build_dir),
   ]
   visibility = [ ":*" ]
 }
diff --git a/pw_protobuf_compiler/proto.gni b/pw_protobuf_compiler/proto.gni
index 241c666..cc0cb27 100644
--- a/pw_protobuf_compiler/proto.gni
+++ b/pw_protobuf_compiler/proto.gni
@@ -49,7 +49,8 @@
     }
 
     _includes =
-        rebase_path(get_target_outputs(":${invoker.base_target}._includes"))
+        rebase_path(get_target_outputs(":${invoker.base_target}._includes"),
+                    root_build_dir)
 
     pw_python_action("$target_name._gen") {
       script =
@@ -79,15 +80,16 @@
                "--include-file",
                _includes[0],
                "--compile-dir",
-               rebase_path(invoker.compile_dir),
+               rebase_path(invoker.compile_dir, root_build_dir),
                "--out-dir",
-               rebase_path(_out_dir),
+               rebase_path(_out_dir, root_build_dir),
                "--sources",
-             ] + rebase_path(invoker.sources)
+             ] + rebase_path(invoker.sources, root_build_dir)
 
       if (defined(invoker.plugin)) {
         inputs = [ invoker.plugin ]
-        args += [ "--plugin-path=" + rebase_path(invoker.plugin) ]
+        args +=
+            [ "--plugin-path=" + rebase_path(invoker.plugin, root_build_dir) ]
       }
 
       if (defined(invoker.outputs)) {
@@ -104,8 +106,9 @@
     # Output a .json file with information about this proto library.
     _proto_info = {
       label = get_label_info(":${invoker.target_name}", "label_no_toolchain")
-      protoc_outputs = rebase_path(get_target_outputs(":$target_name._gen"))
-      root = rebase_path(_out_dir)
+      protoc_outputs =
+          rebase_path(get_target_outputs(":$target_name._gen"), root_build_dir)
+      root = rebase_path(_out_dir, root_build_dir)
       package = invoker.package
 
       nested_in_python_package = ""
@@ -118,7 +121,8 @@
       foreach(dep, invoker.deps) {
         dependencies +=
             rebase_path([ get_label_info(dep, "target_gen_dir") + "/" +
-                              get_label_info(dep, "name") + ".json" ])
+                              get_label_info(dep, "name") + ".json" ],
+                        root_build_dir)
       }
     }
     write_file("$target_gen_dir/$target_name.json", _proto_info, "json")
@@ -494,7 +498,7 @@
 
     # Indicate this library's base directory for its dependents.
     metadata = {
-      protoc_includes = [ rebase_path(_common.compile_dir) ]
+      protoc_includes = [ rebase_path(_common.compile_dir, root_build_dir) ]
     }
   }
 
diff --git a/pw_rpc/BUILD.gn b/pw_rpc/BUILD.gn
index 9f4a95f..bc82b77 100644
--- a/pw_rpc/BUILD.gn
+++ b/pw_rpc/BUILD.gn
@@ -232,7 +232,7 @@
   outputs = [ "$target_gen_dir/generated_ids_test.cc" ]
 
   script = "py/tests/ids_test.py"
-  args = [ "--generate-cc-test" ] + rebase_path(outputs)
+  args = [ "--generate-cc-test" ] + rebase_path(outputs, root_build_dir)
   python_deps = [
     "$dir_pw_build/py",
     "py",
diff --git a/pw_tokenizer/BUILD.gn b/pw_tokenizer/BUILD.gn
index 520beb2..2500f00 100644
--- a/pw_tokenizer/BUILD.gn
+++ b/pw_tokenizer/BUILD.gn
@@ -44,7 +44,7 @@
   if (current_os == "") {
     ldflags = [
       "-T",
-      rebase_path("pw_tokenizer_linker_sections.ld"),
+      rebase_path("pw_tokenizer_linker_sections.ld", root_build_dir),
     ]
   } else if (current_os == "linux" && !pw_toolchain_OSS_FUZZ_ENABLED) {
     # When building for Linux, the linker provides a default linker script.
@@ -53,10 +53,11 @@
     # default linker script instead of overriding it.
     ldflags = [
       "-T",
-      rebase_path("add_tokenizer_sections_to_default_script.ld"),
-      "-L",
-      rebase_path("."),
+      rebase_path("add_tokenizer_sections_to_default_script.ld",
+                  root_build_dir),
     ]
+    lib_dirs = [ "." ]
+
     inputs += [ "add_tokenizer_sections_to_default_script.ld" ]
   }
   visibility = [ ":*" ]
diff --git a/pw_tokenizer/database.gni b/pw_tokenizer/database.gni
index 81d6832..2508470 100644
--- a/pw_tokenizer/database.gni
+++ b/pw_tokenizer/database.gni
@@ -119,9 +119,9 @@
 
     args += [
       "--database",
-      rebase_path(_database),
+      rebase_path(_database, root_build_dir),
     ]
-    args += rebase_path(_input_databases)
+    args += rebase_path(_input_databases, root_build_dir)
 
     foreach(target, _targets) {
       args += [ "<TARGET_FILE($target)>$_domain" ]
@@ -134,9 +134,8 @@
     }
 
     if (defined(invoker.optional_paths)) {
-      _paths = rebase_path(invoker.optional_paths)
-      _out_dir = rebase_path(root_build_dir)
-      assert(filter_include(_paths, [ "$_out_dir/*" ]) == _paths,
+      _paths = rebase_path(invoker.optional_paths, root_build_dir)
+      assert(filter_include(_paths, [ "../*" ]) == [],
              "Paths in 'optional_paths' must be in the out directory. Use " +
                  "'input_databases' for files in the source tree.")
       args += _paths
diff --git a/pw_toolchain/arm_clang/clang_config.gni b/pw_toolchain/arm_clang/clang_config.gni
index 2c71a2f..de212b9 100644
--- a/pw_toolchain/arm_clang/clang_config.gni
+++ b/pw_toolchain/arm_clang/clang_config.gni
@@ -12,7 +12,8 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
-_script_path = rebase_path("../py/pw_toolchain/clang_arm_toolchain.py")
+_script_path =
+    get_path_info("../py/pw_toolchain/clang_arm_toolchain.py", "abspath")
 
 # This template generates a config that can be used to target ARM cores using
 # a clang compiler.
diff --git a/pw_toolchain/default/BUILD.gn b/pw_toolchain/default/BUILD.gn
index e961c97..e8ad524 100644
--- a/pw_toolchain/default/BUILD.gn
+++ b/pw_toolchain/default/BUILD.gn
@@ -24,5 +24,6 @@
   # If the user tries to build a target with the default toolchain, run a script
   # printing out the error.
   command = "python " +
-            rebase_path("$dir_pw_toolchain/py/pw_toolchain/bad_toolchain.py")
+            rebase_path("$dir_pw_toolchain/py/pw_toolchain/bad_toolchain.py",
+                        root_build_dir)
 }
diff --git a/pw_toolchain/non_c_toolchain.gni b/pw_toolchain/non_c_toolchain.gni
index 04e7013..53f7bdd 100644
--- a/pw_toolchain/non_c_toolchain.gni
+++ b/pw_toolchain/non_c_toolchain.gni
@@ -41,14 +41,15 @@
   _command = string_join(" ",
                          [
                            "python",
-                           rebase_path("$dir_pw_build/py/pw_build/error.py"),
+                           rebase_path("$dir_pw_build/py/pw_build/error.py",
+                                       root_build_dir),
                            "--message \"$_message\"",
                            "--target",
                            _label,
                            "--root",
-                           rebase_path("//"),
+                           rebase_path("//", root_build_dir),
                            "--out",
-                           rebase_path(root_build_dir),
+                           ".",
                          ])
 
   if (defined(invoker.command)) {
diff --git a/pw_toolchain/py/pw_toolchain/clang_arm_toolchain.py b/pw_toolchain/py/pw_toolchain/clang_arm_toolchain.py
index 7494727..5c0cf61 100644
--- a/pw_toolchain/py/pw_toolchain/clang_arm_toolchain.py
+++ b/pw_toolchain/py/pw_toolchain/clang_arm_toolchain.py
@@ -32,6 +32,7 @@
 
 import argparse
 import sys
+import os
 import subprocess
 
 from pathlib import Path
@@ -88,8 +89,10 @@
 
 def get_compiler_info(cflags: List[str]) -> Dict[str, str]:
     compiler_info: Dict[str, str] = {}
-    compiler_info['gcc_libs_dir'] = str(get_gcc_lib_dir(cflags))
-    compiler_info['sysroot'] = _compiler_info_command('-print-sysroot', cflags)
+    compiler_info['gcc_libs_dir'] = os.path.relpath(
+        str(get_gcc_lib_dir(cflags)), ".")
+    compiler_info['sysroot'] = os.path.relpath(
+        _compiler_info_command('-print-sysroot', cflags), ".")
     compiler_info['version'] = _compiler_info_command('-dumpversion', cflags)
     compiler_info['multi_dir'] = _compiler_info_command(
         '-print-multi-directory', cflags)
diff --git a/pw_toolchain/universal_tools.gni b/pw_toolchain/universal_tools.gni
index 820276d..eafc0e8 100644
--- a/pw_toolchain/universal_tools.gni
+++ b/pw_toolchain/universal_tools.gni
@@ -19,8 +19,8 @@
     cp_command = "cp -af {{source}} {{output}}"
 
     # Use python script in absence of cp command.
-    copy_tool_path =
-        rebase_path(dir_pw_toolchain) + "/py/pw_toolchain/copy_with_metadata.py"
+    copy_tool_path = rebase_path(dir_pw_toolchain, root_build_dir) +
+                     "/py/pw_toolchain/copy_with_metadata.py"
     fallback_command = "python $copy_tool_path {{source}} {{output}}"
 
     command = "cmd /c \"($cp_command > NUL 2>&1) || ($fallback_command)\""
diff --git a/pw_unit_test/py/pw_unit_test/test_runner.py b/pw_unit_test/py/pw_unit_test/test_runner.py
index 0d395ae..52e69b6 100644
--- a/pw_unit_test/py/pw_unit_test/test_runner.py
+++ b/pw_unit_test/py/pw_unit_test/test_runner.py
@@ -23,6 +23,7 @@
 import subprocess
 import sys
 
+from pathlib import Path
 from typing import Dict, Iterable, List, Optional, Sequence, Set, Tuple
 
 import pw_cli.log
@@ -136,7 +137,13 @@
             test_counter = f'Test {idx:{len(total)}}/{total}'
 
             _LOG.info('%s: [ RUN] %s', test_counter, test.name)
-            command = [self._executable, test.file_path, *self._args]
+
+            # Convert POSIX to native directory seperators as GN produces '/'
+            # but the Windows test runner needs '\\'.
+            command = [
+                str(Path(self._executable)),
+                str(Path(test.file_path)), *self._args
+            ]
 
             if self._executable.endswith('.py'):
                 command.insert(0, sys.executable)
diff --git a/pw_unit_test/test.gni b/pw_unit_test/test.gni
index 02d9e26..fc112cf 100644
--- a/pw_unit_test/test.gni
+++ b/pw_unit_test/test.gni
@@ -185,7 +185,7 @@
       python_deps = [ "$dir_pw_cli/py" ]
       args = [
         "--runner",
-        rebase_path(pw_unit_test_AUTOMATIC_RUNNER),
+        rebase_path(pw_unit_test_AUTOMATIC_RUNNER, root_build_dir),
         "--test",
         "<TARGET_FILE(:$_test_to_run)>",
       ]
diff --git a/targets/arduino/BUILD.gn b/targets/arduino/BUILD.gn
index 7217dff..0032273 100644
--- a/targets/arduino/BUILD.gn
+++ b/targets/arduino/BUILD.gn
@@ -32,7 +32,7 @@
   if (current_toolchain != default_toolchain) {
     config("arduino_build") {
       # Debug: Print out arduinobuilder.py args
-      # print(string_join(" ", [rebase_path(arduino_builder_script)] + arduino_show_command_args))
+      # print(string_join(" ", [rebase_path(arduino_builder_script, root_build_dir)] + arduino_show_command_args))
 
       # Run prebuilds
       # TODO(tonymd) This only needs to be run once but it's happening multiple times.
diff --git a/targets/arduino/target_docs.rst b/targets/arduino/target_docs.rst
index a6f6b0b..379da1b 100644
--- a/targets/arduino/target_docs.rst
+++ b/targets/arduino/target_docs.rst
@@ -217,7 +217,7 @@
 
   _library_args = [
     "--library-path",
-    rebase_path(arduino_core_library_path),
+    rebase_path(arduino_core_library_path, root_build_dir),
     "--library-names",
     "Time",
     "Wire",
diff --git a/third_party/nanopb/BUILD.gn b/third_party/nanopb/BUILD.gn
index 35620ac..c718d9f 100644
--- a/third_party/nanopb/BUILD.gn
+++ b/third_party/nanopb/BUILD.gn
@@ -54,7 +54,7 @@
     sources = [ "generate_nanopb_proto.py" ]
     pylintrc = "$dir_pigweed/.pylintrc"
     action = {
-      args = [ rebase_path(dir_pw_third_party_nanopb) ]
+      args = [ rebase_path(dir_pw_third_party_nanopb, root_build_dir) ]
       stamp = true
     }
   }