Skip to content

Commit c3425fe

Browse files
EdSchoutencopybara-github
authored andcommitted
Let Python stubs respect RUNFILES_* while finding the module space
When invoking a py_binary() through an sh_binary() using the Bash runfiles library, the location of the py_binary() will be resolved from the runfiles manifest file. This means that the argv[0] of the Python stub script will not point to a location under the runfiles directory of the shell script, but to a location in Bazel's execroot. Normally this does not lead to any issues, as argv[0] + ".runfiles" happens to point to be a valid runfiles directory as well. It does become problematic when --nobuild_runfile_links is provided, as in that case only the outer shell script is guaranteed to have a runfiles directory; not the inner Python script. This change extends the Python stub template to also consider RUNFILES_DIR when no runfiles directory can be found. Even though it's not technically correct, we also attempt to derive the runfiles directory from RUNFILES_MANIFEST_FILE. I suspect that this is a necessity as long as py_binary()s cannot operate purely using a manifest file. They currently depend on a concrete instantiation of the runfiles directory. Fixes: #11997 Closes #14740. PiperOrigin-RevId: 478857199 Change-Id: I8cc6ea014bfd4b9ea2f1672e8e814ba38a5bf471
1 parent f61a08d commit c3425fe

File tree

3 files changed

+71
-12
lines changed

3 files changed

+71
-12
lines changed

src/main/java/com/google/devtools/build/lib/bazel/rules/python/python_stub_template.txt

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,24 @@ def CreatePythonPathEntries(python_imports, module_space):
129129
parts = python_imports.split(':')
130130
return [module_space] + ['%s/%s' % (module_space, path) for path in parts]
131131

132-
def FindModuleSpace():
132+
def FindModuleSpace(main_rel_path):
133133
"""Finds the runfiles tree."""
134+
# When the calling process used the runfiles manifest to resolve the
135+
# location of this stub script, the path may be expanded. This means
136+
# argv[0] may no longer point to a location inside the runfiles
137+
# directory. We should therefore respect RUNFILES_DIR and
138+
# RUNFILES_MANIFEST_FILE set by the caller.
139+
runfiles_dir = os.environ.get('RUNFILES_DIR', None)
140+
if not runfiles_dir:
141+
runfiles_manifest_file = os.environ.get('RUNFILES_MANIFEST_FILE', '')
142+
if (runfiles_manifest_file.endswith('.runfiles_manifest') or
143+
runfiles_manifest_file.endswith('.runfiles/MANIFEST')):
144+
runfiles_dir = runfiles_manifest_file[:-9]
145+
# Be defensive: the runfiles dir should contain our main entry point. If
146+
# it doesn't, then it must not be our runfiles directory.
147+
if runfiles_dir and os.path.exists(os.path.join(runfiles_dir, main_rel_path)):
148+
return runfiles_dir
149+
134150
stub_filename = sys.argv[0]
135151
if not os.path.isabs(stub_filename):
136152
stub_filename = os.path.join(os.getcwd(), stub_filename)
@@ -413,10 +429,17 @@ def Main():
413429

414430
new_env = {}
415431

432+
# The main Python source file.
433+
# The magic string percent-main-percent is replaced with the runfiles-relative
434+
# filename of the main file of the Python binary in BazelPythonSemantics.java.
435+
main_rel_path = '%main%'
436+
if IsWindows():
437+
main_rel_path = main_rel_path.replace('/', os.sep)
438+
416439
if IsRunningFromZip():
417440
module_space = CreateModuleSpace()
418441
else:
419-
module_space = FindModuleSpace()
442+
module_space = FindModuleSpace(main_rel_path)
420443

421444
python_imports = '%imports%'
422445
python_path_entries = CreatePythonPathEntries(python_imports, module_space)
@@ -446,14 +469,7 @@ def Main():
446469
# See: https://docs.python.org/3.11/using/cmdline.html#envvar-PYTHONSAFEPATH
447470
new_env['PYTHONSAFEPATH'] = '1'
448471

449-
# Now look for my main python source file.
450-
# The magic string percent-main-percent is replaced with the filename of the
451-
# main file of the Python binary in BazelPythonSemantics.java.
452-
rel_path = '%main%'
453-
if IsWindows():
454-
rel_path = rel_path.replace('/', os.sep)
455-
456-
main_filename = os.path.join(module_space, rel_path)
472+
main_filename = os.path.join(module_space, main_rel_path)
457473
main_filename = GetWindowsPathWithUNCPrefix(main_filename)
458474
assert os.path.exists(main_filename), \
459475
'Cannot exec() %r: file not found.' % main_filename

src/test/shell/bazel/bazel_example_test.sh

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,15 +151,20 @@ function test_shell() {
151151
function test_python() {
152152
assert_build "//examples/py:bin"
153153

154-
./bazel-bin/examples/py/bin >& $TEST_log \
154+
# Don't invoke the Python binary with RUNFILES_* set, as that causes
155+
# it to look in the runfiles directory of this test, instead of the
156+
# one belonging to the Python binary.
157+
env -u RUNFILES_DIR -u RUNFILES_MANIFEST_FILE \
158+
./bazel-bin/examples/py/bin >& $TEST_log \
155159
|| fail "//examples/py:bin execution failed"
156160
expect_log "Fib(5)=8"
157161

158162
# Mutate //examples/py:bin so that it needs to build again.
159163
echo "print('Hello')" > ./examples/py/bin.py
160164
# Ensure that we can rebuild //examples/py::bin without error.
161165
assert_build "//examples/py:bin"
162-
./bazel-bin/examples/py/bin >& $TEST_log \
166+
env -u RUNFILES_DIR -u RUNFILES_MANIFEST_FILE \
167+
./bazel-bin/examples/py/bin >& $TEST_log \
163168
|| fail "//examples/py:bin 2nd build execution failed"
164169
expect_log "Hello"
165170
}

src/test/shell/integration/python_stub_test.sh

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,4 +190,42 @@ EOF
190190
&> $TEST_log || fail "bazel run failed"
191191
}
192192

193+
# When invoking a Python binary using the runfiles manifest, the stub
194+
# script's argv[0] will point to a location in the execroot; not the
195+
# runfiles directory of the caller. The stub script should still be
196+
# capable of finding its runfiles directory by considering RUNFILES_DIR
197+
# and RUNFILES_MANIFEST_FILE set by the caller.
198+
function test_python_through_bash_without_runfile_links() {
199+
mkdir -p python_through_bash
200+
201+
cat > python_through_bash/BUILD << EOF
202+
py_binary(
203+
name = "inner",
204+
srcs = ["inner.py"],
205+
)
206+
207+
sh_binary(
208+
name = "outer",
209+
srcs = ["outer.sh"],
210+
data = [":inner"],
211+
)
212+
EOF
213+
214+
cat > python_through_bash/outer.sh << EOF
215+
#!/bin/bash
216+
# * Bazel run guarantees that our CWD is the runfiles directory itself, so a
217+
# relative path will work.
218+
# * We can't use the usual shell runfiles library because it doesn't work in the
219+
# Google environment nested within a generated shell test.
220+
find . -name inner$EXE_EXT | xargs env
221+
EOF
222+
chmod +x python_through_bash/outer.sh
223+
224+
touch python_through_bash/inner.py
225+
226+
bazel run --nobuild_runfile_links //python_through_bash:outer \
227+
&> $TEST_log || fail "bazel run failed"
228+
expect_log "I am Python"
229+
}
230+
193231
run_suite "Tests for the Python rules without Python execution"

0 commit comments

Comments
 (0)