// 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.Label; 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.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. * *

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 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 buildFilesByPriority; @VisibleForTesting public PathPackageLocator( Path outputBase, List pathEntries, List buildFilesByPriority) { this.outputBase = outputBase; this.pathEntries = ImmutableList.copyOf(pathEntries); this.buildFilesByPriority = ImmutableList.copyOf(buildFilesByPriority); } /** * Returns the path to the build file for this package. * *

The package's root directory may be computed by calling getParentFile() * on the result of this function. * *

Instances of this interface do not attempt to do any caching, nor * implement checks for package-boundary crossing logic; the PackageCache * does that. * *

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 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 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. * *

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 pathElements, EventHandler eventHandler, Path workspace, Path clientWorkingDirectory, List 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 pathElements, List buildFilesByPriority) { return new PathPackageLocator(outputBase, pathElements, buildFilesByPriority); } private static PathPackageLocator createInternal( Path outputBase, List pathElements, EventHandler eventHandler, Path workspace, Path clientWorkingDirectory, List buildFilesByPriority, boolean checkExistence) { List 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. * *

If there are WORKSPACE files beneath multiple package path entries, the first one always * wins. */ public Path getWorkspaceFile() { AtomicReference cache = UnixGlob.DEFAULT_SYSCALLS_REF; // TODO(bazel-team): correctness in the presence of changes to the location of the WORKSPACE // file. return getFilePath(Label.WORKSPACE_FILE_NAME, cache); } private Path getFilePath(PathFragment suffix, AtomicReference cache) { for (Root 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; } }