Skip to content

Commit 6e4072e

Browse files
committed
Allow all characters in runfile source and target paths
Adds support for spaces and newlines in source and target paths of runfiles symlinks to `build-runfiles` as well as to the Bash, C++, and Java runfiles libraries (the Python runfiles library has moved out of the repo). If a symlink has spaces or newlines in its source path, it is prefixed with a space in the manifest and spaces, newlines, and backslashes in the source path are escaped with `\s`, `\n`, and `\b` respectively. This scheme has been chosen as it has the following properties: 1. There is no change to the format of manifest lines for entries whose source and target paths don't contain a space. This ensures compatibility with existing runfiles libraries. 2. There is even no change to the format of manifest lines for entries whose target path contains spaces but whose source path does not. These manifests previously failed in `build-runfiles`, but were usable on Windows and supported by the official runfiles libraries. This also ensures that the initialization snippet for the Bash runfiles library doesn't need to change, even when used on Unix with a space in the output base path. 3. The scheme is fully reversible and only depends on the source path, which gives runfiles libraries a choice between reversing the escaping when parsing the manifest (C++, Java) or applying the escaping when searching the manifest (Bash). Fixes bazelbuild#4327 RELNOTES: Bazel now supports all characters in the rlocation and target paths of runfiles and can be run from workspaces with a space in their full path. Closes bazelbuild#23331. PiperOrigin-RevId: 683362776 Change-Id: I1eb79217dcd53cef0089d62a7ba477b1d8f52c21 (cherry picked from commit 7407cef)
1 parent caa7e00 commit 6e4072e

File tree

16 files changed

+690
-67
lines changed

16 files changed

+690
-67
lines changed

src/main/java/com/google/devtools/build/lib/analysis/SourceManifestAction.java

+38-3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919

2020
import com.google.common.annotations.VisibleForTesting;
2121
import com.google.common.collect.ImmutableList;
22+
import com.google.common.escape.CharEscaperBuilder;
23+
import com.google.common.escape.Escaper;
2224
import com.google.devtools.build.lib.actions.AbstractAction;
2325
import com.google.devtools.build.lib.actions.ActionExecutionContext;
2426
import com.google.devtools.build.lib.actions.ActionKeyContext;
@@ -61,7 +63,16 @@ public final class SourceManifestAction extends AbstractFileWriteAction
6163
private static final String GUID = "07459553-a3d0-4d37-9d78-18ed942470f4";
6264

6365
private static final Comparator<Map.Entry<PathFragment, Artifact>> ENTRY_COMPARATOR =
64-
(path1, path2) -> path1.getKey().getPathString().compareTo(path2.getKey().getPathString());
66+
Comparator.comparing(path -> path.getKey().getPathString());
67+
private static final Escaper ROOT_RELATIVE_PATH_ESCAPER =
68+
new CharEscaperBuilder()
69+
.addEscape(' ', "\\s")
70+
.addEscape('\n', "\\n")
71+
.addEscape('\\', "\\b")
72+
.toEscaper();
73+
private static final Escaper TARGET_PATH_ESCAPER =
74+
new CharEscaperBuilder().addEscape('\n', "\\n").addEscape('\\', "\\b").toEscaper();
75+
6576
private final Artifact repoMappingManifest;
6677
/**
6778
* Interface for defining manifest formatting and reporting specifics. Implementations must be
@@ -291,6 +302,9 @@ public enum ManifestType implements ManifestWriter {
291302
*
292303
* <p>[rootRelativePath] [resolvingSymlink]
293304
*
305+
* <p>If rootRelativePath contains spaces, then each backslash is replaced with '\b', each space
306+
* is replaced with '\s' and the line is prefixed with a space.
307+
*
294308
* <p>This strategy is suitable for creating an input manifest to a source view tree. Its output
295309
* is a valid input to {@link com.google.devtools.build.lib.analysis.actions.SymlinkTreeAction}.
296310
*/
@@ -301,11 +315,32 @@ public void writeEntry(
301315
PathFragment rootRelativePath,
302316
@Nullable PathFragment symlinkTarget)
303317
throws IOException {
304-
manifestWriter.append(rootRelativePath.getPathString());
318+
String rootRelativePathString = rootRelativePath.getPathString();
319+
// Source paths with spaces require escaping. Target paths with spaces don't as consumers
320+
// are expected to split on the first space. Newlines always need to be escaped.
321+
// Note that if any of these characters are present, then we also need to escape the escape
322+
// character (backslash) in both paths. We avoid doing so if none of the problematic
323+
// characters are present for backwards compatibility with existing runfiles libraries. In
324+
// particular, entries with a source path that contains neither spaces nor newlines and
325+
// target paths that contain both spaces and backslashes require no escaping.
326+
boolean needsEscaping =
327+
rootRelativePathString.indexOf(' ') != -1
328+
|| rootRelativePathString.indexOf('\n') != -1
329+
|| (symlinkTarget != null && symlinkTarget.getPathString().indexOf('\n') != -1);
330+
if (needsEscaping) {
331+
manifestWriter.append(' ');
332+
manifestWriter.append(ROOT_RELATIVE_PATH_ESCAPER.escape(rootRelativePathString));
333+
} else {
334+
manifestWriter.append(rootRelativePathString);
335+
}
305336
// This trailing whitespace is REQUIRED to process the single entry line correctly.
306337
manifestWriter.append(' ');
307338
if (symlinkTarget != null) {
308-
manifestWriter.append(symlinkTarget.getPathString());
339+
if (needsEscaping) {
340+
manifestWriter.append(TARGET_PATH_ESCAPER.escape(symlinkTarget.getPathString()));
341+
} else {
342+
manifestWriter.append(symlinkTarget.getPathString());
343+
}
309344
}
310345
manifestWriter.append('\n');
311346
}

