From 5681f9d19f66e6d8bab382d9fe57484f4c120f9b Mon Sep 17 00:00:00 2001 From: laszlocsomor Date: Fri, 10 Aug 2018 04:25:08 -0700 Subject: Java, runfiles: move runfiles library sources Move the Java runfiles library's sources from //src/tools/runfiles/j/c/g/devtools/build/runfiles:* to //tools/java/runfiles. Fixes https://github.com/bazelbuild/bazel/issues/5803 RELNOTES[NEW]: Java, runfiles: the Java runfiles library is now in @bazel_tools//tools/java/runfiles. The old target (@bazel_tools//tools/runfiles:java-runfiles) is deprecated and will be removed in Bazel 0.18.0. PiperOrigin-RevId: 208191521 --- tools/java/runfiles/BUILD | 59 ++++++- tools/java/runfiles/BUILD.tools | 8 +- tools/java/runfiles/MockFile.java | 45 +++++ tools/java/runfiles/Runfiles.java | 278 ++++++++++++++++++++++++++++++ tools/java/runfiles/RunfilesTest.java | 312 ++++++++++++++++++++++++++++++++++ tools/java/runfiles/Util.java | 49 ++++++ tools/java/runfiles/UtilTest.java | 54 ++++++ tools/runfiles/BUILD.tools | 7 +- 8 files changed, 801 insertions(+), 11 deletions(-) create mode 100644 tools/java/runfiles/MockFile.java create mode 100644 tools/java/runfiles/Runfiles.java create mode 100644 tools/java/runfiles/RunfilesTest.java create mode 100644 tools/java/runfiles/Util.java create mode 100644 tools/java/runfiles/UtilTest.java (limited to 'tools') diff --git a/tools/java/runfiles/BUILD b/tools/java/runfiles/BUILD index f81acb3e15..b69fe341f7 100644 --- a/tools/java/runfiles/BUILD +++ b/tools/java/runfiles/BUILD @@ -2,17 +2,68 @@ package(default_visibility = ["//visibility:private"]) filegroup( name = "srcs", + srcs = glob( + ["**"], + exclude = [".*"], # .swp files and such + ), + visibility = ["//tools/java:__pkg__"], +) + +filegroup( + name = "embedded_tools", srcs = [ - "BUILD", "BUILD.tools", + ":java-srcs", ], visibility = ["//tools/java:__pkg__"], ) filegroup( - name = "embedded_tools", - srcs = ["BUILD.tools"], - visibility = ["//tools/java:__pkg__"], + name = "java-srcs", + srcs = [ + "Runfiles.java", + "Util.java", + ], +) + +java_library( + name = "runfiles", + srcs = [":java-srcs"], +) + +java_test( + name = "RunfilesTest", + srcs = ["RunfilesTest.java"], + test_class = "com.google.devtools.build.runfiles.RunfilesTest", + deps = [":test_deps"], +) + +java_test( + name = "UtilTest", + srcs = ["UtilTest.java"], + test_class = "com.google.devtools.build.runfiles.UtilTest", + deps = [":test_deps"], +) + +java_library( + name = "test_deps", + testonly = 1, + exports = [ + ":mock-file", + ":runfiles", + "//third_party:guava", + "//third_party:guava-testlib", + "//third_party:junit4", + "//third_party:truth", + ], +) + +java_library( + name = "mock-file", + testonly = 1, + srcs = ["MockFile.java"], + exports = ["//third_party:guava"], + deps = ["//third_party:guava"], ) test_suite( diff --git a/tools/java/runfiles/BUILD.tools b/tools/java/runfiles/BUILD.tools index 011a549ba3..a2ac4f58cd 100644 --- a/tools/java/runfiles/BUILD.tools +++ b/tools/java/runfiles/BUILD.tools @@ -1,6 +1,8 @@ -alias( +java_library( name = "runfiles", - # TODO(laszlocsomor): move the sources to this package. - actual = "//src/tools/runfiles/java/com/google/devtools/build/runfiles", + srcs = [ + "Runfiles.java", + "Util.java", + ], visibility = ["//visibility:public"], ) diff --git a/tools/java/runfiles/MockFile.java b/tools/java/runfiles/MockFile.java new file mode 100644 index 0000000000..886bfcbcc8 --- /dev/null +++ b/tools/java/runfiles/MockFile.java @@ -0,0 +1,45 @@ +// 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. + +package com.google.devtools.build.runfiles; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +final class MockFile implements Closeable { + + public final Path path; + + public MockFile(ImmutableList lines) throws IOException { + String testTmpdir = System.getenv("TEST_TMPDIR"); + if (Strings.isNullOrEmpty(testTmpdir)) { + throw new IOException("$TEST_TMPDIR is empty or undefined"); + } + path = Files.createTempFile(new File(testTmpdir).toPath(), null, null); + Files.write(path, lines, StandardCharsets.UTF_8); + } + + @Override + public void close() throws IOException { + if (path != null) { + Files.delete(path); + } + } +} diff --git a/tools/java/runfiles/Runfiles.java b/tools/java/runfiles/Runfiles.java new file mode 100644 index 0000000000..718b67948d --- /dev/null +++ b/tools/java/runfiles/Runfiles.java @@ -0,0 +1,278 @@ +// 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. + +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; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Runfiles lookup library for Bazel-built Java binaries and tests. + * + *

