Repository: bazelbuild/bazel
Issue: runfiles, python: runfiles library doesn't work for sandboxed binaries
Labels: type: bug
platform: linux

If a `py_binary` runs in a sandbox and uses Bazel's runfiles library (in `@bazel_tools//tools/python/runfiles`), the library fails to initialize.

Repro:
1. create a py_binary with a data-dependency that uses the runfiles library to look up the runfile
2. create a genrule, add the py_binary to its `tools`
3. in the genrule's cmd, run the py_binary
4. build with sandboxing enabled; observe that it fails to init the runfiles library

Bazel 0.17.2 on Linux with sandboxing enabled.

Issue comments:

Pull request review comments:

Patch that resolved the issue:
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/python/python_stub_template.txt b/src/main/java/com/google/devtools/build/lib/bazel/rules/python/python_stub_template.txt
index b4ab41f3312f39..d5fb78fafc3dc3 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/python/python_stub_template.txt
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/python/python_stub_template.txt
@@ -126,6 +126,11 @@ def RunfilesEnvvar(module_space):
   if os.path.exists(runfiles):
     return ('RUNFILES_DIR', runfiles)
 
+  # If running in a sandbox and no environment variables are set, then
+  # Look for the runfiles  next to the binary.
+  if module_space.endswith('.runfiles') and os.path.isdir(module_space):
+    return ('RUNFILES_DIR', module_space)
+
   return (None, None)
 
 def Main():


Test patch:
diff --git a/src/test/py/bazel/BUILD b/src/test/py/bazel/BUILD
index 57a1671d2f968d..18807473d0bf84 100644
--- a/src/test/py/bazel/BUILD
+++ b/src/test/py/bazel/BUILD
@@ -120,6 +120,18 @@ py_test(
     ],
 )
 
+py_test(
+    name = "runfiles_sandboxed_test",
+    timeout = "long",
+    srcs = ["runfiles_sandboxed_test.py"],
+    data = glob(["testdata/runfiles_test/**"]),
+    deps = [":test_base"],
+    tags = [
+        # Windows does not support sandboxing yet.
+        "no_windows",
+    ],
+)
+
 py_test(
     name = "bazel_windows_cpp_test",
     size = "large",
diff --git a/src/test/py/bazel/runfiles_sandboxed_test.py b/src/test/py/bazel/runfiles_sandboxed_test.py
new file mode 100644
index 00000000000000..c8e492482ae305
--- /dev/null
+++ b/src/test/py/bazel/runfiles_sandboxed_test.py
@@ -0,0 +1,120 @@
+# pylint: disable=g-bad-file-header
+# Copyright 2018 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import unittest
+from src.test.py.bazel import test_base
+
+
+class RunfilesSandboxedTest(test_base.TestBase):
+
+  def _FailWithContents(self, msg, contents):
+    self.fail("%s\ncontents =\n | %s\n---" % (msg, "\n | ".join(contents)))
+
+  def testRunfilesLibrariesFindRunfilesWithoutEnvvars(self):
+    for s, t, exe in [
+        ("WORKSPACE.mock", "WORKSPACE", False),
+        ("bar/BUILD.mock", "bar/BUILD", False),
+        ("bar/bar.py", "bar/bar.py", True),
+        ("bar/bar-py-data.txt", "bar/bar-py-data.txt", False),
+        ("bar/Bar.java", "bar/Bar.java", False),
+        ("bar/bar-java-data.txt", "bar/bar-java-data.txt", False),
+        ("bar/bar.sh", "bar/bar.sh", True),
+        ("bar/bar-sh-data.txt", "bar/bar-sh-data.txt", False),
+        ("bar/bar.cc", "bar/bar.cc", False),
+        ("bar/bar-cc-data.txt", "bar/bar-cc-data.txt", False),
+    ]:
+      self.CopyFile(
+          self.Rlocation("io_bazel/src/test/py/bazel/testdata/runfiles_test/" +
+                         s), t, exe)
+    self.ScratchFile("foo/BUILD", [
+        "genrule(",
+        "    name = 'gen',",
+        "    outs = ['stdout.txt', 'data_files.txt'],",
+        "    cmd = 'cat $$(' + ",
+        # The genrule runs all bar-<language> tools, saves the complete stdout
+        # into stdout.txt, and prints the contents of rlocations reported by the
+        # tools (i.e. the contents of the bar-<language>-data.txt files) into
+        # data_files.txt.
+        "          '  ( $(location //bar:bar-py) && ' +",
+        "          '    $(location //bar:bar-java) && ' +",
+        "          '    $(location //bar:bar-sh) && ' +",
+        "          '    $(location //bar:bar-cc) ; ' +",
+        "          '  ) | ' + ",
+        "          '    tee $(location stdout.txt) | ' + ",
+        "          '    grep \"^rloc=\" | ' + ",
+        "          '    sed \"s,^rloc=,,\"' + ",
+        "          ') > $(location data_files.txt)',",
+        "    tools = [",
+        "        '//bar:bar-cc',",
+        "        '//bar:bar-java',",
+        "        '//bar:bar-py',",
+        "        '//bar:bar-sh',",
+        "    ],",
+        ")"])
+
+    exit_code, stdout, stderr = self.RunBazel(["info", "bazel-genfiles"])
+    self.AssertExitCode(exit_code, 0, stderr)
+    bazel_genfiles = stdout[0]
+
+    exit_code, _, stderr = self.RunBazel([
+        "build", "--verbose_failures",
+        "//foo:gen", "--genrule_strategy=sandboxed",
+    ])
+    self.AssertExitCode(exit_code, 0, stderr)
+
+    stdout_txt = os.path.join(bazel_genfiles, "foo/stdout.txt")
+    self.assertTrue(os.path.isfile(stdout_txt))
+
+    data_files_txt = os.path.join(bazel_genfiles, "foo/data_files.txt")
+    self.assertTrue(os.path.isfile(data_files_txt))
+
+    # Output of the bar-<language> binaries that they printed to stdout.
+    stdout_lines = []
+    with open(stdout_txt, "rt") as f:
+      stdout_lines = [line.strip() for line in f.readlines()]
+
+    # Contents of the bar-<language>-data.txt files.
+    data_files = []
+    with open(data_files_txt, "rt") as f:
+      data_files = [line.strip() for line in f.readlines()]
+
+    if len(stdout_lines) != 8:
+      self._FailWithContents("wrong number of output lines", stdout_lines)
+    i = 0
+    for lang in [("py", "Python", "bar.py"), ("java", "Java", "Bar.java"),
+                 ("sh", "Bash", "bar.sh"), ("cc", "C++", "bar.cc")]:
+      # Check that the bar-<language> binary printed the expected output.
+      if stdout_lines[i * 2] != "Hello %s Bar!" % lang[1]:
+        self._FailWithContents("wrong line for " + lang[1], stdout_lines)
+      if not stdout_lines[i * 2 + 1].startswith("rloc="):
+        self._FailWithContents("wrong line for " + lang[1], stdout_lines)
+      if not stdout_lines[i * 2 + 1].endswith(
+          "foo_ws/bar/bar-%s-data.txt" % lang[0]):
+        self._FailWithContents("wrong line for " + lang[1], stdout_lines)
+
+      # Assert the contents of bar-<language>-data.txt. This indicates that
+      # the runfiles library in the bar-<language> binary found the correct
+      # runfile and returned a valid path.
+      if data_files[i] != "data for " + lang[2]:
+        self._FailWithContents("runfile does not exist for " + lang[1],
+                               stdout_lines)
+
+      i += 1
+
+
+if __name__ == "__main__":
+  unittest.main()
+