blob: 8940eea8fe13074502e181c7fd8c0120c9fa152b [file] [log] [blame]
// 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;
}
}