Skip to content

Commit 356a321

Browse files
fmeumbazel-io
authored andcommitted
Use backslashes in executable paths when remotely executing on Windows
`.bat` scripts can only be executed on Windows when their paths use backslashes, not forward slashes. For this reason, local execution carefully replaces forward slashes with backslashes in the executable of a `Spawn` when executing on Windows. This commit adds equivalent logic for the case of remote execution on Windows from any host: * Remote execution replaces forward slashes with backslashes in the first argument when executing on Windows. * Most calls to `PathFragment#getCallablePathString` are replaced with the new execution platform aware `getCallablePathStringForOs`. This affects test actions, in which various wrappers execute different executables (e.g. `--run_under` targets) and thus can have Bazel-controlled executable path strings in locations other than the `argv[0]`. Fixes #11636 Closes #23986. PiperOrigin-RevId: 689357323 Change-Id: Ifb842babc02c6d741d3b45914a5bf5c032204e2b
1 parent 1faeb68 commit 356a321

File tree

15 files changed

+150
-30
lines changed

15 files changed

+150
-30
lines changed

src/main/java/com/google/devtools/build/lib/analysis/constraints/ConstraintConstants.java

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
// limitations under the License.
1414
package com.google.devtools.build.lib.analysis.constraints;
1515

16-
import com.google.common.collect.ImmutableMap;
16+
import com.google.common.collect.ImmutableBiMap;
17+
import com.google.devtools.build.lib.analysis.platform.ConstraintCollection;
1718
import com.google.devtools.build.lib.analysis.platform.ConstraintSettingInfo;
1819
import com.google.devtools.build.lib.analysis.platform.ConstraintValueInfo;
1920
import com.google.devtools.build.lib.cmdline.Label;
@@ -29,8 +30,12 @@ public final class ConstraintConstants {
2930
Label.parseCanonicalUnchecked("@platforms//os:os"));
3031

3132
// Standard mapping between OS and the corresponding platform constraints.
32-
public static final ImmutableMap<OS, ConstraintValueInfo> OS_TO_CONSTRAINTS =
33-
ImmutableMap.<OS, ConstraintValueInfo>builder()
33+
public static final ImmutableBiMap<OS, ConstraintValueInfo> OS_TO_CONSTRAINTS =
34+
ImmutableBiMap.<OS, ConstraintValueInfo>builder()
35+
.put(
36+
OS.LINUX,
37+
ConstraintValueInfo.create(
38+
OS_CONSTRAINT_SETTING, Label.parseCanonicalUnchecked("@platforms//os:linux")))
3439
.put(
3540
OS.DARWIN,
3641
ConstraintValueInfo.create(
@@ -58,6 +63,19 @@ public final class ConstraintConstants {
5863
Label.parseCanonicalUnchecked("@platforms//os:none")))
5964
.buildOrThrow();
6065

66+
/**
67+
* Returns the OS corresponding to the given constraint collection based on the contained platform
68+
* constraint.
69+
*/
70+
public static OS getOsFromConstraints(ConstraintCollection constraintCollection) {
71+
if (!constraintCollection.has(OS_CONSTRAINT_SETTING)) {
72+
return OS.getCurrent();
73+
}
74+
return OS_TO_CONSTRAINTS
75+
.inverse()
76+
.getOrDefault(constraintCollection.get(OS_CONSTRAINT_SETTING), OS.getCurrent());
77+
}
78+
6179
// No-op constructor to keep this from being instantiated.
6280
private ConstraintConstants() {}
6381
}

src/main/java/com/google/devtools/build/lib/analysis/test/TestActionBuilder.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -433,8 +433,7 @@ private TestParams createTestAction()
433433
run,
434434
config,
435435
ruleContext.getWorkspaceName(),
436-
(!isUsingTestWrapperInsteadOfTestSetupScript
437-
|| executionSettings.needsShell(ruleContext.isExecutedOnWindows()))
436+
(!isUsingTestWrapperInsteadOfTestSetupScript || executionSettings.needsShell())
438437
? ShToolchain.getPathForPlatform(
439438
ruleContext.getConfiguration(), ruleContext.getExecutionPlatform())
440439
: null,
@@ -447,8 +446,7 @@ private TestParams createTestAction()
447446
MoreObjects.firstNonNull(
448447
Allowlist.fetchPackageSpecificationProviderOrNull(
449448
ruleContext, "external_network"),
450-
PackageSpecificationProvider.EMPTY),
451-
ruleContext.isExecutedOnWindows());
449+
PackageSpecificationProvider.EMPTY));
452450

