From ff523801682cbde571cdd8f5c360fb880bf0f518 Mon Sep 17 00:00:00 2001 From: Fabian Meumertzheim Date: Thu, 31 Oct 2024 09:29:04 +0100 Subject: [PATCH] Copy over runfiles library from Bazel The runfiles library can be maintained independently of Bazel releases and `bazel_tools` can refer to it via an alias. Also set flags to build and test with a hermetic JDK 8 to ensure compatibility with that version. --- .gitignore | 2 + MODULE.bazel | 9 +- WORKSPACE | 4 + java/runfiles/BUILD | 5 + .../build/runfiles/AutoBazelRepository.java | 29 + .../AutoBazelRepositoryProcessor.java | 121 ++++ .../com/google/devtools/build/runfiles/BUILD | 23 + .../devtools/build/runfiles/Runfiles.java | 582 +++++++++++++++++ .../google/devtools/build/runfiles/Util.java | 49 ++ test/repositories.bzl | 17 + .../com/google/devtools/build/runfiles/BUILD | 29 + .../devtools/build/runfiles/RunfilesTest.java | 601 ++++++++++++++++++ .../devtools/build/runfiles/UtilTest.java | 47 ++ 13 files changed, 1515 insertions(+), 3 deletions(-) create mode 100644 java/runfiles/BUILD create mode 100644 java/runfiles/src/main/java/com/google/devtools/build/runfiles/AutoBazelRepository.java create mode 100644 java/runfiles/src/main/java/com/google/devtools/build/runfiles/AutoBazelRepositoryProcessor.java create mode 100644 java/runfiles/src/main/java/com/google/devtools/build/runfiles/BUILD create mode 100644 java/runfiles/src/main/java/com/google/devtools/build/runfiles/Runfiles.java create mode 100644 java/runfiles/src/main/java/com/google/devtools/build/runfiles/Util.java create mode 100644 test/repositories.bzl create mode 100644 test/runfiles/src/test/java/com/google/devtools/build/runfiles/BUILD create mode 100644 test/runfiles/src/test/java/com/google/devtools/build/runfiles/RunfilesTest.java create mode 100644 test/runfiles/src/test/java/com/google/devtools/build/runfiles/UtilTest.java diff --git a/.gitignore b/.gitignore index ef436251..86e15abe 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,5 @@ # Ignore jekyll build output. /production /.sass-cache +# Ignore MODULE.bazel.lock as this is a library project. +MODULE.bazel.lock diff --git a/MODULE.bazel b/MODULE.bazel index 77f7bb58..ec262421 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -89,11 +89,14 @@ REMOTE_JDK_REPOS = [(("remote_jdk" if version == "8" else "remotejdk") + version [register_toolchains("@" + name + "_toolchain_config_repo//:all") for name in REMOTE_JDK_REPOS] +# Compatibility layer +compat = use_extension("//java:extensions.bzl", "compatibility_proxy") +use_repo(compat, "compatibility_proxy") + # Dev dependencies bazel_dep(name = "rules_pkg", version = "0.9.1", dev_dependency = True) bazel_dep(name = "stardoc", version = "0.7.1", dev_dependency = True) bazel_dep(name = "rules_shell", version = "0.2.0", dev_dependency = True) -# Compatibility layer -compat = use_extension("//java:extensions.bzl", "compatibility_proxy") -use_repo(compat, "compatibility_proxy") +test_repositories = use_extension("//test:repositories.bzl", "test_repositories_ext", dev_dependency = True) +use_repo(test_repositories, "guava", "truth") diff --git a/WORKSPACE b/WORKSPACE index 49422df8..f58f31c2 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -50,3 +50,7 @@ rules_java_toolchains() load("@stardoc//:setup.bzl", "stardoc_repositories") stardoc_repositories() + +load("//test:repositories.bzl", "test_repositories") + +test_repositories() diff --git a/java/runfiles/BUILD b/java/runfiles/BUILD new file mode 100644 index 00000000..792e45f7 --- /dev/null +++ b/java/runfiles/BUILD @@ -0,0 +1,5 @@ +alias( + name = "runfiles", + actual = "//java/runfiles/src/main/java/com/google/devtools/build/runfiles", + visibility = ["//visibility:public"], +) diff --git a/java/runfiles/src/main/java/com/google/devtools/build/runfiles/AutoBazelRepository.java b/java/runfiles/src/main/java/com/google/devtools/build/runfiles/AutoBazelRepository.java new file mode 100644 index 00000000..6dc53300 --- /dev/null +++ b/java/runfiles/src/main/java/com/google/devtools/build/runfiles/AutoBazelRepository.java @@ -0,0 +1,29 @@ +// Copyright 2022 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.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotating a class {@code Fooer} with this annotation generates a class {@code + * AutoBazelRepository_Fooer} defining a {@link String} constant {@code NAME} containing the + * canonical name of the repository containing the Bazel target that compiled the annotated class. + */ +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.TYPE) +public @interface AutoBazelRepository {} diff --git a/java/runfiles/src/main/java/com/google/devtools/build/runfiles/AutoBazelRepositoryProcessor.java b/java/runfiles/src/main/java/com/google/devtools/build/runfiles/AutoBazelRepositoryProcessor.java new file mode 100644 index 00000000..2b0ce9d2 --- /dev/null +++ b/java/runfiles/src/main/java/com/google/devtools/build/runfiles/AutoBazelRepositoryProcessor.java @@ -0,0 +1,121 @@ +// Copyright 2022 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.IOException; +import java.io.PrintWriter; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Set; +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.annotation.processing.SupportedOptions; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; +import javax.tools.Diagnostic.Kind; + +/** Processor for {@link AutoBazelRepository}. */ +@SupportedAnnotationTypes("com.google.devtools.build.runfiles.AutoBazelRepository") +@SupportedOptions(AutoBazelRepositoryProcessor.BAZEL_REPOSITORY_OPTION) +public final class AutoBazelRepositoryProcessor extends AbstractProcessor { + + static final String BAZEL_REPOSITORY_OPTION = "bazel.repository"; + + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latestSupported(); + } + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + annotations.stream() + .flatMap(element -> roundEnv.getElementsAnnotatedWith(element).stream()) + .map(element -> (TypeElement) element) + .forEach(this::emitClass); + return false; + } + + private void emitClass(TypeElement annotatedClass) { + // This option is always provided by the Java rule implementations. + if (!processingEnv.getOptions().containsKey(BAZEL_REPOSITORY_OPTION)) { + processingEnv + .getMessager() + .printMessage( + Kind.ERROR, + String.format( + "The %1$s annotation processor option is not set. To use this annotation" + + " processor, provide the canonical repository name of the current target as" + + " the value of the -A%1$s flag.", + BAZEL_REPOSITORY_OPTION), + annotatedClass); + return; + } + String repositoryName = processingEnv.getOptions().get(BAZEL_REPOSITORY_OPTION); + if (repositoryName == null) { + // javac translates '-Abazel.repository=' into a null value. + // https://github.com/openjdk/jdk/blob/7a49c9baa1d4ad7df90e7ca626ec48ba76881822/src/jdk.compiler/share/classes/com/sun/tools/javac/processing/JavacProcessingEnvironment.java#L651 + repositoryName = ""; + } + + // For a nested class Outer.Middle.Inner, generate a class with simple name + // AutoBazelRepository_Outer_Middle_Inner. + // Note: There can be collisions when local classes are involved, but since the definition of a + // class depends only on the containing Bazel target, this does not result in ambiguity. + Deque classNameSegments = new ArrayDeque<>(); + Element element = annotatedClass; + while (element instanceof TypeElement) { + classNameSegments.addFirst(element.getSimpleName().toString()); + element = element.getEnclosingElement(); + } + classNameSegments.addFirst("AutoBazelRepository"); + String generatedClassSimpleName = String.join("_", classNameSegments); + + String generatedClassPackage = + processingEnv.getElementUtils().getPackageOf(annotatedClass).getQualifiedName().toString(); + + String generatedClassName = + generatedClassPackage.isEmpty() + ? generatedClassSimpleName + : generatedClassPackage + "." + generatedClassSimpleName; + + try (PrintWriter out = + new PrintWriter( + processingEnv.getFiler().createSourceFile(generatedClassName).openWriter())) { + if (!generatedClassPackage.isEmpty()) { + // This annotation may exist on a class which is at the root package + out.printf("package %s;\n", generatedClassPackage); + } + out.printf("\n"); + out.printf("class %s {\n", generatedClassSimpleName); + out.printf(" /**\n"); + out.printf(" * The canonical name of the repository containing the Bazel target that\n"); + out.printf(" * compiled {@link %s}.\n", annotatedClass.getQualifiedName().toString()); + out.printf(" */\n"); + out.printf(" static final String NAME = \"%s\";\n", repositoryName); + out.printf("\n"); + out.printf(" private %s() {}\n", generatedClassSimpleName); + out.printf("}\n"); + } catch (IOException e) { + processingEnv + .getMessager() + .printMessage( + Kind.ERROR, + String.format("Failed to generate %s: %s", generatedClassName, e.getMessage()), + annotatedClass); + } + } +} diff --git a/java/runfiles/src/main/java/com/google/devtools/build/runfiles/BUILD b/java/runfiles/src/main/java/com/google/devtools/build/runfiles/BUILD new file mode 100644 index 00000000..fd351b73 --- /dev/null +++ b/java/runfiles/src/main/java/com/google/devtools/build/runfiles/BUILD @@ -0,0 +1,23 @@ +load("@rules_java//java:defs.bzl", "java_library", "java_plugin") + +java_library( + name = "runfiles", + srcs = [ + "Runfiles.java", + "Util.java", + ], + exported_plugins = [":auto_bazel_repository_processor"], + visibility = ["//java/runfiles:__pkg__"], + exports = [":auto_bazel_repository"], +) + +java_library( + name = "auto_bazel_repository", + srcs = ["AutoBazelRepository.java"], +) + +java_plugin( + name = "auto_bazel_repository_processor", + srcs = ["AutoBazelRepositoryProcessor.java"], + processor_class = "com.google.devtools.build.runfiles.AutoBazelRepositoryProcessor", +) diff --git a/java/runfiles/src/main/java/com/google/devtools/build/runfiles/Runfiles.java b/java/runfiles/src/main/java/com/google/devtools/build/runfiles/Runfiles.java new file mode 100644 index 00000000..bec2091a --- /dev/null +++ b/java/runfiles/src/main/java/com/google/devtools/build/runfiles/Runfiles.java @@ -0,0 +1,582 @@ +// 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.lang.ref.SoftReference; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * 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 = ["@rules_java//java/runfiles"],
+ *   )
+ * 
+ * + *

