| // 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.devtools.build.lib.cmdline.LabelConstants; |
| 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.BuildFileName; |
| import com.google.devtools.build.lib.packages.BuildFileNotFoundException; |
| import com.google.devtools.build.lib.packages.NoSuchPackageException; |
| import com.google.devtools.build.lib.vfs.Dirent; |
| 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.Root; |
| 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.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 String WORKSPACE_WILDCARD = "%workspace%"; |
| |
| private final ImmutableList<Root> 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; |
| |
| private final ImmutableList<BuildFileName> buildFilesByPriority; |
| |
| @VisibleForTesting |
| public PathPackageLocator( |
| Path outputBase, List<Root> pathEntries, List<BuildFileName> buildFilesByPriority) { |
| this.outputBase = outputBase; |
| this.pathEntries = ImmutableList.copyOf(pathEntries); |
| this.buildFilesByPriority = ImmutableList.copyOf(buildFilesByPriority); |
| } |
| |
| /** |
| * 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. |
| * @return the {@link Path} to the correct build file, or {@code null} if none was found |
| */ |
| public Path getPackageBuildFileNullable( |
| PackageIdentifier packageIdentifier, |
| AtomicReference<? extends UnixGlob.FilesystemCalls> cache) { |
| Preconditions.checkArgument(!packageIdentifier.getRepository().isDefault()); |
| if (packageIdentifier.getRepository().isMain()) { |
| for (BuildFileName buildFileName : buildFilesByPriority) { |
| Path buildFilePath = |
| getFilePath( |
| packageIdentifier |
| .getPackageFragment() |
| .getRelative(buildFileName.getFilenameFragment()), |
| cache); |
| if (buildFilePath != null) { |
| return buildFilePath; |
| } |
| } |
| } 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(). |
| for (BuildFileName buildFileName : buildFilesByPriority) { |
| Path buildFile = |
| outputBase |
| .getRelative(packageIdentifier.getSourceRoot()) |
| .getRelative(buildFileName.getFilenameFragment()); |
| try { |
| FileStatus stat = cache.get().statIfFound(buildFile, Symlinks.FOLLOW); |
| if (stat != null && stat.isFile()) { |
| return buildFile; |
| } |
| } catch (IOException e) { |
| return null; |
| } |
| } |
| } |
| |
| return null; |
| } |
| |
| /** Returns an immutable ordered list of the directories on the package path. */ |
| public ImmutableList<Root> getPathEntries() { |
| return pathEntries; |
| } |
| |
| @Override |
| public String toString() { |
| return "PathPackageLocator" + pathEntries; |
| } |
| |
| public static String maybeReplaceWorkspaceInString(String pathElement, Path workspace) { |
| return pathElement.replace(WORKSPACE_WILDCARD, workspace.getPathString()); |
| } |
| /** |
| * A factory of PathPackageLocators from a list of path elements. Elements may contain |
| * "%workspace%", indicating the workspace. |
| * |
| * <p>If any of the paths given do not exist, an exception will be thrown. |
| * |
| * @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 buildFilesByPriority The ordered collection of {@link BuildFileName}s to check in each |
| * potential package directory. |
| * @return a {@link PathPackageLocator} that uses the {@code outputBase} and {@code pathElements} |
| * provided. |
| */ |
| public static PathPackageLocator create( |
| Path outputBase, |
| List<String> pathElements, |
| EventHandler eventHandler, |
| Path workspace, |
| Path clientWorkingDirectory, |
| List<BuildFileName> buildFilesByPriority) { |
| return createInternal( |
| outputBase, |
| pathElements, |
| eventHandler, |
| workspace, |
| clientWorkingDirectory, |
| buildFilesByPriority, |
| true); |
| } |
| |
| /** |
| * A factory of PathPackageLocators from a list of path elements. |
| * |
| * @param outputBase the output base. Can be null if remote repositories are not in use. |
| * @param pathElements Each element must be a {@link Root} object. |
| * @param buildFilesByPriority The ordered collection of {@link BuildFileName}s to check in each |
| * potential package directory. |
| * @return a {@link PathPackageLocator} that uses the {@code outputBase} and {@code pathElements} |
| * provided. |
| */ |
| public static PathPackageLocator createWithoutExistenceCheck( |
| Path outputBase, List<Root> pathElements, List<BuildFileName> buildFilesByPriority) { |
| return new PathPackageLocator(outputBase, pathElements, buildFilesByPriority); |
| } |
| |
| private static PathPackageLocator createInternal( |
| Path outputBase, |
| List<String> pathElements, |
| EventHandler eventHandler, |
| Path workspace, |
| Path clientWorkingDirectory, |
| List<BuildFileName> buildFilesByPriority, |
| boolean checkExistence) { |
| List<Root> resolvedPaths = new ArrayList<>(); |
| |
| for (String pathElement : pathElements) { |
| // Replace "%workspace%" with the path of the enclosing workspace directory. |
| pathElement = maybeReplaceWorkspaceInString(pathElement, workspace); |
| |
| 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 '" |
| + WORKSPACE_WILDCARD |
| + "' wildcard.")); |
| } |
| |
| if (!checkExistence || rootPath.exists()) { |
| resolvedPaths.add(Root.fromPath(rootPath)); |
| } |
| } |
| |
| return new PathPackageLocator(outputBase, resolvedPaths, buildFilesByPriority); |
| } |
| |
| /** |
| * 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. |
| Path workspaceFile = getFilePath(LabelConstants.WORKSPACE_DOT_BAZEL_FILE_NAME, cache); |
| if (workspaceFile != null) { |
| return workspaceFile; |
| } |
| return getFilePath(LabelConstants.WORKSPACE_FILE_NAME, cache); |
| } |
| |
| private Path getFilePath(PathFragment suffix, |
| AtomicReference<? extends UnixGlob.FilesystemCalls> cache) { |
| for (Root pathEntry : pathEntries) { |
| Path buildFile = pathEntry.getRelative(suffix); |
| try { |
| Dirent.Type type = cache.get().getType(buildFile, Symlinks.FOLLOW); |
| if (type == Dirent.Type.FILE || type == Dirent.Type.UNKNOWN) { |
| 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; |
| } |
| } |