453451
testOutputs.addAll(testRunnerAction.getSpawnOutputs());
454452
testOutputs.addAll(testRunnerAction.getOutputs());

src/main/java/com/google/devtools/build/lib/analysis/test/TestRunnerAction.java

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,6 @@ public class TestRunnerAction extends AbstractAction
147147
private final int runNumber;
148148
private final String workspaceName;
149149

150-
private final boolean isExecutedOnWindows;
151-
152150
/**
153151
* Cached test result status used to minimize disk accesses. This field is set when test status is
154152
* retrieved from disk or saved to disk. This field is null if it has not been set yet. This field
@@ -216,8 +214,7 @@ private static ImmutableSet<Artifact> nonNullAsSet(Artifact... artifacts) {
216214
boolean splitCoveragePostProcessing,
217215
NestedSetBuilder<Artifact> lcovMergerFilesToRun,
218216
@Nullable Artifact lcovMergerRunfilesMiddleman,
219-
PackageSpecificationProvider networkAllowlist,
220-
boolean isExecutedOnWindows) {
217+
PackageSpecificationProvider networkAllowlist) {
221218
super(
222219
owner,
223220
inputs,
@@ -308,8 +305,6 @@ private static ImmutableSet<Artifact> nonNullAsSet(Artifact... artifacts) {
308305
getUndeclaredOutputsDir(),
309306
undeclaredOutputsAnnotationsDir,
310307
baseDir.getRelative("test_attempts"));
311-
312-
this.isExecutedOnWindows = isExecutedOnWindows;
313308
}
314309

315310
public boolean allowLocalTests() {
@@ -326,10 +321,6 @@ public boolean mayModifySpawnOutputsAfterExecution() {
326321
return true;
327322
}
328323

329-
public boolean isExecutedOnWindows() {
330-
return isExecutedOnWindows;
331-
}
332-
333324
public Artifact getRunfilesMiddleman() {
334325
return runfilesMiddleman;
335326
}
@@ -728,7 +719,10 @@ public void setupEnvVariables(Map<String, String> env, Duration timeout) {
728719
env.put("TEST_WORKSPACE", getRunfilesPrefix());
729720
env.put(
730721
"TEST_BINARY",
731-
getExecutionSettings().getExecutable().getRunfilesPath().getCallablePathString());
722+
getExecutionSettings()
723+
.getExecutable()
724+
.getRunfilesPath()
725+
.getCallablePathStringForOs(executionSettings.getExecutionOs()));
732726

733727
// When we run test multiple times, set different TEST_RANDOM_SEED values for each run.
734728
// Don't override any previous setting.

src/main/java/com/google/devtools/build/lib/analysis/test/TestStrategy.java

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import com.google.devtools.build.lib.server.FailureDetails.TestAction.Code;
4646
import com.google.devtools.build.lib.shell.TerminationStatus;
4747
import com.google.devtools.build.lib.util.Fingerprint;
48+
import com.google.devtools.build.lib.util.OS;
4849
import com.google.devtools.build.lib.util.io.OutErr;
4950
import com.google.devtools.build.lib.vfs.Path;
5051
import com.google.devtools.build.lib.vfs.PathFragment;
@@ -211,12 +212,17 @@ public static ImmutableList<String> getArgs(TestRunnerAction testAction)
211212
public static ImmutableList<String> expandedArgsFromAction(TestRunnerAction testAction)
212213
throws CommandLineExpansionException, InterruptedException {
213214
List<String> args = Lists.newArrayList();
215+
OS executionOs = testAction.getExecutionSettings().getExecutionOs();
214216

215217
Artifact testSetup = testAction.getTestSetupScript();
216-
args.add(testSetup.getExecPath().getCallablePathString());
218+
args.add(testSetup.getExecPath().getCallablePathStringForOs(executionOs));
217219

218220
if (testAction.isCoverageMode()) {
219-
args.add(testAction.getCollectCoverageScript().getExecPathString());
221+
args.add(
222+
testAction
223+
.getCollectCoverageScript()
224+
.getExecPath()
225+
.getCallablePathStringForOs(executionOs));
220226
}
221227

222228
TestTargetExecutionSettings execSettings = testAction.getExecutionSettings();
@@ -227,21 +233,28 @@ public static ImmutableList<String> expandedArgsFromAction(TestRunnerAction test
227233
}
228234

229235
// Execute the test using the alias in the runfiles tree, as mandated by the Test Encyclopedia.
236+
// Do not use getCallablePathStringForOs as tw.exe expects a path with forward slashes.
230237
args.add(execSettings.getExecutable().getRunfilesPath().getCallablePathString());
231238
Iterables.addAll(args, execSettings.getArgs().arguments());
232239
return ImmutableList.copyOf(args);
233240
}
234241

235242
private static void addRunUnderArgs(TestRunnerAction testAction, List<String> args) {
236243
TestTargetExecutionSettings execSettings = testAction.getExecutionSettings();
244+
OS executionOs = execSettings.getExecutionOs();
237245
if (execSettings.getRunUnderExecutable() != null) {
238-
args.add(execSettings.getRunUnderExecutable().getRunfilesPath().getCallablePathString());
246+
args.add(
247+
execSettings
248+
.getRunUnderExecutable()
249+
.getRunfilesPath()
250+
.getCallablePathStringForOs(executionOs));
239251
} else {
240-
if (execSettings.needsShell(testAction.isExecutedOnWindows())) {
252+
if (execSettings.needsShell()) {
241253
// TestActionBuilder constructs TestRunnerAction with a 'null' shell only when none is
242254
// required. Something clearly went wrong.
243255
Preconditions.checkNotNull(testAction.getShExecutableMaybe(), "%s", testAction);
244-
String shellExecutable = testAction.getShExecutableMaybe().getPathString();
256+
String shellExecutable =
257+
testAction.getShExecutableMaybe().getCallablePathStringForOs(executionOs);
245258
args.add(shellExecutable);
246259
args.add("-c");
247260
args.add("\"$@\"");

src/main/java/com/google/devtools/build/lib/analysis/test/TestTargetExecutionSettings.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@
2525
import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
2626
import com.google.devtools.build.lib.analysis.config.BuildConfigurationValue;
2727
import com.google.devtools.build.lib.analysis.config.RunUnder;
28+
import com.google.devtools.build.lib.analysis.constraints.ConstraintConstants;
2829
import com.google.devtools.build.lib.packages.TargetUtils;
30+
import com.google.devtools.build.lib.util.OS;
2931
import com.google.devtools.build.lib.vfs.Path;
3032
import javax.annotation.Nullable;
3133

@@ -48,6 +50,7 @@ public final class TestTargetExecutionSettings {
4850
private final Artifact runfilesInputManifest;
4951
private final Artifact instrumentedFileManifest;
5052
private final boolean testRunnerFailFast;
53+
private final OS executionOs;
5154

5255
TestTargetExecutionSettings(
5356
RuleContext ruleContext,
@@ -79,6 +82,8 @@ public final class TestTargetExecutionSettings {
7982
this.runfiles = runfilesSupport.getRunfiles();
8083
this.runfilesInputManifest = runfilesSupport.getRunfilesInputManifest();
8184
this.instrumentedFileManifest = instrumentedFileManifest;
85+
this.executionOs =
86+
ConstraintConstants.getOsFromConstraints(ruleContext.getExecutionPlatform().constraints());
8287
}
8388

8489
@Nullable
@@ -156,7 +161,11 @@ public Artifact getInstrumentedFileManifest() {
156161
return instrumentedFileManifest;
157162
}
158163

159-
public boolean needsShell(boolean executedOnWindows) {
164+
public OS getExecutionOs() {
165+
return executionOs;
166+
}
167+
168+
public boolean needsShell() {
160169
RunUnder r = getRunUnder();
161170
if (r == null) {
162171
return false;
@@ -168,6 +177,6 @@ public boolean needsShell(boolean executedOnWindows) {
168177
// --run_under commands that do not contain '/' are either shell built-ins or need to be
169178
// located on the PATH env, so we wrap them in a shell invocation. Note that we shell-tokenize
170179
// the --run_under parameter and getCommand only returns the first such token.
171-
return !command.contains("/") && (!executedOnWindows || !command.contains("\\"));
180+
return !command.contains("/") && (!executionOs.equals(OS.WINDOWS) || !command.contains("\\"));
172181
}
173182
}

src/main/java/com/google/devtools/build/lib/exec/StandaloneTestStrategy.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -443,7 +443,10 @@ private static Spawn createXmlGeneratingSpawn(
443443
TestRunnerAction action, ImmutableMap<String, String> testEnv, SpawnResult result) {
444444
ImmutableList<String> args =
445445
ImmutableList.of(
446-
action.getTestXmlGeneratorScript().getExecPath().getCallablePathString(),
446+
action
447+
.getTestXmlGeneratorScript()
448+
.getExecPath()
449+
.getCallablePathStringForOs(action.getExecutionSettings().getExecutionOs()),
447450
action.getTestLog().getExecPathString(),
448451
action.getXmlOutputPath().getPathString(),
449452
Integer.toString(result.getWallTimeInMs() / 1000),

src/main/java/com/google/devtools/build/lib/remote/BUILD

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ java_library(
7272
"//src/main/java/com/google/devtools/build/lib/analysis:config/build_options",
7373
"//src/main/java/com/google/devtools/build/lib/analysis:config/core_options",
7474
"//src/main/java/com/google/devtools/build/lib/analysis:configured_target",
75+
"//src/main/java/com/google/devtools/build/lib/analysis:constraints/constraint_constants",
76+
"//src/main/java/com/google/devtools/build/lib/analysis/platform",
7577
"//src/main/java/com/google/devtools/build/lib/analysis/platform:platform_utils",
7678
"//src/main/java/com/google/devtools/build/lib/authandtls",
7779
"//src/main/java/com/google/devtools/build/lib/authandtls/credentialhelper",
@@ -114,11 +116,13 @@ java_library(
114116
"//src/main/java/com/google/devtools/build/lib/util:abrupt_exit_exception",
115117
"//src/main/java/com/google/devtools/build/lib/util:detailed_exit_code",
116118
"//src/main/java/com/google/devtools/build/lib/util:exit_code",
119+
"//src/main/java/com/google/devtools/build/lib/util:os",
117120
"//src/main/java/com/google/devtools/build/lib/util:string",
118121
"//src/main/java/com/google/devtools/build/lib/util:temp_path_generator",
119122
"//src/main/java/com/google/devtools/build/lib/util/io",
120123
"//src/main/java/com/google/devtools/build/lib/util/io:out-err",
121124
"//src/main/java/com/google/devtools/build/lib/vfs",
125+
"//src/main/java/com/google/devtools/build/lib/vfs:ospathpolicy",
122126
"//src/main/java/com/google/devtools/build/lib/vfs:output_service",
123127
"//src/main/java/com/google/devtools/build/lib/vfs:pathfragment",
124128
"//src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs",

src/main/java/com/google/devtools/build/lib/remote/RemoteExecutionService.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@
7474
import com.google.devtools.build.lib.actions.Spawn;
7575
import com.google.devtools.build.lib.actions.SpawnResult;
7676
import com.google.devtools.build.lib.actions.Spawns;
77+
import com.google.devtools.build.lib.analysis.constraints.ConstraintConstants;
78+
import com.google.devtools.build.lib.analysis.platform.PlatformInfo;
7779
import com.google.devtools.build.lib.analysis.platform.PlatformUtils;
7880
import com.google.devtools.build.lib.buildtool.buildevent.BuildCompleteEvent;
7981
import com.google.devtools.build.lib.buildtool.buildevent.BuildInterruptedEvent;
@@ -109,10 +111,12 @@
109111
import com.google.devtools.build.lib.remote.util.Utils.InMemoryOutput;
110112
import com.google.devtools.build.lib.server.FailureDetails.RemoteExecution;
111113
import com.google.devtools.build.lib.util.Fingerprint;
114+
import com.google.devtools.build.lib.util.OS;
112115
import com.google.devtools.build.lib.util.TempPathGenerator;
113116
import com.google.devtools.build.lib.util.io.FileOutErr;
114117
import com.google.devtools.build.lib.vfs.FileSystem;
115118
import com.google.devtools.build.lib.vfs.FileSystemUtils;
119+
import com.google.devtools.build.lib.vfs.OsPathPolicy;
116120
import com.google.devtools.build.lib.vfs.OutputService;
117121
import com.google.devtools.build.lib.vfs.Path;
118122
import com.google.devtools.build.lib.vfs.PathFragment;
@@ -252,7 +256,8 @@ private Command buildCommand(
252256
ImmutableMap<String, String> env,
253257
@Nullable Platform platform,
254258
RemotePathResolver remotePathResolver,
255-
@Nullable SpawnScrubber spawnScrubber) {
259+
@Nullable SpawnScrubber spawnScrubber,
260+
@Nullable PlatformInfo executionPlatform) {
256261
Command.Builder command = Command.newBuilder();
257262
if (useOutputPaths) {
258263
var outputPaths = new ArrayList<String>();
@@ -283,10 +288,16 @@ private Command buildCommand(
283288
if (platform != null) {
284289
command.setPlatform(platform);
285290
}
291+
boolean first = true;
286292
for (String arg : arguments) {
287293
if (spawnScrubber != null) {
288294
arg = spawnScrubber.transformArgument(arg);
289295
}
296+
if (first && executionPlatform != null) {
297+
first = false;
298+
OS executionOs = ConstraintConstants.getOsFromConstraints(executionPlatform.constraints());
299+
arg = OsPathPolicy.of(executionOs).postProcessPathStringForExecution(arg);
300+
}
290301
command.addArguments(reencodeInternalToExternal(arg));
291302
}
292303
// Sorting the environment pairs by variable name.
@@ -646,7 +657,8 @@ public RemoteAction buildRemoteAction(Spawn spawn, SpawnExecutionContext context
646657
spawn.getEnvironment(),
647658
platform,
648659
remotePathResolver,
649-
spawnScrubber);
660+
spawnScrubber,
661+
spawn.getExecutionPlatform());
650662
Digest commandHash = digestUtil.compute(command);
651663
Action action =
652664
Utils.buildAction(

src/main/java/com/google/devtools/build/lib/vfs/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ java_library(
4848
"//src/main/java/com/google/devtools/build/lib/skyframe/serialization",
4949
"//src/main/java/com/google/devtools/build/lib/skyframe/serialization/autocodec:serialization-constant",
5050
"//src/main/java/com/google/devtools/build/lib/util:filetype",
51+
"//src/main/java/com/google/devtools/build/lib/util:os",
5152
"//third_party:error_prone_annotations",
5253
"//third_party:guava",
5354
"//third_party:jsr305",

src/main/java/com/google/devtools/build/lib/vfs/OsPathPolicy.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,17 @@ public interface OsPathPolicy {
8686

8787
boolean isCaseSensitive();
8888

89+
/**
90+
* Modifies the given string to be suitable for execution on the OS represented by this policy.
91+
*/
92+
String postProcessPathStringForExecution(String callablePathString);
93+
94+
static OsPathPolicy of(OS os) {
95+
return os == OS.WINDOWS ? WindowsOsPathPolicy.INSTANCE : UnixOsPathPolicy.INSTANCE;
96+
}
97+
8998
// We *should* use a case-insensitive policy for OS.DARWIN, but we currently don't handle this.
90-
OsPathPolicy HOST_POLICY =
91-
OS.getCurrent() == OS.WINDOWS ? WindowsOsPathPolicy.INSTANCE : UnixOsPathPolicy.INSTANCE;
99+
OsPathPolicy HOST_POLICY = of(OS.getCurrent());
92100