USAGE: + * + *

1. Depend on this runfiles library from your build rule: + * + *

+ *   java_binary(
+ *       name = "my_binary",
+ *       ...
+ *       deps = ["@bazel_tools//tools/java/runfiles"],
+ *   )
+ * 
+ * + *

2. Import the runfiles library. + * + *

+ *   import com.google.devtools.build.runfiles.Runfiles;
+ * 
+ * + *

3. Create a Runfiles object and use rlocation to look up runfile paths: + * + *

+ *   public void myFunction() {
+ *     Runfiles runfiles = Runfiles.create();
+ *     String path = runfiles.rlocation("my_workspace/path/to/my/data.txt");
+ *     ...
+ * 
+ * + *

If you want to start subprocesses that also need runfiles, you need to set the right + * environment variables for them: + * + *

+ *   String path = r.rlocation("path/to/binary");
+ *   ProcessBuilder pb = new ProcessBuilder(path);
+ *   pb.environment().putAll(r.getEnvVars());
+ *   ...
+ *   Process p = pb.start();
+ * 
+ */ +public abstract class Runfiles { + + // Package-private constructor, so only package-private classes may extend it. + private Runfiles() {} + + /** + * Returns a new {@link Runfiles} instance. + * + *

This method passes the JVM's environment variable map to {@link #create(Map)}. + */ + public static Runfiles create() throws IOException { + return create(System.getenv()); + } + + /** + * Returns a new {@link Runfiles} instance. + * + *

The returned object is either: + * + *

+ * + *

If {@code env} contains "RUNFILES_MANIFEST_ONLY" with value "1", this method returns a + * manifest-based implementation. The manifest's path is defined by the "RUNFILES_MANIFEST_FILE" + * key's value in {@code env}. + * + *

Otherwise this method returns a directory-based implementation. The directory's path is + * defined by the value in {@code env} under the "RUNFILES_DIR" key, or if absent, then under the + * "JAVA_RUNFILES" key. + * + *

Note about performance: the manifest-based implementation eagerly reads and caches the whole + * manifest file upon instantiation. + * + * @throws IOException if RUNFILES_MANIFEST_ONLY=1 is in {@code env} but there's no + * "RUNFILES_MANIFEST_FILE", "RUNFILES_DIR", or "JAVA_RUNFILES" key in {@code env} or their + * values are empty, or some IO error occurs + */ + public static Runfiles create(Map env) throws IOException { + if (isManifestOnly(env)) { + // On Windows, Bazel sets RUNFILES_MANIFEST_ONLY=1. + // On every platform, Bazel also sets RUNFILES_MANIFEST_FILE, but on Linux and macOS it's + // faster to use RUNFILES_DIR. + return new ManifestBased(getManifestPath(env)); + } else { + return new DirectoryBased(getRunfilesDir(env)); + } + } + + /** + * Returns the runtime path of a runfile (a Bazel-built binary's/test's data-dependency). + * + *

The returned path may not be valid. The caller should check the path's validity and that the + * path exists. + * + *

The function may return null. In that case the caller can be sure that the rule does not + * know about this data-dependency. + * + * @param path runfiles-root-relative path of the runfile + * @throws IllegalArgumentException if {@code path} fails validation, for example if it's null or + * empty, or not normalized (contains "./", "../", or "//") + */ + public final String rlocation(String path) { + Util.checkArgument(path != null); + Util.checkArgument(!path.isEmpty()); + Util.checkArgument( + !path.startsWith("../") + && !path.contains("/..") + && !path.startsWith("./") + && !path.contains("/./") + && !path.endsWith("/.") + && !path.contains("//"), + "path is not normalized: \"%s\"", + path); + Util.checkArgument( + !path.startsWith("\\"), "path is absolute without a drive letter: \"%s\"", path); + if (new File(path).isAbsolute()) { + return path; + } + return rlocationChecked(path); + } + + /** + * Returns environment variables for subprocesses. + * + *

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 getEnvVars(); + + /** Returns true if the platform supports runfiles only via manifests. */ + private static boolean isManifestOnly(Map env) { + return "1".equals(env.get("RUNFILES_MANIFEST_ONLY")); + } + + private static String getManifestPath(Map env) throws IOException { + String value = env.get("RUNFILES_MANIFEST_FILE"); + if (Util.isNullOrEmpty(value)) { + throw new IOException( + "Cannot load runfiles manifest: $RUNFILES_MANIFEST_ONLY is 1 but" + + " $RUNFILES_MANIFEST_FILE is empty or undefined"); + } + return value; + } + + private static String getRunfilesDir(Map env) throws IOException { + String value = env.get("RUNFILES_DIR"); + if (Util.isNullOrEmpty(value)) { + value = env.get("JAVA_RUNFILES"); + } + if (Util.isNullOrEmpty(value)) { + throw new IOException( + "Cannot find runfiles: $RUNFILES_DIR and $JAVA_RUNFILES are both unset or empty"); + } + return value; + } + + abstract String rlocationChecked(String path); + + /** {@link Runfiles} implementation that parses a runfiles-manifest file to look up runfiles. */ + private static final class ManifestBased extends Runfiles { + private final Map 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); + } + + private static Map loadRunfiles(String path) throws IOException { + HashMap result = new HashMap<>(); + try (BufferedReader r = + new BufferedReader( + new InputStreamReader(new FileInputStream(path), StandardCharsets.UTF_8))) { + String line = null; + while ((line = r.readLine()) != null) { + int index = line.indexOf(' '); + String runfile = (index == -1) ? line : line.substring(0, index); + String realPath = (index == -1) ? line : line.substring(index + 1); + result.put(runfile, realPath); + } + } + 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 getEnvVars() { + HashMap 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; + } + } + + /** {@link Runfiles} implementation that appends runfiles paths to the runfiles root. */ + private static final class DirectoryBased extends Runfiles { + private final String runfilesRoot; + + 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 getEnvVars() { + HashMap 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; + } + } + + static Runfiles createManifestBasedForTesting(String manifestPath) throws IOException { + return new ManifestBased(manifestPath); + } + + static Runfiles createDirectoryBasedForTesting(String runfilesDir) throws IOException { + return new DirectoryBased(runfilesDir); + } +} diff --git a/tools/java/runfiles/RunfilesTest.java b/tools/java/runfiles/RunfilesTest.java new file mode 100644 index 0000000000..157c62e482 --- /dev/null +++ b/tools/java/runfiles/RunfilesTest.java @@ -0,0 +1,312 @@ +// 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. + +package com.google.devtools.build.runfiles; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.Map; +import javax.annotation.Nullable; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link Runfiles}. */ +@RunWith(JUnit4.class) +public final class RunfilesTest { + + private static boolean isWindows() { + return File.separatorChar == '\\'; + } + + private void assertRlocationArg(Runfiles runfiles, String path, @Nullable String error) + throws Exception { + try { + runfiles.rlocation(path); + fail(); + } catch (IllegalArgumentException e) { + if (error != null) { + assertThat(e).hasMessageThat().contains(error); + } + } + } + + @Test + public void testRlocationArgumentValidation() throws Exception { + Path dir = + Files.createTempDirectory( + FileSystems.getDefault().getPath(System.getenv("TEST_TMPDIR")), null); + + Runfiles r = Runfiles.create(ImmutableMap.of("RUNFILES_DIR", dir.toString())); + assertRlocationArg(r, null, null); + assertRlocationArg(r, "", null); + assertRlocationArg(r, "../foo", "is not normalized"); + assertRlocationArg(r, "foo/..", "is not normalized"); + assertRlocationArg(r, "foo/../bar", "is not normalized"); + assertRlocationArg(r, "./foo", "is not normalized"); + assertRlocationArg(r, "foo/.", "is not normalized"); + assertRlocationArg(r, "foo/./bar", "is not normalized"); + assertRlocationArg(r, "//foobar", "is not normalized"); + assertRlocationArg(r, "foo//", "is not normalized"); + assertRlocationArg(r, "foo//bar", "is not normalized"); + assertRlocationArg(r, "\\foo", "path is absolute without a drive letter"); + } + + @Test + public void testCreatesManifestBasedRunfiles() throws Exception { + try (MockFile mf = new MockFile(ImmutableList.of("a/b c/d"))) { + Runfiles r = + Runfiles.create( + ImmutableMap.of( + "RUNFILES_MANIFEST_ONLY", "1", + "RUNFILES_MANIFEST_FILE", mf.path.toString(), + "RUNFILES_DIR", "ignored when RUNFILES_MANIFEST_ONLY=1", + "JAVA_RUNFILES", "ignored when RUNFILES_DIR has a value", + "TEST_SRCDIR", "should always be ignored")); + assertThat(r.rlocation("a/b")).isEqualTo("c/d"); + assertThat(r.rlocation("foo")).isNull(); + + if (isWindows()) { + assertThat(r.rlocation("c:/foo")).isEqualTo("c:/foo"); + assertThat(r.rlocation("c:\\foo")).isEqualTo("c:\\foo"); + } else { + assertThat(r.rlocation("/foo")).isEqualTo("/foo"); + } + } + } + + @Test + public void testCreatesDirectoryBasedRunfiles() throws Exception { + Path dir = + Files.createTempDirectory( + FileSystems.getDefault().getPath(System.getenv("TEST_TMPDIR")), null); + + Runfiles r = + Runfiles.create( + ImmutableMap.of( + "RUNFILES_MANIFEST_FILE", "ignored when RUNFILES_MANIFEST_ONLY is not set to 1", + "RUNFILES_DIR", dir.toString(), + "JAVA_RUNFILES", "ignored when RUNFILES_DIR has a value", + "TEST_SRCDIR", "should always be ignored")); + assertThat(r.rlocation("a/b")).endsWith("/a/b"); + assertThat(r.rlocation("foo")).endsWith("/foo"); + + r = + Runfiles.create( + ImmutableMap.of( + "RUNFILES_MANIFEST_FILE", "ignored when RUNFILES_MANIFEST_ONLY is not set to 1", + "RUNFILES_DIR", "", + "JAVA_RUNFILES", dir.toString(), + "TEST_SRCDIR", "should always be ignored")); + assertThat(r.rlocation("a/b")).endsWith("/a/b"); + assertThat(r.rlocation("foo")).endsWith("/foo"); + } + + @Test + public void testIgnoresTestSrcdirWhenJavaRunfilesIsUndefinedAndJustFails() throws Exception { + Path dir = + Files.createTempDirectory( + FileSystems.getDefault().getPath(System.getenv("TEST_TMPDIR")), null); + + Runfiles.create( + ImmutableMap.of( + "RUNFILES_DIR", dir.toString(), + "RUNFILES_MANIFEST_FILE", "ignored when RUNFILES_MANIFEST_ONLY is not set to 1", + "TEST_SRCDIR", "should always be ignored")); + + Runfiles.create( + ImmutableMap.of( + "JAVA_RUNFILES", dir.toString(), + "RUNFILES_MANIFEST_FILE", "ignored when RUNFILES_MANIFEST_ONLY is not set to 1", + "TEST_SRCDIR", "should always be ignored")); + + try { + // The method must ignore TEST_SRCDIR, for the scenario when Bazel runs a test which itself + // runs Bazel to build and run java_binary. The java_binary should not pick up the test's + // TEST_SRCDIR. + Runfiles.create( + ImmutableMap.of( + "RUNFILES_DIR", "", + "JAVA_RUNFILES", "", + "RUNFILES_MANIFEST_FILE", "ignored when RUNFILES_MANIFEST_ONLY is not set to 1", + "TEST_SRCDIR", "should always be ignored")); + fail(); + } catch (IOException e) { + assertThat(e).hasMessageThat().contains("$RUNFILES_DIR and $JAVA_RUNFILES"); + } + } + + @Test + public void testFailsToCreateManifestBasedBecauseManifestDoesNotExist() throws Exception { + try { + Runfiles.create( + ImmutableMap.of( + "RUNFILES_MANIFEST_ONLY", "1", + "RUNFILES_MANIFEST_FILE", "non-existing path")); + fail(); + } catch (IOException e) { + assertThat(e).hasMessageThat().contains("non-existing path"); + } + } + + @Test + public void testManifestBasedEnvVars() throws Exception { + Path dir = + Files.createTempDirectory( + FileSystems.getDefault().getPath(System.getenv("TEST_TMPDIR")), null); + + Path mf = dir.resolve("MANIFEST"); + Files.write(mf, Collections.emptyList(), StandardCharsets.UTF_8); + Map envvars = + Runfiles.create( + ImmutableMap.of( + "RUNFILES_MANIFEST_ONLY", "1", + "RUNFILES_MANIFEST_FILE", mf.toString(), + "RUNFILES_DIR", "ignored when RUNFILES_MANIFEST_ONLY=1", + "JAVA_RUNFILES", "ignored when RUNFILES_DIR has a value", + "TEST_SRCDIR", "should always be ignored")) + .getEnvVars(); + assertThat(envvars.keySet()) + .containsExactly( + "RUNFILES_MANIFEST_ONLY", "RUNFILES_MANIFEST_FILE", "RUNFILES_DIR", "JAVA_RUNFILES"); + assertThat(envvars.get("RUNFILES_MANIFEST_ONLY")).isEqualTo("1"); + assertThat(envvars.get("RUNFILES_MANIFEST_FILE")).isEqualTo(mf.toString()); + assertThat(envvars.get("RUNFILES_DIR")).isEqualTo(dir.toString()); + assertThat(envvars.get("JAVA_RUNFILES")).isEqualTo(dir.toString()); + + Path rfDir = dir.resolve("foo.runfiles"); + Files.createDirectories(rfDir); + mf = dir.resolve("foo.runfiles_manifest"); + Files.write(mf, Collections.emptyList(), StandardCharsets.UTF_8); + envvars = + Runfiles.create( + ImmutableMap.of( + "RUNFILES_MANIFEST_ONLY", "1", + "RUNFILES_MANIFEST_FILE", mf.toString(), + "RUNFILES_DIR", "ignored when RUNFILES_MANIFEST_ONLY=1", + "JAVA_RUNFILES", "ignored when RUNFILES_DIR has a value", + "TEST_SRCDIR", "should always be ignored")) + .getEnvVars(); + assertThat(envvars.get("RUNFILES_MANIFEST_ONLY")).isEqualTo("1"); + assertThat(envvars.get("RUNFILES_MANIFEST_FILE")).isEqualTo(mf.toString()); + assertThat(envvars.get("RUNFILES_DIR")).isEqualTo(rfDir.toString()); + assertThat(envvars.get("JAVA_RUNFILES")).isEqualTo(rfDir.toString()); + } + + @Test + public void testDirectoryBasedEnvVars() throws Exception { + Path dir = + Files.createTempDirectory( + FileSystems.getDefault().getPath(System.getenv("TEST_TMPDIR")), null); + + Map envvars = + Runfiles.create( + ImmutableMap.of( + "RUNFILES_MANIFEST_FILE", + "ignored when RUNFILES_MANIFEST_ONLY is not set to 1", + "RUNFILES_DIR", + dir.toString(), + "JAVA_RUNFILES", + "ignored when RUNFILES_DIR has a value", + "TEST_SRCDIR", + "should always be ignored")) + .getEnvVars(); + assertThat(envvars.keySet()).containsExactly("RUNFILES_DIR", "JAVA_RUNFILES"); + assertThat(envvars.get("RUNFILES_DIR")).isEqualTo(dir.toString()); + assertThat(envvars.get("JAVA_RUNFILES")).isEqualTo(dir.toString()); + } + + @Test + public void testDirectoryBasedRlocation() 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. + File dir = new File(System.getenv("TEST_TMPDIR"), "mock/runfiles"); + assertThat(dir.mkdirs()).isTrue(); + Runfiles r = Runfiles.createDirectoryBasedForTesting(dir.toString()); + // Escaping for "\": once for string and once for regex. + assertThat(r.rlocation("arg")).matches(".*[/\\\\]mock[/\\\\]runfiles[/\\\\]arg"); + } + + @Test + public void testManifestBasedRlocation() throws Exception { + try (MockFile mf = + new MockFile( + ImmutableList.of( + "Foo/runfile1 C:/Actual Path\\runfile1", + "Foo/Bar/runfile2 D:\\the path\\run file 2.txt"))) { + Runfiles r = Runfiles.createManifestBasedForTesting(mf.path.toString()); + assertThat(r.rlocation("Foo/runfile1")).isEqualTo("C:/Actual Path\\runfile1"); + assertThat(r.rlocation("Foo/Bar/runfile2")).isEqualTo("D:\\the path\\run file 2.txt"); + assertThat(r.rlocation("unknown")).isNull(); + } + } + + @Test + public void testDirectoryBasedCtorArgumentValidation() throws Exception { + try { + Runfiles.createDirectoryBasedForTesting(null); + fail(); + } catch (IllegalArgumentException e) { + // expected + } + + try { + Runfiles.createDirectoryBasedForTesting(""); + fail(); + } catch (IllegalArgumentException e) { + // expected + } + + try { + Runfiles.createDirectoryBasedForTesting("non-existent directory is bad"); + fail(); + } catch (IllegalArgumentException e) { + // expected + } + + Runfiles.createDirectoryBasedForTesting(System.getenv("TEST_TMPDIR")); + } + + @Test + public void testManifestBasedCtorArgumentValidation() throws Exception { + try { + Runfiles.createManifestBasedForTesting(null); + fail(); + } catch (IllegalArgumentException e) { + // expected + } + + try { + Runfiles.createManifestBasedForTesting(""); + fail(); + } catch (IllegalArgumentException e) { + // expected + } + + try (MockFile mf = new MockFile(ImmutableList.of("a b"))) { + Runfiles.createManifestBasedForTesting(mf.path.toString()); + } + } +} diff --git a/tools/java/runfiles/Util.java b/tools/java/runfiles/Util.java new file mode 100644 index 0000000000..73f0b98eeb --- /dev/null +++ b/tools/java/runfiles/Util.java @@ -0,0 +1,49 @@ +// 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. + +package com.google.devtools.build.runfiles; + +/** + * Utilities for the other classes in this package. + * + *

