Skip to content

Commit

Permalink
Java,runfiles: add Runfiles.getEnvVars()
Browse files Browse the repository at this point in the history
The new method lets Java programs export the
RUNFILES_* environment variables for subprocesses,
in case those subprocesses are other Bazel-built
binaries that also need runfiles.

See bazelbuild#4460

Change-Id: I05c0b84db357128bc15f21a167a0d92e8d17599c

Closes bazelbuild#5016.

Change-Id: I66ca5c44067a7353b8de180a64f20c8352e3c9ec
PiperOrigin-RevId: 193013289
  • Loading branch information
laszlocsomor authored and Copybara-Service committed Apr 16, 2018
1 parent fa135cf commit ef247ef
Show file tree
Hide file tree
Showing 8 changed files with 235 additions and 59 deletions.
47 changes: 6 additions & 41 deletions src/test/py/bazel/runfiles_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,50 +31,12 @@ def testAttemptToBuildRunfilesOnWindows(self):
self.assertIn("building runfiles is not supported on Windows",
"\n".join(stderr))

def testJavaRunfilesLibraryInBazelToolsRepo(self):
for s, t in [
("WORKSPACE.mock", "WORKSPACE"),
("foo/BUILD.mock", "foo/BUILD"),
("foo/Foo.java", "foo/Foo.java"),
("foo/datadep/hello.txt", "foo/datadep/hello.txt"),
]:
self.CopyFile(
self.Rlocation(
"io_bazel/src/test/py/bazel/testdata/runfiles_test/" + s), t)

exit_code, stdout, stderr = self.RunBazel(["info", "bazel-bin"])
self.AssertExitCode(exit_code, 0, stderr)
bazel_bin = stdout[0]

exit_code, _, stderr = self.RunBazel(["build", "//foo:runfiles-java"])
self.AssertExitCode(exit_code, 0, stderr)

if test_base.TestBase.IsWindows():
bin_path = os.path.join(bazel_bin, "foo/runfiles-java.exe")
else:
bin_path = os.path.join(bazel_bin, "foo/runfiles-java")

self.assertTrue(os.path.exists(bin_path))

exit_code, stdout, stderr = self.RunProgram(
[bin_path], env_add={"TEST_SRCDIR": "__ignore_me__"})
self.AssertExitCode(exit_code, 0, stderr)
if len(stdout) != 2:
self.fail("stdout: %s" % stdout)
self.assertEqual(stdout[0], "Hello Java Foo!")
six.assertRegex(self, stdout[1], "^rloc=.*/foo/datadep/hello.txt")
self.assertNotIn("__ignore_me__", stdout[1])
with open(stdout[1].split("=", 1)[1], "r") as f:
lines = [l.strip() for l in f.readlines()]
if len(lines) != 1:
self.fail("lines: %s" % lines)
self.assertEqual(lines[0], "world")

