blob: 308c6374d23cb6b9b88ca23357d320c0d20d91fc [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.collect.ImmutableSet.toImmutableSet;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.actions.ArtifactRoot;
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 com.google.devtools.build.lib.vfs.Root;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
/**
* Static utilities for managing output directory symlinks.
*/
public class OutputDirectoryLinksUtils {
static interface SymlinkDefinition {
String getLinkName(String symlinkPrefix, String productName, String workspaceBaseName);
Optional<Path> getLinkPath(
Set<BuildConfiguration> targetConfigs,
RepositoryName repositoryName,
Path outputPath,
Path execRoot);
}
private static final class ConfigSymlink implements SymlinkDefinition {
@FunctionalInterface
private static interface ConfigPathGetter {
ArtifactRoot apply(BuildConfiguration configuration, RepositoryName repositoryName);
}
private final String suffix;
private final ConfigPathGetter configToRoot;
public ConfigSymlink(String suffix, ConfigPathGetter configToRoot) {
this.suffix = suffix;
this.configToRoot = configToRoot;
}
@Override
public String getLinkName(String symlinkPrefix, String productName, String workspaceBaseName) {
return symlinkPrefix + suffix;
}
@Override
public Optional<Path> getLinkPath(
Set<BuildConfiguration> targetConfigs,
RepositoryName repositoryName,
Path outputPath,
Path execRoot) {
Set<Path> paths =
targetConfigs
.stream()
.map(config -> configToRoot.apply(config, repositoryName))
.map(ArtifactRoot::getRoot)
.map(Root::asPath)
.distinct()
.collect(toImmutableSet());
if (paths.size() == 1) {
return Optional.of(Iterables.getOnlyElement(paths));
} else {
return Optional.empty();
}
}
}
private static enum ExecRootSymlink implements SymlinkDefinition {
INSTANCE;
@Override
public String getLinkName(String symlinkPrefix, String productName, String workspaceBaseName) {
return symlinkPrefix + workspaceBaseName;
}
@Override
public Optional<Path> getLinkPath(
Set<BuildConfiguration> targetConfigs,
RepositoryName repositoryName,
Path outputPath,
Path execRoot) {
return Optional.of(execRoot);
}
}
private static enum OutputSymlink implements SymlinkDefinition {
PRODUCT_NAME {
@Override
public String getLinkName(
String symlinkPrefix, String productName, String workspaceBaseName) {
// TODO(b/35234395): This symlink is created for backwards compatibility, remove it once
// we're sure it won't cause any other issues.
return productName + "-out";
}
},
SYMLINK_PREFIX {
@Override
public String getLinkName(
String symlinkPrefix, String productName, String workspaceBaseName) {
return symlinkPrefix + "out";
}
};
@Override
public Optional<Path> getLinkPath(
Set<BuildConfiguration> targetConfigs,
RepositoryName repositoryName,
Path outputPath,
Path execRoot) {
return Optional.of(outputPath);
}
}
// Links to create, delete, and use for pretty-printing.
// Note that the order in which items appear in this list controls priority for getPrettyPath.
// It will try each link as a prefix from first to last.
private static final ImmutableList<SymlinkDefinition> LINK_DEFINITIONS =
ImmutableList.of(
new ConfigSymlink("bin", BuildConfiguration::getBinDirectory),
new ConfigSymlink("testlogs", BuildConfiguration::getTestLogsDirectory),
new ConfigSymlink("genfiles", BuildConfiguration::getGenfilesDirectory),
OutputSymlink.PRODUCT_NAME,
OutputSymlink.SYMLINK_PREFIX,
ExecRootSymlink.INSTANCE);
private static final String NO_CREATE_SYMLINKS_PREFIX = "/";
public static Iterable<String> getOutputSymlinkNames(String productName, String symlinkPrefix) {
ImmutableSet.Builder<String> builder = ImmutableSet.<String>builder();
for (OutputSymlink definition : OutputSymlink.values()) {
builder.add(definition.getLinkName(symlinkPrefix, productName, null));
}
return builder.build();
}
/**
* 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<>();
List<String> missingLinks = new ArrayList<>();
Set<String> createdLinks = new LinkedHashSet<>();
String workspaceBaseName = workspace.getBaseName();
RepositoryName repositoryName = RepositoryName.createFromValidStrippedName(workspaceName);
for (SymlinkDefinition definition : LINK_DEFINITIONS) {
String symlinkName = definition.getLinkName(symlinkPrefix, productName, workspaceBaseName);
if (!createdLinks.add(symlinkName)) {
// already created a link by this name
continue;
}
Optional<Path> result =
definition.getLinkPath(targetConfigs, repositoryName, outputPath, execRoot);
if (result.isPresent()) {
createLink(workspace, symlinkName, result.get(), failures);
} else {
removeLink(workspace, symlinkName, failures);
missingLinks.add(symlinkName);
}
}
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))));
}
if (!missingLinks.isEmpty()) {
eventHandler.handle(
Event.warn(
String.format(
"cleared convenience symlink(s) %s because their destinations would be ambiguous",
Joiner.on(", ").join(missingLinks))));
}
}
public static PathPrettyPrinter getPathPrettyPrinter(
String symlinkPrefix, String productName, Path workspaceDirectory, Path workingDirectory) {
return new PathPrettyPrinter(
LINK_DEFINITIONS, symlinkPrefix, productName, workspaceDirectory, workingDirectory);
}
/**
* 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<>();
String workspaceBaseName = workspace.getBaseName();
for (SymlinkDefinition link : LINK_DEFINITIONS) {
removeLink(
workspace, link.getLinkName(symlinkPrefix, productName, workspaceBaseName), 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;
}
}
}