2. Import the runfiles library. + * + *

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

3. Create a {@link Runfiles.Preloaded} object: + * + *

+ *   public void myFunction() {
+ *     Runfiles.Preloaded runfiles = Runfiles.preload();
+ *     ...
+ * 
+ * + *

4. To look up a runfile, use either of the following approaches: + * + *

4a. Annotate the class from which runfiles should be looked up with {@link + * AutoBazelRepository} and obtain the name of the Bazel repository containing the class from a + * constant generated by this annotation: + * + *

+ *   import com.google.devtools.build.runfiles.AutoBazelRepository;
+ *   @AutoBazelRepository
+ *   public class MyClass {
+ *     public void myFunction() {
+ *       Runfiles.Preloaded runfiles = Runfiles.preload();
+ *       String path = runfiles.withSourceRepository(AutoBazelRepository_MyClass.NAME)
+ *                             .rlocation("my_workspace/path/to/my/data.txt");
+ *       ...
+ *
+ * 
+ * + *

4b. Let Bazel compute the path passed to rlocation and pass it into a java_binary + * via an argument or an environment variable: + * + *

+ *   java_binary(
+ *       name = "my_binary",
+ *       srcs = ["MyClass.java"],
+ *       data = ["@my_workspace//path/to/my:data.txt"],
+ *       env = {"MY_RUNFILE": "$(rlocationpath @my_workspace//path/to/my:data.txt)"},
+ *   )
+ * 
+ * + *
+ *   public class MyClass {
+ *     public void myFunction() {
+ *       Runfiles.Preloaded runfiles = Runfiles.preload();
+ *       String path = runfiles.unmapped().rlocation(System.getenv("MY_RUNFILE"));
+ *       ...
+ *
+ * 
+ * + * For more details on why it is required to pass in the current repository name, see {@see + * https://bazel.build/build/bzlmod#repository-names}. + * + *

Subprocesses

+ * + *

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();
+ * 
+ * + *

{@link Runfiles.Preloaded} vs. {@link Runfiles}

+ * + *

Instances of {@link Runfiles.Preloaded} are meant to be stored and passed around to other + * components that need to access runfiles. They are created by calling {@link Runfiles#preload()} + * {@link Runfiles#preload(java.util.Map)} and immutably encapsulate all data required to look up + * runfiles with the repository mapping of any Bazel repository specified at a later time. + * + *

Creating {@link Runfiles.Preloaded} instances can be costly, so applications should try to + * create as few instances as possible. {@link Runfiles#preload()}, but not {@link + * Runfiles#preload(java.util.Map)}, returns a single global, softly cached instance of {@link + * Runfiles.Preloaded} that is constructed based on the JVM's environment variables. + * + *

Instance of {@link Runfiles} are only meant to be used by code located in a single Bazel + * repository and should not be passed around. They are created by calling {@link + * Runfiles.Preloaded#withSourceRepository(String)} or {@link Runfiles.Preloaded#unmapped()} and in + * addition to the data in {@link Runfiles.Preloaded} also fix a source repository relative to which + * apparent repository names are resolved. + * + *

Creating {@link Runfiles.Preloaded} instances is cheap. + */ +public final class Runfiles { + + /** + * A class that encapsulates all data required to look up runfiles relative to any Bazel + * repository fixed at a later time. + * + *

This class is immutable. + */ + public abstract static class Preloaded { + + /** See {@link com.google.devtools.build.lib.analysis.RepoMappingManifestAction.Entry}. */ + static class RepoMappingKey { + + public final String sourceRepo; + public final String targetRepoApparentName; + + public RepoMappingKey(String sourceRepo, String targetRepoApparentName) { + this.sourceRepo = sourceRepo; + this.targetRepoApparentName = targetRepoApparentName; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || !(o instanceof RepoMappingKey)) { + return false; + } + RepoMappingKey that = (RepoMappingKey) o; + return sourceRepo.equals(that.sourceRepo) + && targetRepoApparentName.equals(that.targetRepoApparentName); + } + + @Override + public int hashCode() { + return Objects.hash(sourceRepo, targetRepoApparentName); + } + } + + /** + * Returns a {@link Runfiles} instance that uses the provided source repository's repository + * mapping to translate apparent into canonical repository names. + * + *

{@see https://bazel.build/build/bzlmod#repository-names} + * + * @param sourceRepository the canonical name of the Bazel repository relative to which apparent + * repository names should be resolved. Should generally coincide with the Bazel repository + * that contains the caller of this method, which can be obtained via {@link + * AutoBazelRepository}. + * @return a {@link Runfiles} instance that looks up runfiles relative to the provided source + * repository and shares all other data with this {@link Runfiles.Preloaded} instance. + */ + public final Runfiles withSourceRepository(String sourceRepository) { + Util.checkArgument(sourceRepository != null); + return new Runfiles(this, sourceRepository); + } + + /** + * Returns a {@link Runfiles} instance backed by the preloaded runfiles data that can be used to + * look up runfiles paths with canonical repository names only. + * + * @return a {@link Runfiles} instance that can only look up paths with canonical repository + * names and shared all data with this {@link Runfiles.Preloaded} instance. + */ + public final Runfiles unmapped() { + return new Runfiles(this, null); + } + + protected abstract Map getEnvVars(); + + protected abstract String rlocationChecked(String path); + + protected abstract Map getRepoMapping(); + + // Private constructor, so only nested classes may extend it. + private Preloaded() {} + } + + private static final String MAIN_REPOSITORY = ""; + + private static SoftReference defaultInstance = new SoftReference<>(null); + + private final Preloaded preloadedRunfiles; + private final String sourceRepository; + + private Runfiles(Preloaded preloadedRunfiles, String sourceRepository) { + this.preloadedRunfiles = preloadedRunfiles; + this.sourceRepository = sourceRepository; + } + + /** + * Returns the softly cached global {@link Runfiles.Preloaded} instance, creating it if needed. + * + *

This method passes the JVM's environment variable map to {@link #create(java.util.Map)}. + */ + public static synchronized Preloaded preload() throws IOException { + Preloaded instance = defaultInstance.get(); + if (instance != null) { + return instance; + } + instance = preload(System.getenv()); + defaultInstance = new SoftReference<>(instance); + return instance; + } + + /** + * Returns a new {@link Runfiles.Preloaded} 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 java.io.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 Preloaded preload(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 a new {@link Runfiles} instance. + * + *

This method passes the JVM's environment variable map to {@link #create(java.util.Map)}. + * + * @deprecated Use {@link #preload()} instead. With {@code --enable_bzlmod}, this function does + * not work correctly. + */ + @Deprecated + public static Runfiles create() throws IOException { + return preload().withSourceRepository(MAIN_REPOSITORY); + } + + /** + * 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 + * @deprecated Use {@link #preload(java.util.Map)} instead. With {@code --enable_bzlmod}, this + * function does not work correctly. + */ + @Deprecated + public static Runfiles create(Map env) throws IOException { + return preload(env).withSourceRepository(MAIN_REPOSITORY); + } + + /** + * 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 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; + } + + if (sourceRepository == null) { + return preloadedRunfiles.rlocationChecked(path); + } + String[] apparentTargetAndRemainder = path.split("/", 2); + if (apparentTargetAndRemainder.length < 2) { + return preloadedRunfiles.rlocationChecked(path); + } + String targetCanonical = getCanonicalRepositoryName(apparentTargetAndRemainder[0]); + return preloadedRunfiles.rlocationChecked( + targetCanonical + "/" + apparentTargetAndRemainder[1]); + } + + /** + * 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 Map getEnvVars() { + return preloadedRunfiles.getEnvVars(); + } + + String getCanonicalRepositoryName(String apparentRepositoryName) { + return preloadedRunfiles + .getRepoMapping() + .getOrDefault( + new Preloaded.RepoMappingKey(sourceRepository, apparentRepositoryName), + apparentRepositoryName); + } + + /** 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; + } + + private static Map loadRepositoryMapping(String path) + throws IOException { + if (path == null || !new File(path).exists()) { + return Collections.emptyMap(); + } + + try (BufferedReader r = + new BufferedReader( + new InputStreamReader(new FileInputStream(path), StandardCharsets.UTF_8))) { + return Collections.unmodifiableMap( + r.lines() + .filter(line -> !line.isEmpty()) + .map( + line -> { + String[] split = line.split(","); + if (split.length != 3) { + throw new IllegalArgumentException( + "Invalid line in repository mapping: '" + line + "'"); + } + return split; + }) + .collect( + Collectors.toMap( + split -> new Preloaded.RepoMappingKey(split[0], split[1]), + split -> split[2]))); + } + } + + /** {@link Runfiles} implementation that parses a runfiles-manifest file to look up runfiles. */ + private static final class ManifestBased extends Preloaded { + + private final Map runfiles; + private final String manifestPath; + private final Map repoMapping; + + ManifestBased(String manifestPath) throws IOException { + Util.checkArgument(manifestPath != null); + Util.checkArgument(!manifestPath.isEmpty()); + this.manifestPath = manifestPath; + this.runfiles = loadRunfiles(manifestPath); + this.repoMapping = loadRepositoryMapping(rlocationChecked("_repo_mapping")); + } + + @Override + protected String rlocationChecked(String path) { + String exactMatch = runfiles.get(path); + if (exactMatch != null) { + return exactMatch; + } + // If path references a runfile that lies under a directory that itself is a runfile, then + // only the directory is listed in the manifest. Look up all prefixes of path in the manifest + // and append the relative path from the prefix if there is a match. + int prefixEnd = path.length(); + while ((prefixEnd = path.lastIndexOf('/', prefixEnd - 1)) != -1) { + String prefixMatch = runfiles.get(path.substring(0, prefixEnd)); + if (prefixMatch != null) { + return prefixMatch + '/' + path.substring(prefixEnd + 1); + } + } + return null; + } + + @Override + protected 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; + } + + @Override + protected Map getRepoMapping() { + return repoMapping; + } + + 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; + while ((line = r.readLine()) != null) { + String runfile; + String realPath; + if (line.startsWith(" ")) { + // In lines starting with a space, the runfile path contains spaces and backslashes + // escaped with a backslash. The real path is the rest of the line after the first + // unescaped space. + int firstSpace = line.indexOf(' ', 1); + if (firstSpace == -1) { + throw new IOException( + "Invalid runfiles manifest line, expected at least one space after the leading" + + " space: " + + line); + } + runfile = + line.substring(1, firstSpace) + .replace("\\s", " ") + .replace("\\n", "\n") + .replace("\\b", "\\"); + realPath = line.substring(firstSpace + 1).replace("\\n", "\n").replace("\\b", "\\"); + } else { + int firstSpace = line.indexOf(' '); + if (firstSpace == -1) { + throw new IOException( + "Invalid runfiles manifest line, expected at least one space: " + line); + } + runfile = line.substring(0, firstSpace); + realPath = line.substring(firstSpace + 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 ""; + } + } + + /** {@link Runfiles} implementation that appends runfiles paths to the runfiles root. */ + private static final class DirectoryBased extends Preloaded { + + private final String runfilesRoot; + private final Map repoMapping; + + DirectoryBased(String runfilesDir) throws IOException { + Util.checkArgument(!Util.isNullOrEmpty(runfilesDir)); + Util.checkArgument(new File(runfilesDir).isDirectory()); + this.runfilesRoot = runfilesDir; + this.repoMapping = loadRepositoryMapping(rlocationChecked("_repo_mapping")); + } + + @Override + protected String rlocationChecked(String path) { + return runfilesRoot + "/" + path; + } + + @Override + protected Map getRepoMapping() { + return repoMapping; + } + + @Override + protected 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 Preloaded createManifestBasedForTesting(String manifestPath) throws IOException { + return new ManifestBased(manifestPath); + } + + static Preloaded createDirectoryBasedForTesting(String runfilesDir) throws IOException { + return new DirectoryBased(runfilesDir); + } +} diff --git a/java/runfiles/src/main/java/com/google/devtools/build/runfiles/Util.java b/java/runfiles/src/main/java/com/google/devtools/build/runfiles/Util.java new file mode 100644 index 00000000..73f0b98e --- /dev/null +++ b/java/runfiles/src/main/java/com/google/devtools/build/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/test/repositories.bzl b/test/repositories.bzl new file mode 100644 index 00000000..4341eb38 --- /dev/null +++ b/test/repositories.bzl @@ -0,0 +1,17 @@ +"""Test dependencies for rules_java.""" + +load("@bazel_skylib//lib:modules.bzl", "modules") +load("//java:http_jar", "http_jar") + +def test_repositories(): + http_jar( + name = "guava", + url = "https://repo1.maven.org/maven2/com/google/guava/guava/33.3.1-jre/guava-33.3.1-jre.jar", + integrity = "sha256-S/Dixa+ORSXJbo/eF6T3MH+X+EePEcTI41oOMpiuTpA=", + ) + http_jar( + name = "truth", + url = "https://repo1.maven.org/maven2/com/google/truth/truth/1.4.4/truth-1.4.4.jar", + ) + +test_repositories_ext = modules.as_extension(test_repositories) diff --git a/test/runfiles/src/test/java/com/google/devtools/build/runfiles/BUILD b/test/runfiles/src/test/java/com/google/devtools/build/runfiles/BUILD new file mode 100644 index 00000000..cf9b0402 --- /dev/null +++ b/test/runfiles/src/test/java/com/google/devtools/build/runfiles/BUILD @@ -0,0 +1,29 @@ +load("@rules_java//java:defs.bzl", "java_library", "java_test") + +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 = True, + exports = [ + "//java/runfiles", + "@guava//jar", + "@truth//jar", + ], +) diff --git a/test/runfiles/src/test/java/com/google/devtools/build/runfiles/RunfilesTest.java b/test/runfiles/src/test/java/com/google/devtools/build/runfiles/RunfilesTest.java new file mode 100644 index 00000000..035a0b52 --- /dev/null +++ b/test/runfiles/src/test/java/com/google/devtools/build/runfiles/RunfilesTest.java @@ -0,0 +1,601 @@ +// 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.assertThrows; + +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.Map; +import javax.annotation.Nullable; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link Runfiles}. */ +@RunWith(JUnit4.class) +public final class RunfilesTest { + + @Rule + public TemporaryFolder tempDir = new TemporaryFolder(new File(System.getenv("TEST_TMPDIR"))); + + private static boolean isWindows() { + return File.separatorChar == '\\'; + } + + private void assertRlocationArg(Runfiles runfiles, String path, @Nullable String error) { + IllegalArgumentException e = + assertThrows(IllegalArgumentException.class, () -> runfiles.rlocation(path)); + 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 { + Path mf = tempFile("foo.runfiles_manifest", ImmutableList.of("a/b c/d")); + Runfiles r = + 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")); + 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")); + + IOException e = + assertThrows( + IOException.class, + () -> + 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"))); + assertThat(e).hasMessageThat().contains("$RUNFILES_DIR and $JAVA_RUNFILES"); + } + + @Test + public void testFailsToCreateManifestBasedBecauseManifestDoesNotExist() { + IOException e = + assertThrows( + IOException.class, + () -> + Runfiles.create( + ImmutableMap.of( + "RUNFILES_MANIFEST_ONLY", "1", + "RUNFILES_MANIFEST_FILE", "non-existing path"))); + assertThat(e).hasMessageThat().contains("non-existing path"); + } + + @Test + public void testManifestBasedEnvVars() throws Exception { + Path mf = tempFile("MANIFEST", ImmutableList.of()); + 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(tempDir.getRoot().toString()); + assertThat(envvars.get("JAVA_RUNFILES")).isEqualTo(tempDir.getRoot().toString()); + + Path rfDir = tempDir.getRoot().toPath().resolve("foo.runfiles"); + Files.createDirectories(rfDir); + mf = tempFile("foo.runfiles_manifest", ImmutableList.of()); + 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 { + Map envvars = + Runfiles.create( + ImmutableMap.of( + "RUNFILES_MANIFEST_FILE", + "ignored when RUNFILES_MANIFEST_ONLY is not set to 1", + "RUNFILES_DIR", + tempDir.getRoot().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(tempDir.getRoot().toString()); + assertThat(envvars.get("JAVA_RUNFILES")).isEqualTo(tempDir.getRoot().toString()); + } + + @Test + public void testDirectoryBasedRlocation() throws IOException { + // 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()).withSourceRepository(""); + // Escaping for "\": once for string and once for regex. + assertThat(r.rlocation("arg")).matches(".*[/\\\\]mock[/\\\\]runfiles[/\\\\]arg"); + } + + @Test + public void testManifestBasedRlocation() throws Exception { + Path mf = + tempFile( + "MANIFEST", + ImmutableList.of( + "Foo/runfile1 C:/Actual Path\\runfile1", + "Foo/Bar/runfile2 D:\\the path\\run file 2.txt", + "Foo/Bar/Dir E:\\Actual Path\\bDirectory", + " h/\\si F:\\bjk", + " dir\\swith\\sspaces F:\\bj k\\bdir with spaces", + " h/\\s\\n\\bi F:\\bjk\\nb")); + Runfiles r = Runfiles.createManifestBasedForTesting(mf.toString()).withSourceRepository(""); + 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("Foo/Bar/Dir")).isEqualTo("E:\\Actual Path\\bDirectory"); + assertThat(r.rlocation("Foo/Bar/Dir/File")).isEqualTo("E:\\Actual Path\\bDirectory/File"); + assertThat(r.rlocation("Foo/Bar/Dir/Deeply/Nested/File")) + .isEqualTo("E:\\Actual Path\\bDirectory/Deeply/Nested/File"); + assertThat(r.rlocation("Foo/Bar/Dir/Deeply/Nested/File With Spaces")) + .isEqualTo("E:\\Actual Path\\bDirectory/Deeply/Nested/File With Spaces"); + assertThat(r.rlocation("h/ i")).isEqualTo("F:\\jk"); + assertThat(r.rlocation("h/ \n\\i")).isEqualTo("F:\\jk\nb"); + assertThat(r.rlocation("dir with spaces")).isEqualTo("F:\\j k\\dir with spaces"); + assertThat(r.rlocation("dir with spaces/file")).isEqualTo("F:\\j k\\dir with spaces/file"); + assertThat(r.rlocation("unknown")).isNull(); + } + + @Test + public void testManifestBasedRlocationWithRepoMapping_fromMain() throws Exception { + Path rm = + tempFile( + "foo.repo_mapping", + ImmutableList.of( + ",config.json,config.json+1.2.3", + ",my_module,_main", + ",my_protobuf,protobuf+3.19.2", + ",my_workspace,_main", + "protobuf+3.19.2,config.json,config.json+1.2.3", + "protobuf+3.19.2,protobuf,protobuf+3.19.2")); + Path mf = + tempFile( + "foo.runfiles_manifest", + ImmutableList.of( + "_repo_mapping " + rm, + "config.json /etc/config.json", + "protobuf+3.19.2/foo/runfile C:/Actual Path\\protobuf\\runfile", + "_main/bar/runfile /the/path/./to/other//other runfile.txt", + "protobuf+3.19.2/bar/dir E:\\Actual Path\\Directory")); + Runfiles r = Runfiles.createManifestBasedForTesting(mf.toString()).withSourceRepository(""); + + assertThat(r.rlocation("my_module/bar/runfile")) + .isEqualTo("/the/path/./to/other//other runfile.txt"); + assertThat(r.rlocation("my_workspace/bar/runfile")) + .isEqualTo("/the/path/./to/other//other runfile.txt"); + assertThat(r.rlocation("my_protobuf/foo/runfile")) + .isEqualTo("C:/Actual Path\\protobuf\\runfile"); + assertThat(r.rlocation("my_protobuf/bar/dir")).isEqualTo("E:\\Actual Path\\Directory"); + assertThat(r.rlocation("my_protobuf/bar/dir/file")) + .isEqualTo("E:\\Actual Path\\Directory/file"); + assertThat(r.rlocation("my_protobuf/bar/dir/de eply/nes ted/fi+le")) + .isEqualTo("E:\\Actual Path\\Directory/de eply/nes ted/fi+le"); + + assertThat(r.rlocation("protobuf/foo/runfile")).isNull(); + assertThat(r.rlocation("protobuf/bar/dir")).isNull(); + assertThat(r.rlocation("protobuf/bar/dir/file")).isNull(); + assertThat(r.rlocation("protobuf/bar/dir/dir/de eply/nes ted/fi+le")).isNull(); + + assertThat(r.rlocation("_main/bar/runfile")) + .isEqualTo("/the/path/./to/other//other runfile.txt"); + assertThat(r.rlocation("protobuf+3.19.2/foo/runfile")) + .isEqualTo("C:/Actual Path\\protobuf\\runfile"); + assertThat(r.rlocation("protobuf+3.19.2/bar/dir")).isEqualTo("E:\\Actual Path\\Directory"); + assertThat(r.rlocation("protobuf+3.19.2/bar/dir/file")) + .isEqualTo("E:\\Actual Path\\Directory/file"); + assertThat(r.rlocation("protobuf+3.19.2/bar/dir/de eply/nes ted/fi+le")) + .isEqualTo("E:\\Actual Path\\Directory/de eply/nes ted/fi+le"); + + assertThat(r.rlocation("config.json")).isEqualTo("/etc/config.json"); + assertThat(r.rlocation("_main")).isNull(); + assertThat(r.rlocation("my_module")).isNull(); + assertThat(r.rlocation("protobuf")).isNull(); + } + + @Test + public void testManifestBasedRlocationUnmapped() throws Exception { + Path rm = + tempFile( + "foo.repo_mapping", + ImmutableList.of( + ",config.json,config.json+1.2.3", + ",my_module,_main", + ",my_protobuf,protobuf+3.19.2", + ",my_workspace,_main", + "protobuf+3.19.2,config.json,config.json+1.2.3", + "protobuf+3.19.2,protobuf,protobuf+3.19.2")); + Path mf = + tempFile( + "foo.runfiles_manifest", + ImmutableList.of( + "_repo_mapping " + rm, + "config.json /etc/config.json", + "protobuf+3.19.2/foo/runfile C:/Actual Path\\protobuf\\runfile", + "_main/bar/runfile /the/path/./to/other//other runfile.txt", + "protobuf+3.19.2/bar/dir E:\\Actual Path\\Directory")); + Runfiles r = Runfiles.createManifestBasedForTesting(mf.toString()).unmapped(); + + assertThat(r.rlocation("my_module/bar/runfile")).isNull(); + assertThat(r.rlocation("my_workspace/bar/runfile")).isNull(); + assertThat(r.rlocation("my_protobuf/foo/runfile")).isNull(); + assertThat(r.rlocation("my_protobuf/bar/dir")).isNull(); + assertThat(r.rlocation("my_protobuf/bar/dir/file")).isNull(); + assertThat(r.rlocation("my_protobuf/bar/dir/de eply/nes ted/fi+le")).isNull(); + + assertThat(r.rlocation("protobuf/foo/runfile")).isNull(); + assertThat(r.rlocation("protobuf/bar/dir")).isNull(); + assertThat(r.rlocation("protobuf/bar/dir/file")).isNull(); + assertThat(r.rlocation("protobuf/bar/dir/dir/de eply/nes ted/fi+le")).isNull(); + + assertThat(r.rlocation("_main/bar/runfile")) + .isEqualTo("/the/path/./to/other//other runfile.txt"); + assertThat(r.rlocation("protobuf+3.19.2/foo/runfile")) + .isEqualTo("C:/Actual Path\\protobuf\\runfile"); + assertThat(r.rlocation("protobuf+3.19.2/bar/dir")).isEqualTo("E:\\Actual Path\\Directory"); + assertThat(r.rlocation("protobuf+3.19.2/bar/dir/file")) + .isEqualTo("E:\\Actual Path\\Directory/file"); + assertThat(r.rlocation("protobuf+3.19.2/bar/dir/de eply/nes ted/fi+le")) + .isEqualTo("E:\\Actual Path\\Directory/de eply/nes ted/fi+le"); + + assertThat(r.rlocation("config.json")).isEqualTo("/etc/config.json"); + assertThat(r.rlocation("_main")).isNull(); + assertThat(r.rlocation("my_module")).isNull(); + assertThat(r.rlocation("protobuf")).isNull(); + } + + @Test + public void testManifestBasedRlocationWithRepoMapping_fromOtherRepo() throws Exception { + Path rm = + tempFile( + "foo.repo_mapping", + ImmutableList.of( + ",config.json,config.json+1.2.3", + ",my_module,_main", + ",my_protobuf,protobuf+3.19.2", + ",my_workspace,_main", + "protobuf+3.19.2,config.json,config.json+1.2.3", + "protobuf+3.19.2,protobuf,protobuf+3.19.2")); + Path mf = + tempFile( + "foo.runfiles/MANIFEST", + ImmutableList.of( + "_repo_mapping " + rm, + "config.json /etc/config.json", + "protobuf+3.19.2/foo/runfile C:/Actual Path\\protobuf\\runfile", + "_main/bar/runfile /the/path/./to/other//other runfile.txt", + "protobuf+3.19.2/bar/dir E:\\Actual Path\\Directory")); + Runfiles r = + Runfiles.createManifestBasedForTesting(mf.toString()) + .withSourceRepository("protobuf+3.19.2"); + + assertThat(r.rlocation("protobuf/foo/runfile")).isEqualTo("C:/Actual Path\\protobuf\\runfile"); + assertThat(r.rlocation("protobuf/bar/dir")).isEqualTo("E:\\Actual Path\\Directory"); + assertThat(r.rlocation("protobuf/bar/dir/file")).isEqualTo("E:\\Actual Path\\Directory/file"); + assertThat(r.rlocation("protobuf/bar/dir/de eply/nes ted/fi+le")) + .isEqualTo("E:\\Actual Path\\Directory/de eply/nes ted/fi+le"); + + assertThat(r.rlocation("my_module/bar/runfile")).isNull(); + assertThat(r.rlocation("my_protobuf/foo/runfile")).isNull(); + assertThat(r.rlocation("my_protobuf/bar/dir")).isNull(); + assertThat(r.rlocation("my_protobuf/bar/dir/file")).isNull(); + assertThat(r.rlocation("my_protobuf/bar/dir/de eply/nes ted/fi+le")).isNull(); + + assertThat(r.rlocation("_main/bar/runfile")) + .isEqualTo("/the/path/./to/other//other runfile.txt"); + assertThat(r.rlocation("protobuf+3.19.2/foo/runfile")) + .isEqualTo("C:/Actual Path\\protobuf\\runfile"); + assertThat(r.rlocation("protobuf+3.19.2/bar/dir")).isEqualTo("E:\\Actual Path\\Directory"); + assertThat(r.rlocation("protobuf+3.19.2/bar/dir/file")) + .isEqualTo("E:\\Actual Path\\Directory/file"); + assertThat(r.rlocation("protobuf+3.19.2/bar/dir/de eply/nes ted/fi+le")) + .isEqualTo("E:\\Actual Path\\Directory/de eply/nes ted/fi+le"); + + assertThat(r.rlocation("config.json")).isEqualTo("/etc/config.json"); + assertThat(r.rlocation("_main")).isNull(); + assertThat(r.rlocation("my_module")).isNull(); + assertThat(r.rlocation("protobuf")).isNull(); + } + + @Test + public void testDirectoryBasedRlocationWithRepoMapping_fromMain() throws Exception { + Path dir = tempDir.newFolder("foo.runfiles").toPath(); + Path unused = + tempFile( + dir.resolve("_repo_mapping").toString(), + ImmutableList.of( + ",config.json,config.json+1.2.3", + ",my_module,_main", + ",my_protobuf,protobuf+3.19.2", + ",my_workspace,_main", + "protobuf+3.19.2,config.json,config.json+1.2.3", + "protobuf+3.19.2,protobuf,protobuf+3.19.2")); + Runfiles r = Runfiles.createDirectoryBasedForTesting(dir.toString()).withSourceRepository(""); + + assertThat(r.rlocation("my_module/bar/runfile")).isEqualTo(dir + "/_main/bar/runfile"); + assertThat(r.rlocation("my_workspace/bar/runfile")).isEqualTo(dir + "/_main/bar/runfile"); + assertThat(r.rlocation("my_protobuf/foo/runfile")) + .isEqualTo(dir + "/protobuf+3.19.2/foo/runfile"); + assertThat(r.rlocation("my_protobuf/bar/dir")).isEqualTo(dir + "/protobuf+3.19.2/bar/dir"); + assertThat(r.rlocation("my_protobuf/bar/dir/file")) + .isEqualTo(dir + "/protobuf+3.19.2/bar/dir/file"); + assertThat(r.rlocation("my_protobuf/bar/dir/de eply/nes ted/fi+le")) + .isEqualTo(dir + "/protobuf+3.19.2/bar/dir/de eply/nes ted/fi+le"); + + assertThat(r.rlocation("protobuf/foo/runfile")).isEqualTo(dir + "/protobuf/foo/runfile"); + assertThat(r.rlocation("protobuf/bar/dir/dir/de eply/nes ted/fi+le")) + .isEqualTo(dir + "/protobuf/bar/dir/dir/de eply/nes ted/fi+le"); + + assertThat(r.rlocation("_main/bar/runfile")).isEqualTo(dir + "/_main/bar/runfile"); + assertThat(r.rlocation("protobuf+3.19.2/foo/runfile")) + .isEqualTo(dir + "/protobuf+3.19.2/foo/runfile"); + assertThat(r.rlocation("protobuf+3.19.2/bar/dir")).isEqualTo(dir + "/protobuf+3.19.2/bar/dir"); + assertThat(r.rlocation("protobuf+3.19.2/bar/dir/file")) + .isEqualTo(dir + "/protobuf+3.19.2/bar/dir/file"); + assertThat(r.rlocation("protobuf+3.19.2/bar/dir/de eply/nes ted/fi+le")) + .isEqualTo(dir + "/protobuf+3.19.2/bar/dir/de eply/nes ted/fi+le"); + + assertThat(r.rlocation("config.json")).isEqualTo(dir + "/config.json"); + } + + @Test + public void testDirectoryBasedRlocationUnmapped() throws Exception { + Path dir = tempDir.newFolder("foo.runfiles").toPath(); + Path unused = + tempFile( + dir.resolve("_repo_mapping").toString(), + ImmutableList.of( + ",config.json,config.json+1.2.3", + ",my_module,_main", + ",my_protobuf,protobuf+3.19.2", + ",my_workspace,_main", + "protobuf+3.19.2,config.json,config.json+1.2.3", + "protobuf+3.19.2,protobuf,protobuf+3.19.2")); + Runfiles r = Runfiles.createDirectoryBasedForTesting(dir.toString()).unmapped(); + + assertThat(r.rlocation("my_module/bar/runfile")).isEqualTo(dir + "/my_module/bar/runfile"); + assertThat(r.rlocation("my_workspace/bar/runfile")) + .isEqualTo(dir + "/my_workspace/bar/runfile"); + assertThat(r.rlocation("my_protobuf/foo/runfile")).isEqualTo(dir + "/my_protobuf/foo/runfile"); + assertThat(r.rlocation("my_protobuf/bar/dir")).isEqualTo(dir + "/my_protobuf/bar/dir"); + assertThat(r.rlocation("my_protobuf/bar/dir/file")) + .isEqualTo(dir + "/my_protobuf/bar/dir/file"); + assertThat(r.rlocation("my_protobuf/bar/dir/de eply/nes ted/fi+le")) + .isEqualTo(dir + "/my_protobuf/bar/dir/de eply/nes ted/fi+le"); + + assertThat(r.rlocation("protobuf/foo/runfile")).isEqualTo(dir + "/protobuf/foo/runfile"); + assertThat(r.rlocation("protobuf/bar/dir/dir/de eply/nes ted/fi+le")) + .isEqualTo(dir + "/protobuf/bar/dir/dir/de eply/nes ted/fi+le"); + + assertThat(r.rlocation("_main/bar/runfile")).isEqualTo(dir + "/_main/bar/runfile"); + assertThat(r.rlocation("protobuf+3.19.2/foo/runfile")) + .isEqualTo(dir + "/protobuf+3.19.2/foo/runfile"); + assertThat(r.rlocation("protobuf+3.19.2/bar/dir")).isEqualTo(dir + "/protobuf+3.19.2/bar/dir"); + assertThat(r.rlocation("protobuf+3.19.2/bar/dir/file")) + .isEqualTo(dir + "/protobuf+3.19.2/bar/dir/file"); + assertThat(r.rlocation("protobuf+3.19.2/bar/dir/de eply/nes ted/fi+le")) + .isEqualTo(dir + "/protobuf+3.19.2/bar/dir/de eply/nes ted/fi+le"); + + assertThat(r.rlocation("config.json")).isEqualTo(dir + "/config.json"); + } + + @Test + public void testDirectoryBasedRlocationWithRepoMapping_fromOtherRepo() throws Exception { + Path dir = tempDir.newFolder("foo.runfiles").toPath(); + Path unused = + tempFile( + dir.resolve("_repo_mapping").toString(), + ImmutableList.of( + ",config.json,config.json+1.2.3", + ",my_module,_main", + ",my_protobuf,protobuf+3.19.2", + ",my_workspace,_main", + "protobuf+3.19.2,config.json,config.json+1.2.3", + "protobuf+3.19.2,protobuf,protobuf+3.19.2")); + Runfiles r = + Runfiles.createDirectoryBasedForTesting(dir.toString()) + .withSourceRepository("protobuf+3.19.2"); + + assertThat(r.rlocation("protobuf/foo/runfile")).isEqualTo(dir + "/protobuf+3.19.2/foo/runfile"); + assertThat(r.rlocation("protobuf/bar/dir")).isEqualTo(dir + "/protobuf+3.19.2/bar/dir"); + assertThat(r.rlocation("protobuf/bar/dir/file")) + .isEqualTo(dir + "/protobuf+3.19.2/bar/dir/file"); + assertThat(r.rlocation("protobuf/bar/dir/de eply/nes ted/fi+le")) + .isEqualTo(dir + "/protobuf+3.19.2/bar/dir/de eply/nes ted/fi+le"); + + assertThat(r.rlocation("my_module/bar/runfile")).isEqualTo(dir + "/my_module/bar/runfile"); + assertThat(r.rlocation("my_protobuf/bar/dir/de eply/nes ted/fi+le")) + .isEqualTo(dir + "/my_protobuf/bar/dir/de eply/nes ted/fi+le"); + + assertThat(r.rlocation("_main/bar/runfile")).isEqualTo(dir + "/_main/bar/runfile"); + assertThat(r.rlocation("protobuf+3.19.2/foo/runfile")) + .isEqualTo(dir + "/protobuf+3.19.2/foo/runfile"); + assertThat(r.rlocation("protobuf+3.19.2/bar/dir")).isEqualTo(dir + "/protobuf+3.19.2/bar/dir"); + assertThat(r.rlocation("protobuf+3.19.2/bar/dir/file")) + .isEqualTo(dir + "/protobuf+3.19.2/bar/dir/file"); + assertThat(r.rlocation("protobuf+3.19.2/bar/dir/de eply/nes ted/fi+le")) + .isEqualTo(dir + "/protobuf+3.19.2/bar/dir/de eply/nes ted/fi+le"); + + assertThat(r.rlocation("config.json")).isEqualTo(dir + "/config.json"); + } + + @Test + public void testDirectoryBasedCtorArgumentValidation() throws IOException { + assertThrows( + IllegalArgumentException.class, + () -> Runfiles.createDirectoryBasedForTesting(null).withSourceRepository("")); + + assertThrows( + IllegalArgumentException.class, + () -> Runfiles.createDirectoryBasedForTesting("").withSourceRepository("")); + + assertThrows( + IllegalArgumentException.class, + () -> + Runfiles.createDirectoryBasedForTesting("non-existent directory is bad") + .withSourceRepository("")); + + Runfiles unused = + Runfiles.createDirectoryBasedForTesting(System.getenv("TEST_TMPDIR")) + .withSourceRepository(""); + } + + @Test + public void testManifestBasedCtorArgumentValidation() throws Exception { + assertThrows( + IllegalArgumentException.class, + () -> Runfiles.createManifestBasedForTesting(null).withSourceRepository("")); + + assertThrows( + IllegalArgumentException.class, + () -> Runfiles.createManifestBasedForTesting("").withSourceRepository("")); + + Path mf = tempFile("foobar", ImmutableList.of("a b")); + Runfiles unused = Runfiles.createManifestBasedForTesting(mf.toString()).withSourceRepository(""); + } + + @Test + public void testInvalidRepoMapping() throws Exception { + Path rm = tempFile("foo.repo_mapping", ImmutableList.of("a,b,c,d")); + Path mf = tempFile("foo.runfiles/MANIFEST", ImmutableList.of("_repo_mapping " + rm)); + assertThrows( + IllegalArgumentException.class, + () -> Runfiles.createManifestBasedForTesting(mf.toString()).withSourceRepository("")); + } + + private Path tempFile(String path, ImmutableList lines) throws IOException { + Path file = tempDir.getRoot().toPath().resolve(path.replace('/', File.separatorChar)); + Files.createDirectories(file.getParent()); + return Files.write(file, lines, StandardCharsets.UTF_8); + } +} diff --git a/test/runfiles/src/test/java/com/google/devtools/build/runfiles/UtilTest.java b/test/runfiles/src/test/java/com/google/devtools/build/runfiles/UtilTest.java new file mode 100644 index 00000000..38273261 --- /dev/null +++ b/test/runfiles/src/test/java/com/google/devtools/build/runfiles/UtilTest.java @@ -0,0 +1,47 @@ +// 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.assertThrows; + +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() { + assertThat(Util.isNullOrEmpty(null)).isTrue(); + assertThat(Util.isNullOrEmpty("")).isTrue(); + assertThat(Util.isNullOrEmpty("\0")).isFalse(); + assertThat(Util.isNullOrEmpty("some text")).isFalse(); + } + + @Test + public void testCheckArgument() { + Util.checkArgument(true, null, null); + + IllegalArgumentException e = + assertThrows(IllegalArgumentException.class, () -> Util.checkArgument(false, null, null)); + assertThat(e).hasMessageThat().isEqualTo("argument validation failed"); + + e = assertThrows(IllegalArgumentException.class, () -> Util.checkArgument(false, "foo-%s", 42)); + assertThat(e).hasMessageThat().isEqualTo("foo-42"); + } +}