def _AssertPythonRunfilesLibraryInBazelToolsRepo(self, family, lang_name):
def _AssertRunfilesLibraryInBazelToolsRepo(self, family, lang_name):
for s, t in [
("WORKSPACE.mock", "WORKSPACE"),
("foo/BUILD.mock", "foo/BUILD"),
("foo/foo.py", "foo/foo.py"),
("foo/Foo.java", "foo/Foo.java"),
("foo/datadep/hello.txt", "foo/datadep/hello.txt"),
("bar/BUILD.mock", "bar/BUILD"),
("bar/bar.py", "bar/bar.py"),
Expand Down Expand Up @@ -132,7 +94,10 @@ def _AssertPythonRunfilesLibraryInBazelToolsRepo(self, family, lang_name):
i += 2

def testPythonRunfilesLibraryInBazelToolsRepo(self):
self._AssertPythonRunfilesLibraryInBazelToolsRepo("py", "Python")
self._AssertRunfilesLibraryInBazelToolsRepo("py", "Python")

def testJavaRunfilesLibraryInBazelToolsRepo(self):
self._AssertRunfilesLibraryInBazelToolsRepo("java", "Java")

def testRunfilesLibrariesFindRunfilesWithoutEnvvars(self):
for s, t in [
Expand Down
2 changes: 2 additions & 0 deletions src/test/py/bazel/testdata/runfiles_test/foo/BUILD.mock
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ java_binary(
srcs = ["Foo.java"],
data = [
"datadep/hello.txt",
"//bar:bar-py",
"//bar:bar-java",
],
main_class = "Foo",
deps = ["@bazel_tools//tools/runfiles:java-runfiles"],
Expand Down
55 changes: 54 additions & 1 deletion src/test/py/bazel/testdata/runfiles_test/foo/Foo.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,66 @@
// limitations under the License.

import com.google.devtools.build.runfiles.Runfiles;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;

/** A mock Java binary only used in tests, to exercise the Java Runfiles library. */
public class Foo {
public static void main(String[] args) throws IOException {
public static void main(String[] args) throws IOException, InterruptedException {
System.out.println("Hello Java Foo!");
Runfiles r = Runfiles.create();
System.out.println("rloc=" + r.rlocation("foo_ws/foo/datadep/hello.txt"));

for (String lang : new String[] {"py", "java"}) {
String path = r.rlocation(childBinaryName(lang));
if (path == null || path.isEmpty()) {
throw new IOException("cannot find child binary for " + lang);
}

ProcessBuilder pb = new ProcessBuilder(path);
pb.environment().putAll(r.getEnvVars());
if (isWindows()) {
pb.environment().put("SYSTEMROOT", System.getenv("SYSTEMROOT"));
}
Process p = pb.start();
if (!p.waitFor(3, TimeUnit.SECONDS)) {
throw new IOException("child process for " + lang + " timed out");
}
if (p.exitValue() != 0) {
throw new IOException(
"child process for " + lang + " failed: " + readStream(p.getErrorStream()));
}
System.out.printf(readStream(p.getInputStream()));
}
}

private static boolean isWindows() {
return File.separatorChar == '\\';
}

private static String childBinaryName(String lang) {
if (isWindows()) {
return "foo_ws/bar/bar-" + lang + ".exe";
} else {
return "foo_ws/bar/bar-" + lang;
}
}

private static String readStream(InputStream stm) throws IOException {
StringBuilder result = new StringBuilder();
try (BufferedReader r =
new BufferedReader(new InputStreamReader(stm, StandardCharsets.UTF_8))) {
String line = null;
while ((line = r.readLine()) != null) {
line = line.trim(); // trim CRLF on Windows, LF on Linux
result.append(line).append("\n"); // ensure uniform line ending
}
}
return result.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,32 @@

package com.google.devtools.build.runfiles;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/** {@link Runfiles} implementation that appends runfiles paths to the runfiles root. */
class DirectoryBased extends Runfiles {
final class DirectoryBased extends Runfiles {
private final String runfilesRoot;

DirectoryBased(String runfilesDir) {
Util.checkArgument(runfilesDir != null);
Util.checkArgument(!runfilesDir.isEmpty());
DirectoryBased(String runfilesDir) throws IOException {
Util.checkArgument(!Util.isNullOrEmpty(runfilesDir));
Util.checkArgument(new File(runfilesDir).isDirectory());
this.runfilesRoot = runfilesDir;
}

@Override
String rlocationChecked(String path) {
return runfilesRoot + "/" + path;
}

@Override
public Map<String, String> getEnvVars() {
HashMap<String, String> result = new HashMap<>(2);
result.put("RUNFILES_DIR", runfilesRoot);
// TODO(laszlocsomor): remove JAVA_RUNFILES once the Java launcher can pick up RUNFILES_DIR.
result.put("JAVA_RUNFILES", runfilesRoot);
return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;

import java.io.File;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
Expand All @@ -30,8 +31,11 @@ public void testRlocation() throws Exception {
// The DirectoryBased implementation simply joins the runfiles directory and the runfile's path
// on a "/". DirectoryBased does not perform any normalization, nor does it check that the path
// exists.
DirectoryBased r = new DirectoryBased("foo/bar baz//qux/");
assertThat(r.rlocation("arg")).isEqualTo("foo/bar baz//qux//arg");
File dir = new File(System.getenv("TEST_TMPDIR"), "mock/runfiles");
assertThat(dir.mkdirs()).isTrue();
DirectoryBased r = new DirectoryBased(dir.toString());
// Escaping for "\": once for string and once for regex.
assertThat(r.rlocation("arg")).matches(".*[/\\\\]mock[/\\\\]runfiles[/\\\\]arg");
}

@Test
Expand All @@ -50,6 +54,13 @@ public void testCtorArgumentValidation() throws Exception {
// expected
}

new DirectoryBased("non-empty value is fine");
try {
new DirectoryBased("non-existent directory is bad");
fail();
} catch (IllegalArgumentException e) {
// expected
}

new DirectoryBased(System.getenv("TEST_TMPDIR"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package com.google.devtools.build.runfiles;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
Expand All @@ -24,12 +25,14 @@
import java.util.Map;

/** {@link Runfiles} implementation that parses a runfiles-manifest file to look up runfiles. */
class ManifestBased extends Runfiles {
final class ManifestBased extends Runfiles {
private final Map<String, String> runfiles;
private final String manifestPath;

ManifestBased(String manifestPath) throws IOException {
Util.checkArgument(manifestPath != null);
Util.checkArgument(!manifestPath.isEmpty());
this.manifestPath = manifestPath;
this.runfiles = loadRunfiles(manifestPath);
}

Expand All @@ -49,8 +52,32 @@ private static Map<String, String> loadRunfiles(String path) throws IOException
return Collections.unmodifiableMap(result);
}

private static String findRunfilesDir(String manifest) {
if (manifest.endsWith("/MANIFEST")
|| manifest.endsWith("\\MANIFEST")
|| manifest.endsWith(".runfiles_manifest")) {
String path = manifest.substring(0, manifest.length() - 9);
if (new File(path).isDirectory()) {
return path;
}
}
return "";
}

@Override
public String rlocationChecked(String path) {
return runfiles.get(path);
}

@Override
public Map<String, String> getEnvVars() {
HashMap<String, String> result = new HashMap<>(4);
result.put("RUNFILES_MANIFEST_ONLY", "1");
result.put("RUNFILES_MANIFEST_FILE", manifestPath);
String runfilesDir = findRunfilesDir(manifestPath);
result.put("RUNFILES_DIR", runfilesDir);
// TODO(laszlocsomor): remove JAVA_RUNFILES once the Java launcher can pick up RUNFILES_DIR.
result.put("JAVA_RUNFILES", runfilesDir);
return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@
* Runfiles runfiles = Runfiles.create();
* File p = new File(runfiles.rlocation("io_bazel/src/bazel"));
* </pre>
*
* <p>If you want to start subprocesses that also need runfiles, you need to set the right
* environment variables for them:
*
* <pre>
* String path = r.rlocation("path/to/binary");
* ProcessBuilder pb = new ProcessBuilder(path);
* pb.environment().putAll(r.getEnvVars());
* ...
* Process p = pb.start();
* </pre>
*/
public abstract class Runfiles {

Expand Down Expand Up @@ -103,6 +114,14 @@ public final String rlocation(String path) {
return rlocationChecked(path);
}

/**
* Returns environment variables for subprocesses.
*
* <p>The caller should add the returned key-value pairs to the environment of subprocesses in
* case those subprocesses are also Bazel-built binaries that need to use runfiles.
*/
public abstract Map<String, String> getEnvVars();

/** Returns true if the platform supports runfiles only via manifests. */
private static boolean isManifestOnly(Map<String, String> env) {
return "1".equals(env.get("RUNFILES_MANIFEST_ONLY"));
Expand Down
Loading

0 comments on commit ef247ef

Please sign in to comment.