| // Copyright 2014 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.lib.pkgcache; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.Preconditions; |
| import com.google.common.base.Verify; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.devtools.build.lib.cmdline.PackageIdentifier; |
| import com.google.devtools.build.lib.events.Event; |
| import com.google.devtools.build.lib.events.EventHandler; |
| import com.google.devtools.build.lib.packages.BuildFileNotFoundException; |
| import com.google.devtools.build.lib.packages.NoSuchPackageException; |
| import com.google.devtools.build.lib.vfs.FileStatus; |
| import com.google.devtools.build.lib.vfs.Path; |
| import com.google.devtools.build.lib.vfs.PathFragment; |
| import com.google.devtools.build.lib.vfs.Symlinks; |
| import com.google.devtools.build.lib.vfs.UnixGlob; |
| import java.io.IOException; |
| import java.io.Serializable; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.Objects; |
| import java.util.concurrent.atomic.AtomicReference; |
| |
| /** |
| * A mapping from the name of a package to the location of its BUILD file. |
| * The implementation composes an ordered sequence of directories according to |
| * the package-path rules. |
| * |
| * <p>All methods are thread-safe, and (assuming no change to the underlying |
| * filesystem) idempotent. |
| */ |
| public class PathPackageLocator implements Serializable { |
| private static final PathFragment BUILD_PATH_FRAGMENT = PathFragment.create("BUILD"); |
| |
| public static final ImmutableSet<String> DEFAULT_TOP_LEVEL_EXCLUDES = |
| ImmutableSet.of("experimental"); |
| |
| private final ImmutableList<Path> pathEntries; |
| // Transient because this is an injected value in Skyframe, and as such, its serialized |
| // representation is used as a key. We want a change to output base not to invalidate things. |
| private final transient Path outputBase; |
| |
| public static final PathPackageLocator EMPTY = |
| new PathPackageLocator(null, ImmutableList.<Path>of()); |
| |
| @VisibleForTesting |
| public PathPackageLocator(Path outputBase, List<Path> pathEntries) { |
| this.outputBase = outputBase; |
| this.pathEntries = ImmutableList.copyOf(pathEntries); |
| } |
| |
| /** |
| * Constructs a PathPackageLocator based on the specified list of package root directories. |
| */ |
| @VisibleForTesting |
| public PathPackageLocator(Path... pathEntries) { |
| this(null, Arrays.asList(pathEntries)); |
| } |
| |
| /** |
| * Returns the path to the build file for this package. |
| * |
| * <p>The package's root directory may be computed by calling getParentFile() |
| * on the result of this function. |
| * |
| * <p>Instances of this interface do not attempt to do any caching, nor |
| * implement checks for package-boundary crossing logic; the PackageCache |
| * does that. |
| * |
| * <p>If the same package exists beneath multiple package path entries, the |
| * first path that matches always wins. |
| */ |
| public Path getPackageBuildFile(PackageIdentifier packageName) throws NoSuchPackageException { |
| Path buildFile = getPackageBuildFileNullable(packageName, UnixGlob.DEFAULT_SYSCALLS_REF); |
| if (buildFile == null) { |
| throw new BuildFileNotFoundException(packageName, "BUILD file not found on package path"); |
| } |
| return buildFile; |
| } |
| |
| /** |
| * Like #getPackageBuildFile(), but returns null instead of throwing. |
| * @param packageIdentifier the name of the package. |
| * @param cache a filesystem-level cache of stat() calls. |
| */ |
| public Path getPackageBuildFileNullable(PackageIdentifier packageIdentifier, |
| AtomicReference<? extends UnixGlob.FilesystemCalls> cache) { |
| Preconditions.checkArgument(!packageIdentifier.getRepository().isDefault()); |
| if (packageIdentifier.getRepository().isMain()) { |
| return getFilePath( |
| packageIdentifier.getPackageFragment().getRelative(BUILD_PATH_FRAGMENT), cache); |
| } else { |
| Verify.verify(outputBase != null, String.format( |
| "External package '%s' needs to be loaded but this PathPackageLocator instance does not " |
| + "support external packages", packageIdentifier)); |
| // This works only to some degree, because it relies on the presence of the repository under |
| // $OUTPUT_BASE/external, which is created by the appropriate RepositoryDirectoryValue. This |
| // is true for the invocation in GlobCache, but not for the locator.getBuildFileForPackage() |
| // invocation in Parser#include(). |
| Path buildFile = outputBase.getRelative( |
| packageIdentifier.getSourceRoot()).getRelative(BUILD_PATH_FRAGMENT); |
| try { |
| FileStatus stat = cache.get().statIfFound(buildFile, Symlinks.FOLLOW); |
| if (stat != null && stat.isFile()) { |
| return buildFile; |
| } else { |
| return null; |
| } |
| } catch (IOException e) { |
| return null; |
| } |
| } |
| } |
| |
| |
| /** |
| * Returns an immutable ordered list of the directories on the package path. |
| */ |
| public ImmutableList<Path> getPathEntries() { |
| return pathEntries; |
| } |
| |
| @Override |
| public String toString() { |
| return "PathPackageLocator" + pathEntries; |
| } |
| |
| /** |
| * A factory of PathPackageLocators from a list of path elements. Elements |
| * may contain "%workspace%", indicating the workspace. |
| * |
| * @param outputBase the output base. Can be null if remote repositories are not in use. |
| * @param pathElements Each element must be an absolute path, relative path, |
| * or some string "%workspace%" + relative, where relative is itself a |
| * relative path. The special symbol "%workspace%" means to interpret |
| * the path relative to the nearest enclosing workspace. Relative |
| * paths are interpreted relative to the client's working directory, |
| * which may be below the workspace. |
| * @param eventHandler The eventHandler. |
| * @param workspace The nearest enclosing package root directory. |
| * @param clientWorkingDirectory The client's working directory. |
| * @param checkExistence If true, verify that the element exists before adding it to the locator. |
| * @return a list of {@link Path}s. |
| */ |
| public static PathPackageLocator create(Path outputBase, |
| List<String> pathElements, |
| EventHandler eventHandler, |
| Path workspace, |
| Path clientWorkingDirectory, |
| boolean checkExistence) { |
| List<Path> resolvedPaths = new ArrayList<>(); |
| final String workspaceWildcard = "%workspace%"; |
| |
| for (String pathElement : pathElements) { |
| // Replace "%workspace%" with the path of the enclosing workspace directory. |
| pathElement = pathElement.replace(workspaceWildcard, workspace.getPathString()); |
| |
| PathFragment pathElementFragment = PathFragment.create(pathElement); |
| |
| // If the path string started with "%workspace%" or "/", it is already absolute, |
| // so the following line is a no-op. |
| Path rootPath = clientWorkingDirectory.getRelative(pathElementFragment); |
| |
| if (!pathElementFragment.isAbsolute() && !clientWorkingDirectory.equals(workspace)) { |
| eventHandler.handle( |
| Event.warn("The package path element '" + pathElementFragment + "' will be " |
| + "taken relative to your working directory. You may have intended " |
| + "to have the path taken relative to your workspace directory. " |
| + "If so, please use the '" + workspaceWildcard + "' wildcard.")); |
| } |
| |
| if (!checkExistence || rootPath.exists()) { |
| resolvedPaths.add(rootPath); |
| } |
| } |
| |
| return new PathPackageLocator(outputBase, resolvedPaths); |
| } |
| |
| /** |
| * A factory of PathPackageLocators from a list of path elements. Elements |
| * may contain "%workspace%", indicating the workspace. |
| * |
| * @param outputBase the output base. Can be null if remote repositories are not in use. |
| * @param pathElements Each element must be an absolute path, relative path, |
| * or some string "%workspace%" + relative, where relative is itself a |
| * relative path. The special symbol "%workspace%" means to interpret |
| * the path relative to the nearest enclosing workspace. Relative |
| * paths are interpreted relative to the client's working directory, |
| * which may be below the workspace. |
| * @param eventHandler The eventHandler. |
| * @param workspace The nearest enclosing package root directory. |
| * @param clientWorkingDirectory The client's working directory. |
| * @return a list of {@link Path}s. |
| */ |
| public static PathPackageLocator create(Path outputBase, |
| List<String> pathElements, EventHandler eventHandler, Path workspace, |
| Path clientWorkingDirectory) { |
| return create(outputBase, pathElements, eventHandler, workspace, clientWorkingDirectory, |
| /*checkExistence=*/true); |
| } |
| |
| /** |
| * Returns the path to the WORKSPACE file for this build. |
| * |
| * <p>If there are WORKSPACE files beneath multiple package path entries, the first one always |
| * wins. |
| */ |
| public Path getWorkspaceFile() { |
| AtomicReference<? extends UnixGlob.FilesystemCalls> cache = UnixGlob.DEFAULT_SYSCALLS_REF; |
| // TODO(bazel-team): correctness in the presence of changes to the location of the WORKSPACE |
| // file. |
| return getFilePath(PathFragment.create("WORKSPACE"), cache); |
| } |
| |
| private Path getFilePath(PathFragment suffix, |
| AtomicReference<? extends UnixGlob.FilesystemCalls> cache) { |
| for (Path pathEntry : pathEntries) { |
| Path buildFile = pathEntry.getRelative(suffix); |
| try { |
| FileStatus stat = cache.get().statIfFound(buildFile, Symlinks.FOLLOW); |
| if (stat != null && stat.isFile()) { |
| return buildFile; |
| } |
| } catch (IOException ignored) { |
| // Treat IOException as a missing file. |
| } |
| } |
| return null; |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hash(pathEntries, outputBase); |
| } |
| |
| @Override |
| public boolean equals(Object other) { |
| if (this == other) { |
| return true; |
| } |
| if (!(other instanceof PathPackageLocator)) { |
| return false; |
| } |
| PathPackageLocator pathPackageLocator = (PathPackageLocator) other; |
| return Objects.equals(getPathEntries(), pathPackageLocator.getPathEntries()) |
| && Objects.equals(outputBase, pathPackageLocator.outputBase); |
| } |
| |
| public Path getOutputBase() { |
| return outputBase; |
| } |
| } |