src/main/java/com/google/devtools/build/lib/runtime/BlazeOptionHandler.java

-12
Original file line numberDiff line numberDiff line change
@@ -164,18 +164,6 @@ private DetailedExitCode checkCwdInWorkspace(EventHandler eventHandler) {
164164
}
165165

166166
Path workspacePath = workspace.getWorkspace();
167-
// TODO(kchodorow): Remove this once spaces are supported.
168-
if (workspacePath.getPathString().contains(" ")) {
169-
String message =
170-
runtime.getProductName()
171-
+ " does not currently work properly from paths "
172-
+ "containing spaces ("
173-
+ workspacePath
174-
+ ").";
175-
eventHandler.handle(Event.error(message));
176-
return createDetailedExitCode(message, Code.SPACES_IN_WORKSPACE_PATH);
177-
}
178-
179167
if (workspacePath.getParentDirectory() != null) {
180168
Path doNotBuild =
181169
workspacePath.getParentDirectory().getRelative(BlazeWorkspace.DO_NOT_BUILD_FILE_NAME);

src/main/protobuf/failure_details.proto

+1-1
Original file line numberDiff line numberDiff line change
@@ -566,7 +566,7 @@ message Command {
566566
STARLARK_OPTIONS_PARSE_FAILURE = 10 [(metadata) = { exit_code: 2 }];
567567
ARGUMENTS_NOT_RECOGNIZED = 11 [(metadata) = { exit_code: 2 }];
568568
NOT_IN_WORKSPACE = 12 [(metadata) = { exit_code: 2 }];
569-
SPACES_IN_WORKSPACE_PATH = 13 [(metadata) = { exit_code: 36 }];
569+
reserved 13;
570570
IN_OUTPUT_DIRECTORY = 14 [(metadata) = { exit_code: 2 }];
571571
}
572572

src/main/tools/build-runfiles-windows.cc

+51-17
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,6 @@ wstring GetParentDirFromPath(const wstring& path) {
100100
return path.substr(0, path.find_last_of(L"\\/"));
101101
}
102102

103-
inline void Trim(wstring& str) {
104-
str.erase(0, str.find_first_not_of(' '));
105-
str.erase(str.find_last_not_of(' ') + 1);
106-
}
107-
108103
bool ReadSymlink(const wstring& abs_path, wstring* target, wstring* error) {
109104
switch (bazel::windows::ReadSymlinkOrJunction(abs_path, target, error)) {
110105
case bazel::windows::ReadSymlinkOrJunctionResult::kSuccess:
@@ -129,6 +124,39 @@ bool ReadSymlink(const wstring& abs_path, wstring* target, wstring* error) {
129124
return false;
130125
}
131126

127+
// Replaces \s, \n, and \b with their respective characters.
128+
std::string Unescape(const std::string& path) {
129+
std::string result;
130+
result.reserve(path.size());
131+
for (size_t i = 0; i < path.size(); ++i) {
132+
if (path[i] == '\\' && i + 1 < path.size()) {
133+
switch (path[i + 1]) {
134+
case 's': {
135+
result.push_back(' ');
136+
break;
137+
}
138+
case 'n': {
139+
result.push_back('\n');
140+
break;
141+
}
142+
case 'b': {
143+
result.push_back('\\');
144+
break;
145+
}
146+
default: {
147+
result.push_back(path[i]);
148+
result.push_back(path[i + 1]);
149+
break;
150+
}
151+
}
152+
++i;
153+
} else {
154+
result.push_back(path[i]);
155+
}
156+
}
157+
return result;
158+
}
159+
132160
} // namespace
133161

134162
class RunfilesCreator {
@@ -164,21 +192,27 @@ class RunfilesCreator {
164192
continue;
165193
}
166194

167-
size_t space_pos = line.find_first_of(' ');
168-
wstring wline = blaze_util::CstringToWstring(line);
169-
wstring link, target;
170-
if (space_pos == string::npos) {
171-
link = wline;
172-
target = wstring();
195+
wstring link;
196+
wstring target;
197+
if (!line.empty() && line[0] == ' ') {
198+
// The link path contains escape sequences for spaces and backslashes.
199+
string::size_type idx = line.find(' ', 1);
200+
if (idx == string::npos) {
201+
die(L"Missing separator in manifest line: %hs", line.c_str());
202+
}
203+
std::string link_path = Unescape(line.substr(1, idx - 1));
204+
link = blaze_util::CstringToWstring(link_path);
205+
std::string target_path = Unescape(line.substr(idx + 1));
206+
target = blaze_util::CstringToWstring(target_path);
173207
} else {
174-
link = wline.substr(0, space_pos);
175-
target = wline.substr(space_pos + 1);
208+
string::size_type idx = line.find(' ');
209+
if (idx == string::npos) {
210+
die(L"Missing separator in manifest line: %hs", line.c_str());
211+
}
212+
link = blaze_util::CstringToWstring(line.substr(0, idx));
213+
target = blaze_util::CstringToWstring(line.substr(idx + 1));
176214
}
177215

178-
// Removing leading and trailing spaces
179-
Trim(link);
180-
Trim(target);
181-
182216
// We sometimes need to create empty files under the runfiles tree.
183217
// For example, for python binary, __init__.py is needed under every
184218
// directory. Storing an entry with an empty target indicates we need to

src/main/tools/build-runfiles.cc

+52-8
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,39 @@ struct FileInfo {
103103

104104
typedef std::map<std::string, FileInfo> FileInfoMap;
105105

106+
// Replaces \s, \n, and \b with their respective characters.
107+
std::string Unescape(const std::string &path) {
108+
std::string result;
109+
result.reserve(path.size());
110+
for (size_t i = 0; i < path.size(); ++i) {
111+
if (path[i] == '\\' && i + 1 < path.size()) {
112+
switch (path[i + 1]) {
113+
case 's': {
114+
result.push_back(' ');
115+
break;
116+
}
117+
case 'n': {
118+
result.push_back('\n');
119+
break;
120+
}
121+
case 'b': {
122+
result.push_back('\\');
123+
break;
124+
}
125+
default: {
126+
result.push_back(path[i]);
127+
result.push_back(path[i + 1]);
128+
break;
129+
}
130+
}
131+
++i;
132+
} else {
133+
result.push_back(path[i]);
134+
}
135+
}
136+
return result;
137+
}
138+
106139
class RunfilesCreator {
107140
public:
108141
explicit RunfilesCreator(const std::string &output_base)
@@ -157,15 +190,26 @@ class RunfilesCreator {
157190
if (buf[0] == '/') {
158191
DIE("paths must not be absolute: line %d: '%s'\n", lineno, buf);
159192
}
160-
const char *s = strchr(buf, ' ');
161-
if (!s) {
162-
DIE("missing field delimiter at line %d: '%s'\n", lineno, buf);
163-
} else if (strchr(s+1, ' ')) {
164-
DIE("link or target filename contains space on line %d: '%s'\n",
165-
lineno, buf);
193+
std::string link;
194+
std::string target;
195+
if (buf[0] == ' ') {
196+
// The link path contains escape sequences for spaces and backslashes.
197+
char *s = strchr(buf + 1, ' ');
198+
if (!s) {
199+
DIE("missing field delimiter at line %d: '%s'\n", lineno, buf);
200+
}
201+
link = Unescape(std::string(buf + 1, s));
202+
target = Unescape(s + 1);
203+
} else {
204+
// The line is of the form "foo /target/path", with only a single space
205+
// in the link path.
206+
const char *s = strchr(buf, ' ');
207+
if (!s) {
208+
DIE("missing field delimiter at line %d: '%s'\n", lineno, buf);
209+
}
210+
link = std::string(buf, s - buf);
211+
target = s + 1;
166212
}
167-
std::string link(buf, s-buf);
168-
const char *target = s+1;
169213
if (!allow_relative && target[0] != '\0' && target[0] != '/'
170214
&& target[1] != ':') { // Match Windows paths, e.g. C:\foo or C:/foo.
171215
DIE("expected absolute path at line %d: '%s'\n", lineno, buf);

src/test/java/com/google/devtools/build/lib/analysis/BUILD

+1-1
Original file line numberDiff line numberDiff line change
@@ -340,10 +340,10 @@ java_test(
340340
srcs = ["SourceManifestActionTest.java"],
341341
deps = [
342342
"//src/main/java/com/google/devtools/build/lib/actions:artifacts",
343-
"//src/main/java/com/google/devtools/build/lib/actions:commandline_item",
344343
"//src/main/java/com/google/devtools/build/lib/analysis:analysis_cluster",
345344
"//src/main/java/com/google/devtools/build/lib/collect/nestedset",
346345
"//src/main/java/com/google/devtools/build/lib/util",
346+
"//src/main/java/com/google/devtools/build/lib/util:os",
347347
"//src/main/java/com/google/devtools/build/lib/vfs",
348348
"//src/main/java/com/google/devtools/build/lib/vfs:pathfragment",
349349
"//src/test/java/com/google/devtools/build/lib/actions/util",

src/test/java/com/google/devtools/build/lib/analysis/SourceManifestActionTest.java

+73
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
3131
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
3232
import com.google.devtools.build.lib.util.Fingerprint;
33+
import com.google.devtools.build.lib.util.OS;
3334
import com.google.devtools.build.lib.vfs.Path;
3435
import com.google.devtools.build.lib.vfs.PathFragment;
3536
import com.google.devtools.build.lib.vfs.Root;
@@ -388,6 +389,78 @@ public void testUnresolvedSymlink() throws Exception {
388389
""");
389390
}
390391

392+
@Test
393+
public void testEscaping() throws Exception {
394+
Artifact manifest = getBinArtifactWithNoOwner("manifest1");
395+
396+
ArtifactRoot trivialRoot =
397+
ArtifactRoot.asSourceRoot(Root.fromPath(rootDirectory.getRelative("trivial")));
398+
Path fileWithSpaceAndBackslashPath = scratch.file("trivial/file with sp\\ace", "foo");
399+
Artifact fileWithSpaceAndBackslash =
400+
ActionsTestUtil.createArtifact(trivialRoot, fileWithSpaceAndBackslashPath);
401+
Path fileWithNewlineAndBackslashPath = scratch.file("trivial/file\nwith\\newline", "foo");
402+
Artifact fileWithNewlineAndBackslash =
403+
ActionsTestUtil.createArtifact(trivialRoot, fileWithNewlineAndBackslashPath);
404+
405+
SourceManifestAction action =
406+
new SourceManifestAction(
407+
ManifestType.SOURCE_SYMLINKS,
408+
NULL_ACTION_OWNER,
409+
manifest,
410+
new Runfiles.Builder("TESTING", false)
411+
.addSymlink(PathFragment.create("no/sp\\ace"), buildFile)
412+
.addSymlink(PathFragment.create("also/no/sp\\ace"), fileWithSpaceAndBackslash)
413+
.addSymlink(PathFragment.create("still/no/sp\\ace"), fileWithNewlineAndBackslash)
414+
.addSymlink(PathFragment.create("with sp\\ace"), buildFile)
415+
.addSymlink(PathFragment.create("also/with sp\\ace"), fileWithSpaceAndBackslash)
416+
.addSymlink(PathFragment.create("more/with sp\\ace"), fileWithNewlineAndBackslash)
417+
.addSymlink(PathFragment.create("with\nnew\\line"), buildFile)
418+
.addSymlink(PathFragment.create("also/with\nnewline"), fileWithSpaceAndBackslash)
419+
.addSymlink(PathFragment.create("more/with\nnewline"), fileWithNewlineAndBackslash)
420+
.addSymlink(PathFragment.create("with\nnew\\line and space"), buildFile)
421+
.addSymlink(
422+
PathFragment.create("also/with\nnewline and space"), fileWithSpaceAndBackslash)
423+
.addSymlink(
424+
PathFragment.create("more/with\nnewline and space"),
425+
fileWithNewlineAndBackslash)
426+
.build());
427+
if (OS.getCurrent().equals(OS.WINDOWS)) {
428+
assertThat(action.getFileContents(reporter))
429+
.isEqualTo(
430+
"""
431+
TESTING/also/no/sp/ace /workspace/trivial/file with sp/ace
432+
TESTING/also/with\\nnewline /workspace/trivial/file with sp/ace
433+
TESTING/also/with\\nnewline\\sand\\sspace /workspace/trivial/file with sp/ace
434+
TESTING/also/with\\ssp/ace /workspace/trivial/file with sp/ace
435+
TESTING/more/with\\nnewline /workspace/trivial/file\\nwith/newline
436+
TESTING/more/with\\nnewline\\sand\\sspace /workspace/trivial/file\\nwith/newline
437+
TESTING/more/with\\ssp/ace /workspace/trivial/file\\nwith/newline
438+
TESTING/no/sp/ace /workspace/trivial/BUILD
439+
TESTING/still/no/sp/ace /workspace/trivial/file\\nwith/newline
440+
TESTING/with\\nnew/line /workspace/trivial/BUILD
441+
TESTING/with\\nnew/line\\sand\\sspace /workspace/trivial/BUILD
442+
TESTING/with\\ssp/ace /workspace/trivial/BUILD
443+
""");
444+
} else {
445+
assertThat(action.getFileContents(reporter))
446+
.isEqualTo(
447+
"""
448+
TESTING/also/no/sp\\ace /workspace/trivial/file with sp\\ace
449+
TESTING/also/with\\nnewline /workspace/trivial/file with sp\\bace
450+
TESTING/also/with\\nnewline\\sand\\sspace /workspace/trivial/file with sp\\bace
451+
TESTING/also/with\\ssp\\bace /workspace/trivial/file with sp\\bace
452+
TESTING/more/with\\nnewline /workspace/trivial/file\\nwith\\bnewline
453+
TESTING/more/with\\nnewline\\sand\\sspace /workspace/trivial/file\\nwith\\bnewline
454+
TESTING/more/with\\ssp\\bace /workspace/trivial/file\\nwith\\bnewline
455+
TESTING/no/sp\\ace /workspace/trivial/BUILD
456+
TESTING/still/no/sp\\bace /workspace/trivial/file\\nwith\\bnewline
457+
TESTING/with\\nnew\\bline /workspace/trivial/BUILD
458+
TESTING/with\\nnew\\bline\\sand\\sspace /workspace/trivial/BUILD
459+
TESTING/with\\ssp\\bace /workspace/trivial/BUILD
460+
""");
461+
}
462+
}
463+
391464
private String computeKey(SourceManifestAction action) {
392465
Fingerprint fp = new Fingerprint();
393466
action.computeKey(actionKeyContext, /* artifactExpander= */ null, fp);

src/test/shell/bazel/bazel_determinism_test.sh

+2-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ function hash_outputs() {
6161
}
6262

6363
function test_determinism() {
64-
local workdir="${TEST_TMPDIR}/workdir"
64+
# Verify that Bazel can build itself under a path with spaces.
65+
local workdir="${TEST_TMPDIR}/work dir"
6566
mkdir "${workdir}" || fail "Could not create work directory"
6667
cd "${workdir}" || fail "Could not change to work directory"
6768
unzip -q "${DISTFILE}"

src/test/shell/bazel/workspace_test.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ function test_path_with_spaces() {
7373
cd "$ws"
7474
create_workspace_with_default_repos WORKSPACE
7575

76-
bazel info &> $TEST_log && fail "Info succeeeded"
76+
bazel info &> $TEST_log || fail "Info failed"
7777
bazel help &> $TEST_log || fail "Help failed"
7878
}
7979

0 commit comments

Comments
 (0)