From ae953da977ba6cafca6f38bfba52814ed339b7de Mon Sep 17 00:00:00 2001 From: Alexey Preobrazhenskiy Date: Thu, 28 Nov 2024 20:33:55 +0100 Subject: [PATCH] feat(runfiles): add support for spaces and newlines in runfiles paths (#2456) Bazel 7.4.0 introduced support for all characters in runfile source and target paths: https://github.com/bazelbuild/bazel/pull/23912 This is a backwards-compatible change, based on a similar change in rules_go: https://github.com/bazel-contrib/rules_go/pull/4136 --- CHANGELOG.md | 1 + python/runfiles/runfiles.py | 25 ++++++++++++++++++------- tests/runfiles/runfiles_test.py | 7 ++++++- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a20057e84c..cc66afa510 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -112,6 +112,7 @@ Other changes: * (providers) Added {obj}`py_runtime_info.site_init_template` and {obj}`PyRuntimeInfo.site_init_template` for specifying the template to use to initialize the interpreter via venv startup hooks. +* (runfiles) (Bazel 7.4+) Added support for spaces and newlines in runfiles paths {#v0-0-0-removed} ### Removed diff --git a/python/runfiles/runfiles.py b/python/runfiles/runfiles.py index 6d47d249b4..ea816c64fd 100644 --- a/python/runfiles/runfiles.py +++ b/python/runfiles/runfiles.py @@ -58,13 +58,24 @@ def _LoadRunfiles(path: str) -> Dict[str, str]: result = {} with open(path, "r") as f: for line in f: - line = line.strip() - if line: - tokens = line.split(" ", 1) - if len(tokens) == 1: - result[line] = line - else: - result[tokens[0]] = tokens[1] + line = line.rstrip("\n") + if line.startswith(" "): + # In lines that start with a space, spaces, newlines, and backslashes are escaped as \s, \n, and \b in + # link and newlines and backslashes are escaped in target. + escaped_link, escaped_target = line[1:].split(" ", maxsplit=1) + link = ( + escaped_link.replace(r"\s", " ") + .replace(r"\n", "\n") + .replace(r"\b", "\\") + ) + target = escaped_target.replace(r"\n", "\n").replace(r"\b", "\\") + else: + link, target = line.split(" ", maxsplit=1) + + if target: + result[link] = target + else: + result[link] = link return result def _GetRunfilesDir(self) -> str: diff --git a/tests/runfiles/runfiles_test.py b/tests/runfiles/runfiles_test.py index 03350f3fff..cf6a70a020 100644 --- a/tests/runfiles/runfiles_test.py +++ b/tests/runfiles/runfiles_test.py @@ -185,10 +185,11 @@ def testFailsToCreateAnyRunfilesBecauseEnvvarsAreNotDefined(self) -> None: def testManifestBasedRlocation(self) -> None: with _MockFile( contents=[ - "Foo/runfile1", + "Foo/runfile1 ", # A trailing whitespace is always present in single entry lines. "Foo/runfile2 C:/Actual Path\\runfile2", "Foo/Bar/runfile3 D:\\the path\\run file 3.txt", "Foo/Bar/Dir E:\\Actual Path\\Directory", + " Foo\\sBar\\bDir\\nNewline/runfile5 F:\\bActual Path\\bwith\\nnewline/runfile5", ] ) as mf: r = runfiles.CreateManifestBased(mf.Path()) @@ -205,6 +206,10 @@ def testManifestBasedRlocation(self) -> None: r.Rlocation("Foo/Bar/Dir/Deeply/Nested/runfile4"), "E:\\Actual Path\\Directory/Deeply/Nested/runfile4", ) + self.assertEqual( + r.Rlocation("Foo Bar\\Dir\nNewline/runfile5"), + "F:\\Actual Path\\with\nnewline/runfile5", + ) self.assertIsNone(r.Rlocation("unknown")) if RunfilesTest.IsWindows(): self.assertEqual(r.Rlocation("c:/foo"), "c:/foo")