blob: 7211b916db395c888d6acf83c697e9df6575210e [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.buildtool;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
import com.google.devtools.build.lib.cmdline.RepositoryName;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.EventHandler;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* Static utilities for managing output directory symlinks.
*/
public class OutputDirectoryLinksUtils {
// Used in getPrettyPath() method below.
private static final String[] LINKS = { "bin", "genfiles", "includes" };
private static final String NO_CREATE_SYMLINKS_PREFIX = "/";
public static ImmutableList<String> getOutputSymlinkNames(String productName,
String symlinkPrefix) {
ImmutableList.Builder<String> builder = ImmutableList.<String>builder();
// TODO(b/35234395): This symlink is created for backwards compatiblity, remove it once
// we're sure it won't cause any other issues.
builder.add(productName + "-out");
if (!productName.equals(symlinkPrefix)) {
builder.add(symlinkPrefix + "out");
}
return builder.build();
}
private static String execRootSymlink(String symlinkPrefix, String workspaceName) {
return symlinkPrefix + workspaceName;
}
/**
* Attempts to create convenience symlinks in the workspaceDirectory and in execRoot to the output
* area and to the configuration-specific output directories. Issues a warning if it fails, e.g.
* because workspaceDirectory is readonly.
*
* <p>Configuration-specific output symlinks will be created or updated if and only if the set of
* {@code targetConfigs} contains only configurations whose output directories match. Otherwise -
* i.e., if there are multiple configurations with distinct output directories or there were no
* targets with non-null configurations in the build - any stale symlinks left over from previous
* invocations will be removed.
*/
static void createOutputDirectoryLinks(
String workspaceName,
Path workspace,
Path execRoot,
Path outputPath,
EventHandler eventHandler,
Set<BuildConfiguration> targetConfigs,
String symlinkPrefix,
String productName) {
if (NO_CREATE_SYMLINKS_PREFIX.equals(symlinkPrefix)) {
return;
}
List<String> failures = new ArrayList<>();
// Make the two non-specific links from the workspace to the output area,
// and the configuration-specific links in both the workspace and the execution root dirs.
// IMPORTANT: Keep in sync with removeOutputDirectoryLinks below.
for (String outputSymlinkName : getOutputSymlinkNames(productName, symlinkPrefix)) {
createLink(workspace, outputSymlinkName, outputPath, failures);
}
// Points to execroot
createLink(workspace, execRootSymlink(
symlinkPrefix, workspace.getBaseName()), execRoot, failures);
RepositoryName repositoryName = RepositoryName.createFromValidStrippedName(workspaceName);
// Set up convenience symlinks iff there's only one useful target for them to point to;
// otherwise, remove stale symlinks from previous invocations at that path to avoid confusion
Set<Path> binPaths =
targetConfigs
.stream()
.map(targetConfig -> targetConfig.getBinDirectory(repositoryName).getPath())
.distinct()
.collect(toImmutableSet());
if (binPaths.size() == 1) {
createLink(workspace, symlinkPrefix + "bin", Iterables.getOnlyElement(binPaths), failures);
} else {
removeLink(workspace, symlinkPrefix + "bin", failures);
}
Set<Path> testLogsPaths =
targetConfigs
.stream()
.map(targetConfig -> targetConfig.getTestLogsDirectory(repositoryName).getPath())
.distinct()
.collect(toImmutableSet());
if (testLogsPaths.size() == 1) {
createLink(
workspace,
symlinkPrefix + "testlogs",
Iterables.getOnlyElement(testLogsPaths),
failures);
} else {
removeLink(workspace, symlinkPrefix + "testlogs", failures);
}
Set<Path> genfilesPaths =
targetConfigs
.stream()
.map(targetConfig -> targetConfig.getGenfilesDirectory(repositoryName).getPath())
.distinct()
.collect(toImmutableSet());
if (genfilesPaths.size() == 1) {
createLink(
workspace,
symlinkPrefix + "genfiles",
Iterables.getOnlyElement(genfilesPaths),
failures);
} else {
removeLink(workspace, symlinkPrefix + "genfiles", failures);
}
if (!failures.isEmpty()) {
eventHandler.handle(Event.warn(String.format(
"failed to create one or more convenience symlinks for prefix '%s':\n %s",
symlinkPrefix, Joiner.on("\n ").join(failures))));
}
}
/**
* Returns a convenient path to the specified file, relativizing it and using output-dir symlinks
* if possible. Otherwise, return a path relative to the workspace directory if possible.
* Otherwise, return the absolute path.
*
* <p>This method must be called after the symlinks are created at the end of a build. If called
* before, the pretty path may be incorrect if the symlinks end up pointing somewhere new.
*/
public static PathFragment getPrettyPath(Path file, String workspaceName,
Path workspaceDirectory, String symlinkPrefix, String productName) {
for (String link : LINKS) {
PathFragment result = relativize(file, workspaceDirectory, symlinkPrefix + link);
if (result != null) {
return result;
}
}
PathFragment result = relativize(file, workspaceDirectory,
execRootSymlink(symlinkPrefix, workspaceName));
if (result != null) {
return result;
}
ImmutableList<String> outputSymlinkNames = getOutputSymlinkNames(productName, symlinkPrefix);
checkArgument(!outputSymlinkNames.isEmpty());
result = relativize(file, workspaceDirectory, outputSymlinkNames.get(0));
if (result != null) {
return result;
}
return file.asFragment();
}
// Helper to getPrettyPath. Returns file, relativized w.r.t. the referent of
// "linkname", or null if it was a not a child.
private static PathFragment relativize(Path file, Path workspaceDirectory, String linkname) {
PathFragment link = PathFragment.create(linkname);
try {
Path dir = workspaceDirectory.getRelative(link);
PathFragment levelOneLinkTarget = dir.readSymbolicLink();
if (levelOneLinkTarget.isAbsolute() &&
file.startsWith(dir = file.getRelative(levelOneLinkTarget))) {
return link.getRelative(file.relativeTo(dir));
}
} catch (IOException e) {
/* ignore */
}
return null;
}
/**
* Attempts to remove the convenience symlinks in the workspace directory.
*
* <p>Issues a warning if it fails, e.g. because workspaceDirectory is readonly.
* Also cleans up any child directories created by a custom prefix.
*
* @param workspace the runtime's workspace
* @param eventHandler the error eventHandler
* @param symlinkPrefix the symlink prefix which should be removed
* @param productName the product name
*/
public static void removeOutputDirectoryLinks(String workspaceName, Path workspace,
EventHandler eventHandler, String symlinkPrefix, String productName) {
if (NO_CREATE_SYMLINKS_PREFIX.equals(symlinkPrefix)) {
return;
}
List<String> failures = new ArrayList<>();
for (String outputSymlinkName : getOutputSymlinkNames(productName, symlinkPrefix)) {
removeLink(workspace, outputSymlinkName, failures);
}
removeLink(workspace, execRootSymlink(symlinkPrefix, workspaceName), failures);
removeLink(workspace, execRootSymlink(symlinkPrefix, workspace.getBaseName()), failures);
removeLink(workspace, symlinkPrefix + "bin", failures);
removeLink(workspace, symlinkPrefix + "testlogs", failures);
removeLink(workspace, symlinkPrefix + "genfiles", failures);
FileSystemUtils.removeDirectoryAndParents(workspace, PathFragment.create(symlinkPrefix));
if (!failures.isEmpty()) {
eventHandler.handle(Event.warn(String.format(
"failed to remove one or more convenience symlinks for prefix '%s':\n %s", symlinkPrefix,
Joiner.on("\n ").join(failures))));
}
}
/**
* Helper to createOutputDirectoryLinks that creates a symlink from base + name to target.
*/
private static boolean createLink(Path base, String name, Path target, List<String> failures) {
try {
FileSystemUtils.createDirectoryAndParents(target);
} catch (IOException e) {
failures.add(String.format("cannot create directory %s: %s",
target.getPathString(), e.getMessage()));
return false;
}
try {
FileSystemUtils.ensureSymbolicLink(base.getRelative(name), target);
} catch (IOException e) {
failures.add(String.format("cannot create symbolic link %s -> %s: %s",
name, target.getPathString(), e.getMessage()));
return false;
}
return true;
}
/**
* Helper to removeOutputDirectoryLinks that removes one of the Blaze convenience symbolic links.
*/
private static boolean removeLink(Path base, String name, List<String> failures) {
Path link = base.getRelative(name);
try {
if (link.isSymbolicLink()) {
ExecutionTool.logger.finest("Removing " + link);
link.delete();
}
return true;
} catch (IOException e) {
failures.add(String.format("%s: %s", name, e.getMessage()));
return false;
}
}
}