Skip to content

Commit ae818c4

Browse files
fmeumcopybara-github
authored andcommitted
Make Window launchers relocatable
When the Windows launcher binary is copied to become the `executable` of another target, it can't expect to find the main file right next to it. Instead, look it up via `Rlocation`, which doesn't require colocation to find the file. This helps with a common pattern in wrapper rules where `ctx.symlink` is used to work around the limitation that the `executable` of a target has to be produced by that target. For the Python rules, for which the source of truth is not the Bazel repo anymore, the new launch key is checked for in a backwards compatible way. Closes #23076. PiperOrigin-RevId: 657116461 Change-Id: I3f966b48e0812668cfa7bd394f2eaf23d66889b6
1 parent 8e79fe0 commit ae818c4

File tree

11 files changed

+209
-23
lines changed

11 files changed

+209
-23
lines changed

src/main/java/com/google/devtools/build/lib/bazel/rules/sh/ShBinary.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ private static boolean isWindowsExecutable(Artifact artifact) {
102102
}
103103

104104
private static Artifact createWindowsExeLauncher(
105-
RuleContext ruleContext, PathFragment shExecutable) throws RuleErrorException {
105+
RuleContext ruleContext, PathFragment shExecutable, Artifact primaryOutput) {
106106
Artifact bashLauncher =
107107
ruleContext.getImplicitOutputArtifact(ruleContext.getTarget().getName() + ".exe");
108108

@@ -114,6 +114,11 @@ private static Artifact createWindowsExeLauncher(
114114
"symlink_runfiles_enabled",
115115
ruleContext.getConfiguration().runfilesEnabled() ? "1" : "0")
116116
.addKeyValuePair("bash_bin_path", shExecutable.getPathString())
117+
.addKeyValuePair(
118+
"bash_file_rlocationpath",
119+
PathFragment.create(ruleContext.getWorkspaceName())
120+
.getRelative(primaryOutput.getRunfilesPathString())
121+
.getPathString())
117122
.build();
118123

119124
LauncherFileWriteAction.createAndRegister(ruleContext, bashLauncher, launchInfo);
@@ -139,6 +144,6 @@ private static Artifact launcherForWindows(
139144
PathFragment shExecutable =
140145
ShToolchain.getPathForPlatform(
141146
ruleContext.getConfiguration(), ruleContext.getExecutionPlatform());
142-
return createWindowsExeLauncher(ruleContext, shExecutable);
147+
return createWindowsExeLauncher(ruleContext, shExecutable, primaryOutput);
143148
}
144149
}

