// 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; } }