These functions are implementations of some basic utilities in the Guava library. We + * reimplement these functions instead of depending on Guava, so that the Runfiles library has no + * third-party dependencies, thus any Java project can depend on it without the risk of pulling + * unwanted or conflicting dependencies (for example if the project already depends on Guava, or + * wishes not to depend on it at all). + */ +class Util { + private Util() {} + + /** Returns true when {@code s} is null or an empty string. */ + public static boolean isNullOrEmpty(String s) { + return s == null || s.isEmpty(); + } + + /** Throws an {@code IllegalArgumentException} if {@code condition} is false. */ + public static void checkArgument(boolean condition) { + checkArgument(condition, null, null); + } + + /** Throws an {@code IllegalArgumentException} if {@code condition} is false. */ + public static void checkArgument(boolean condition, String error, Object arg1) { + if (!condition) { + if (isNullOrEmpty(error)) { + throw new IllegalArgumentException("argument validation failed"); + } else { + throw new IllegalArgumentException(String.format(error, arg1)); + } + } + } +} diff --git a/tools/java/runfiles/UtilTest.java b/tools/java/runfiles/UtilTest.java new file mode 100644 index 0000000000..48a5f9446c --- /dev/null +++ b/tools/java/runfiles/UtilTest.java @@ -0,0 +1,54 @@ +// 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. + +package com.google.devtools.build.runfiles; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link Util}. */ +@RunWith(JUnit4.class) +public final class UtilTest { + + @Test + public void testIsNullOrEmpty() throws Exception { + assertThat(Util.isNullOrEmpty(null)).isTrue(); + assertThat(Util.isNullOrEmpty("")).isTrue(); + assertThat(Util.isNullOrEmpty("\0")).isFalse(); + assertThat(Util.isNullOrEmpty("some text")).isFalse(); + } + + @Test + public void testCheckArgument() throws Exception { + Util.checkArgument(true, null, null); + + try { + Util.checkArgument(false, null, null); + fail("expected failure"); + } catch (IllegalArgumentException e) { + assertThat(e).hasMessageThat().isEqualTo("argument validation failed"); + } + + try { + Util.checkArgument(false, "foo-%s", 42); + fail("expected failure"); + } catch (IllegalArgumentException e) { + assertThat(e).hasMessageThat().isEqualTo("foo-42"); + } + } +} diff --git a/tools/runfiles/BUILD.tools b/tools/runfiles/BUILD.tools index 6f107ea3e7..04c8e6fc3d 100644 --- a/tools/runfiles/BUILD.tools +++ b/tools/runfiles/BUILD.tools @@ -1,7 +1,6 @@ -package(default_visibility = ["//visibility:public"]) - java_library( name = "java-runfiles", - exports = ["//src/tools/runfiles/java/com/google/devtools/build/runfiles"], - deprecation = "Depend on @bazel_tools//tools/java/runfiles instead. This target goes away in Bazel release 0.18.0", + exports = ["//tools/java/runfiles"], + visibility = ["//visibility:public"], + deprecation = "Depend on @bazel_tools//tools/java/runfiles instead. This target will be deleted in Bazel 0.18.0", ) -- cgit v1.2.3