src/main/starlark/builtins_bzl/common/python/py_executable_bazel.bzl

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -209,8 +209,8 @@ def _create_executable(
209209
# 2. (non-Windows) A self-executable zip file of a bootstrap template based program.
210210
# 3. (Windows) A native Windows executable that finds and launches
211211
# the actual underlying Bazel program (one of the above). Note that
212-
# it implicitly assumes one of the above is located next to it, and
213-
# that --build_python_zip defaults to true for Windows.
212+
# it implicitly assumes that --build_python_zip defaults to true for
213+
# Windows.
214214

215215
should_create_executable_zip = False
216216
bootstrap_output = None
@@ -220,20 +220,24 @@ def _create_executable(
220220
else:
221221
bootstrap_output = executable
222222
else:
223-
_create_windows_exe_launcher(
224-
ctx,
225-
output = executable,
226-
use_zip_file = build_zip_enabled,
227-
python_binary_path = runtime_details.executable_interpreter_path,
228-
)
229-
if not build_zip_enabled:
223+
if build_zip_enabled:
224+
python_file = zip_file
225+
else:
230226
# On Windows, the main executable has an "exe" extension, so
231227
# here we re-use the un-extensioned name for the bootstrap output.
232228
bootstrap_output = ctx.actions.declare_file(base_executable_name)
229+
python_file = bootstrap_output
233230

234231
# The launcher looks for the non-zip executable next to
235232
# itself, so add it to the default outputs.
236233
extra_files_to_build.append(bootstrap_output)
234+
_create_windows_exe_launcher(
235+
ctx,
236+
output = executable,
237+
use_zip_file = build_zip_enabled,
238+
python_binary_path = runtime_details.executable_interpreter_path,
239+
python_file = python_file,
240+
)
237241

238242
if should_create_executable_zip:
239243
if bootstrap_output != None:
@@ -316,7 +320,8 @@ def _create_windows_exe_launcher(
316320
*,
317321
output,
318322
python_binary_path,
319-
use_zip_file):
323+
use_zip_file,
324+
python_file):
320325
launch_info = ctx.actions.args()
321326
launch_info.use_param_file("%s", use_always = True)
322327
launch_info.set_param_file_format("multiline")
@@ -328,6 +333,7 @@ def _create_windows_exe_launcher(
328333
)
329334
launch_info.add(python_binary_path, format = "python_bin_path=%s")
330335
launch_info.add("1" if use_zip_file else "0", format = "use_zip_file=%s")
336+
launch_info.add(python_file.short_path, format = "python_file_short_path=%s")
331337

332338
ctx.actions.run(
333339
executable = ctx.executable._windows_launcher_maker,

src/test/java/com/google/devtools/build/lib/blackbox/framework/ProcessRunner.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package com.google.devtools.build.lib.blackbox.framework;
1616

1717
import com.google.common.collect.ImmutableList;
18+
import com.google.common.collect.ImmutableSet;
1819
import com.google.common.collect.Lists;
1920
import com.google.common.flogger.GoogleLogger;
2021
import com.google.common.flogger.LazyArgs;
@@ -77,6 +78,18 @@ public ProcessResult runSynchronously() throws Exception {
7778
ProcessBuilder processBuilder = new ProcessBuilder(commandParts);
7879
processBuilder.directory(parameters.workingDirectory());
7980
parameters.environment().ifPresent(map -> processBuilder.environment().putAll(map));
81+
// Always clear the variables used for runfiles discovery so that the process doesn't inherit
82+
// them from the Bazel test environment.
83+
processBuilder
84+
.environment()
85+
.keySet()
86+
.removeAll(
87+
ImmutableSet.of(
88+
"RUNFILES_DIR",
89+
"RUNFILES_MANIFEST_FILE",
90+
"RUNFILES_MANIFEST_ONLY",
91+
"JAVA_RUNFILES",
92+
"PYTHON_RUNFILES"));
8093

8194
parameters.redirectOutput().ifPresent(path -> processBuilder.redirectOutput(path.toFile()));
8295
parameters.redirectError().ifPresent(path -> processBuilder.redirectError(path.toFile()));

src/test/py/bazel/runfiles_test.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,138 @@ def testTestsRunWithNoBuildRunfileLinksAndNoEnableRunfiles(self):
480480
["test", ":test", "--nobuild_runfile_links", "--noenable_runfiles"]
481481
)
482482

483+
def testWrappedShBinary(self):
484+
self.writeWrapperRule()
485+
self.ScratchFile("MODULE.bazel")
486+
self.ScratchFile(
487+
"BUILD",
488+
[
489+
"sh_binary(",
490+
" name = 'binary',",
491+
" srcs = ['binary.sh'],",
492+
" visibility = ['//visibility:public'],",
493+
")",
494+
],
495+
)
496+
self.ScratchFile(
497+
"binary.sh",
498+
[
499+
"echo Hello, World!",
500+
],
501+
executable=True,
502+
)
503+
504+
_, stdout, _ = self.RunBazel(["run", "//wrapped"])
505+
self.assertEqual(stdout, ["Hello, World!"])
506+
507+
def testWrappedPyBinary(self):
508+
self.writeWrapperRule()
509+
self.ScratchFile("MODULE.bazel")
510+
self.ScratchFile(
511+
"BUILD",
512+
[
513+
"py_binary(",
514+
" name = 'binary',",
515+
" srcs = ['binary.py'],",
516+
" visibility = ['//visibility:public'],",
517+
")",
518+
],
519+
)
520+
self.ScratchFile(
521+
"binary.py",
522+
[
523+
"print('Hello, World!')",
524+
],
525+
)
526+
527+
_, stdout, _ = self.RunBazel(["run", "//wrapped"])
528+
self.assertEqual(stdout, ["Hello, World!"])
529+
530+
def testWrappedJavaBinary(self):
531+
self.writeWrapperRule()
532+
self.ScratchFile("MODULE.bazel")
533+
self.ScratchFile(
534+
"BUILD",
535+
[
536+
"java_binary(",
537+
" name = 'binary',",
538+
" srcs = ['Binary.java'],",
539+
" main_class = 'Binary',",
540+
" visibility = ['//visibility:public'],",
541+
")",
542+
],
543+
)
544+
self.ScratchFile(
545+
"Binary.java",
546+
[
547+
"public class Binary {",
548+
" public static void main(String[] args) {",
549+
' System.out.println("Hello, World!");',
550+
" }",
551+
"}",
552+
],
553+
)
554+
555+
_, stdout, _ = self.RunBazel(["run", "//wrapped"])
556+
self.assertEqual(stdout, ["Hello, World!"])
557+
558+
def writeWrapperRule(self):
559+
self.ScratchFile("rules/BUILD")
560+
self.ScratchFile(
561+
"rules/wrapper.bzl",
562+
[
563+
"def _wrapper_impl(ctx):",
564+
" target = ctx.attr.target",
565+
(
566+
" original_executable ="
567+
" target[DefaultInfo].files_to_run.executable"
568+
),
569+
(
570+
" executable ="
571+
" ctx.actions.declare_file(original_executable.basename)"
572+
),
573+
(
574+
" ctx.actions.symlink(output = executable, target_file ="
575+
" original_executable)"
576+
),
577+
(
578+
" data_runfiles ="
579+
" ctx.runfiles([executable]).merge(target[DefaultInfo].data_runfiles)"
580+
),
581+
(
582+
" default_runfiles ="
583+
" ctx.runfiles([executable]).merge(target[DefaultInfo].default_runfiles)"
584+
),
585+
" return [",
586+
" DefaultInfo(",
587+
" executable = executable,",
588+
" files = target[DefaultInfo].files,",
589+
" data_runfiles = data_runfiles,",
590+
" default_runfiles = default_runfiles,",
591+
" ), ]",
592+
"wrapper = rule(",
593+
" implementation = _wrapper_impl,",
594+
" attrs = {",
595+
" 'target': attr.label(",
596+
" cfg = 'target',",
597+
" executable = True,",
598+
" ),",
599+
" },",
600+
" executable = True,",
601+
")",
602+
],
603+
)
604+
self.ScratchFile(
605+
"wrapped/BUILD",
606+
[
607+
"load('//rules:wrapper.bzl', 'wrapper')",
608+
"wrapper(",
609+
" name = 'wrapped',",
610+
" target = '//:binary',",
611+
")",
612+
],
613+
)
614+
483615

484616
if __name__ == "__main__":
485617
absltest.main()

src/test/shell/bazel/bazel_windows_example_test.sh

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -293,8 +293,11 @@ function test_java_test() {
293293
function test_native_python() {
294294
# On windows, we build a python executable zip as the python binary
295295
assert_build //examples/py_native:bin
296-
# run the python package directly
297-
./bazel-bin/examples/py_native/bin >& $TEST_log \
296+
# run the python package directly, clearing out runfiles variables to
297+
# ensure that the binary finds its own runfiles correctly
298+
env -u RUNFILES_DIR -u RUNFILES_MANIFEST_FILE -u RUNFILES_MANIFEST_ONLY \
299+
-u JAVA_RUNFILES -u PYTHON_RUNFILES \
300+
./bazel-bin/examples/py_native/bin >& $TEST_log \
298301
|| fail "//examples/py_native:bin execution failed"
299302
expect_log "Fib(5) == 8"
300303
# Using python <zipfile> to run the python package

src/test/shell/bazel/python_version_test.sh

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,11 @@ print(__file__)
266266
EOF
267267

268268
bazel build //test:pybin --build_python_zip &> $TEST_log || fail "bazel build failed"
269-
pybin_location=$(bazel-bin/test/pybin)
269+
# Clear out runfiles variables to ensure that the binary finds its own
270+
# runfiles correctly.
271+
pybin_location=$(env -u RUNFILES_DIR -u RUNFILES_MANIFEST_FILE \
272+
-u RUNFILES_MANIFEST_ONLY -u JAVA_RUNFILES -u PYTHON_RUNFILES \
273+
bazel-bin/test/pybin)
270274

271275
# The pybin location is "<ms root>/runfiles/<workspace>/test/pybin.py",
272276
# so we have to go up 4 directories to get to the module space root

src/test/shell/integration/run_test.sh

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -594,15 +594,18 @@ echo "goodbye $@"
594594
EOF
595595
chmod +x "$pkg/farewell.sh"
596596

597-
bazel run --run_under="//$pkg:greetings friend &&" -- "//$pkg:farewell" buddy \
597+
bazel run --run_under="//$pkg:greetings friend && unset RUNFILES_MANIFEST_FILE &&" -- "//$pkg:farewell" buddy \
598598
>$TEST_log || fail "expected test to pass"
599599
# TODO(https://github.com/bazelbuild/bazel/issues/22148): bazel-team - This is
600600
# just demonstrating how things are, it's probably not how we want them to be.
601+
# "unset RUNFILES_MANIFEST_FILE" is necessary because the environment
602+
# variables set by //pkg:greetings are otherwise passed to //pkg:farewell and
603+
# break its runfiles discovery.
601604
if "$is_windows"; then
602605
expect_log "hello there friend"
603606
expect_log "goodbye buddy"
604607
else
605-
expect_log "hello there friend && .*bin/$pkg/farewell buddy"
608+
expect_log "hello there friend && unset RUNFILES_MANIFEST_FILE && .*bin/$pkg/farewell buddy"
606609
expect_not_log "goodbye"
607610
fi
608611
}

src/tools/launcher/bash_launcher.cc

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ using std::wostringstream;
2727
using std::wstring;
2828

2929
static constexpr const char* BASH_BIN_PATH = "bash_bin_path";
30+
static constexpr const char* BASH_FILE_RLOCATIONPATH =
31+
"bash_file_rlocationpath";
3032

3133
ExitCode BashBinaryLauncher::Launch() {
3234
wstring bash_binary = this->GetLaunchInfoByKey(BASH_BIN_PATH);
@@ -52,8 +54,10 @@ ExitCode BashBinaryLauncher::Launch() {
5254

5355
vector<wstring> origin_args = this->GetCommandlineArguments();
5456
wostringstream bash_command;
55-
wstring bash_main_file = GetBinaryPathWithoutExtension(GetLauncherPath());
56-
bash_command << BashEscapeArg(bash_main_file);
57+
wstring bash_file_rlocationpath =
58+
this->GetLaunchInfoByKey(BASH_FILE_RLOCATIONPATH);
59+
wstring bash_file = Rlocation(bash_file_rlocationpath, true);
60+
bash_command << BashEscapeArg(bash_file);
5761
for (int i = 1; i < origin_args.size(); i++) {
5862
bash_command << L' ';
5963
bash_command << BashEscapeArg(origin_args[i]);

src/tools/launcher/launcher.cc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,14 @@ wstring BinaryLauncherBase::GetLaunchInfoByKey(const string& key) {
197197
return item->second;
198198
}
199199

200+
wstring BinaryLauncherBase::GetLaunchInfoByKeyOrEmpty(const std::string& key) {
201+
auto item = launch_info.find(key);
202+
if (item == launch_info.end()) {
203+
return L"";
204+
}
205+
return item->second;
206+
}
207+
200208
const vector<wstring>& BinaryLauncherBase::GetCommandlineArguments() const {
201209
return this->commandline_arguments;
202210
}

src/tools/launcher/launcher.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class BinaryLauncherBase {
4949

5050
// Get launch information based on a launch info key.
5151
std::wstring GetLaunchInfoByKey(const std::string& key);
52+
std::wstring GetLaunchInfoByKeyOrEmpty(const std::string& key);
5253

5354
// Get the original command line arguments passed to this binary.
5455
const std::vector<std::wstring>& GetCommandlineArguments() const;

0 commit comments

Comments
 (0)