93101
static OsPathPolicy getFilePathOs() {
94102
return HOST_POLICY;

src/main/java/com/google/devtools/build/lib/vfs/PathFragment.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,8 @@ public String getSafePathString() {
622622
*
623623
* <p>In this way, a shell will always interpret such a string as path (absolute or relative to
624624
* the working directory) and not as command to be searched for in the search path.
625+
*
626+
* <p>Prefer {@link #getCallablePathStringForOs} if the execution OS is available.
625627
*/
626628
public String getCallablePathString() {
627629
if (isAbsolute()) {
@@ -635,6 +637,18 @@ public String getCallablePathString() {
635637
}
636638
}
637639

640+
/**
641+
* Returns the path string using the native name-separator for the given OS, but does so in a way
642+
* unambiguously recognizable as path. In other words, return "." for relative and empty paths,
643+
* and prefix relative paths with an additional "." segment.
644+
*
645+
* <p>In this way, a shell will always interpret such a string as path (absolute or relative to
646+
* the working directory) and not as command to be searched for in the search path.
647+
*/
648+
public String getCallablePathStringForOs(com.google.devtools.build.lib.util.OS executionOs) {
649+
return OsPathPolicy.of(executionOs).postProcessPathStringForExecution(getCallablePathString());
650+
}
651+
638652
/**
639653
* Returns the file extension of this path, excluding the period, or "" if there is no extension.
640654
*/

src/main/java/com/google/devtools/build/lib/vfs/UnixOsPathPolicy.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,4 +139,9 @@ public char additionalSeparator() {
139139
public boolean isCaseSensitive() {
140140
return true;
141141
}
142+
143+
@Override
144+
public String postProcessPathStringForExecution(String callablePathString) {
145+
return callablePathString;
146+
}
142147
}

0 commit comments

Comments
 (0)