| // 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 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.common.flogger.GoogleLogger; |
| import com.google.devtools.build.lib.analysis.BlazeDirectories; |
| import com.google.devtools.build.lib.analysis.config.BuildConfiguration; |
| import com.google.devtools.build.lib.analysis.config.BuildOptions; |
| import com.google.devtools.build.lib.analysis.config.ConvenienceSymlinks; |
| import com.google.devtools.build.lib.analysis.config.ConvenienceSymlinks.OutputSymlink; |
| import com.google.devtools.build.lib.analysis.config.ConvenienceSymlinks.SymlinkDefinition; |
| import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.ConvenienceSymlink; |
| import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.ConvenienceSymlink.Action; |
| import com.google.devtools.build.lib.buildtool.BuildRequestOptions.ConvenienceSymlinksMode; |
| 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.LinkedHashSet; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.function.Function; |
| |
| /** Static utilities for managing output directory symlinks. */ |
| public final class OutputDirectoryLinksUtils { |
| private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); |
| |
| // Static utilities class. |
| private OutputDirectoryLinksUtils() {} |
| |
| /** |
| * Returns all (types of) convenience symlinks that may be created. |
| * |
| * <p>Note that this is independent of which symlinks are actually requested by the build options; |
| * that's controlled by returning no candidates in {@link SymlinkDefinition#getLinkPaths}. |
| * |
| * <p>The order of the result indicates precedence for {@link PathPrettyPrinter}. |
| */ |
| private static final ImmutableList<SymlinkDefinition> getAllLinkDefinitions( |
| Iterable<SymlinkDefinition> symlinkDefinitions, boolean includeProductOut) { |
| // TODO(mitchellhyang): Remove the boolean parameter "includeProductOut" after the flag |
| // "--experimental_no_product_name_out_symlink" has been removed. We should avoid adding new |
| // parameters to this method. However, in this case, without the "includeProductOut", callers |
| // such as the pretty printer may print out false information. |
| ImmutableList.Builder<SymlinkDefinition> builder = ImmutableList.builder(); |
| builder.addAll(ConvenienceSymlinks.getStandardLinkDefinitions(includeProductOut)); |
| builder.addAll(symlinkDefinitions); |
| return builder.build(); |
| } |
| |
| 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 or delete convenience symlinks in the workspace to the various output |
| * directories, and generates associated log events. |
| * |
| * <p>If {@code --symlink_prefix} is {@link NO_CREATE_SYMLINKS_PREFIX}, or {@code |
| * --experimental_convenience_symlinks} is {@link ConvenienceSymlinksMode.IGNORE}, this method is |
| * a no-op. |
| * |
| * <p>Otherwise, for each symlink type, we decide whether the symlink should exist or not. If it |
| * should exist, it is created with the appropriate destination path; if not, it is deleted if |
| * already present on the file system. In either case, the decision of whether to create or delete |
| * the symlink is logged. (Note that deleting pre-existing symlinks helps ensure the user's |
| * workspace is in a consistent state after the build. However, if the {@code --symlink_prefix} |
| * has changed, we have no way to cleanup old symlink names leftover from a previous invocation.) |
| * |
| * <p>If {@code --experimental_convenience_symlinks} is set to {@link |
| * ConvenienceSymlinksMode.CLEAN}, all symlinks are set to be deleted. If it's set to {@link |
| * ConvenienceSymlinksMode.NORMAL}, each symlink type decides whether it should be created or |
| * deleted. (A symlink may decide to be deleted if e.g. it is disabled by a flag, or would want to |
| * point to more than one destination.) If it's set to {@link ConvenienceSymlinksMode.LOG_ONLY}, |
| * the same logic is run as in the {@code NORMAL} case, but the result is only emitting log |
| * messages, with no actual filesystem mutations. |
| * |
| * <p>A warning is emitted if a symlink would resolve to multiple destinations, or if a filesystem |
| * mutation operation fails. |
| * |
| * @return a list of {@link ConvenienceSymlink} messages describing what was created and |
| * destroyed. |
| */ |
| static ImmutableList<ConvenienceSymlink> createOutputDirectoryLinks( |
| Iterable<SymlinkDefinition> symlinkDefinitions, |
| BuildRequestOptions buildRequestOptions, |
| String workspaceName, |
| Path workspace, |
| BlazeDirectories directories, |
| EventHandler eventHandler, |
| Set<BuildConfiguration> targetConfigs, |
| Function<BuildOptions, BuildConfiguration> configGetter, |
| String productName) { |
| Path execRoot = directories.getExecRoot(workspaceName); |
| Path outputPath = directories.getOutputPath(workspaceName); |
| Path outputBase = directories.getOutputBase(); |
| String symlinkPrefix = buildRequestOptions.getSymlinkPrefix(productName); |
| ConvenienceSymlinksMode mode = buildRequestOptions.experimentalConvenienceSymlinks; |
| if (NO_CREATE_SYMLINKS_PREFIX.equals(symlinkPrefix)) { |
| return ImmutableList.of(); |
| } |
| |
| ImmutableList.Builder<ConvenienceSymlink> convenienceSymlinksBuilder = ImmutableList.builder(); |
| List<String> failures = new ArrayList<>(); |
| List<String> ambiguousLinks = new ArrayList<>(); |
| Set<String> createdLinks = new LinkedHashSet<>(); |
| String workspaceBaseName = workspace.getBaseName(); |
| RepositoryName repositoryName = RepositoryName.createFromValidStrippedName(workspaceName); |
| boolean logOnly = mode == ConvenienceSymlinksMode.LOG_ONLY; |
| |
| for (SymlinkDefinition symlink : |
| getAllLinkDefinitions( |
| symlinkDefinitions, !buildRequestOptions.experimentalNoProductNameOutSymlink)) { |
| String linkName = symlink.getLinkName(symlinkPrefix, productName, workspaceBaseName); |
| if (!createdLinks.add(linkName)) { |
| // already created a link by this name |
| continue; |
| } |
| if (mode == ConvenienceSymlinksMode.CLEAN) { |
| removeLink(workspace, linkName, failures, convenienceSymlinksBuilder, logOnly); |
| } else { |
| Set<Path> candidatePaths = |
| symlink.getLinkPaths( |
| buildRequestOptions, |
| targetConfigs, |
| configGetter, |
| repositoryName, |
| outputPath, |
| execRoot); |
| if (candidatePaths.size() == 1) { |
| createLink( |
| workspace, |
| linkName, |
| outputBase, |
| Iterables.getOnlyElement(candidatePaths), |
| failures, |
| convenienceSymlinksBuilder, |
| logOnly); |
| } else { |
| removeLink(workspace, linkName, failures, convenienceSymlinksBuilder, logOnly); |
| // candidatePaths can be empty if the symlink decided not to be created. This can happen |
| // if the symlink is disabled by a flag, or it intercepts an error while computing its |
| // target path. In that case, don't trigger a warning about an ambiguous link. |
| if (candidatePaths.size() > 1) { |
| ambiguousLinks.add(linkName); |
| } |
| } |
| } |
| } |
| |
| 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 (!ambiguousLinks.isEmpty()) { |
| eventHandler.handle( |
| Event.warn( |
| String.format( |
| "cleared convenience symlink(s) %s because their destinations would be ambiguous", |
| Joiner.on(", ").join(ambiguousLinks)))); |
| } |
| return convenienceSymlinksBuilder.build(); |
| } |
| |
| public static PathPrettyPrinter getPathPrettyPrinter( |
| Iterable<SymlinkDefinition> symlinkDefinitions, |
| String symlinkPrefix, |
| String productName, |
| Path workspaceDirectory, |
| Path workingDirectory, |
| boolean omitProductOut) { |
| return new PathPrettyPrinter( |
| getAllLinkDefinitions(symlinkDefinitions, !omitProductOut), |
| 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 symlinkDefinitions extra symlink types added by the {@link ConfiguredRuleClassProvider} |
| * @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( |
| Iterable<SymlinkDefinition> symlinkDefinitions, |
| 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(); |
| // Defaulting includeProductOut here to true since we want to remove all possible symlinks. |
| for (SymlinkDefinition link : |
| getAllLinkDefinitions(symlinkDefinitions, /*includeProductOut=*/ true)) { |
| removeLink( |
| workspace, |
| link.getLinkName(symlinkPrefix, productName, workspaceBaseName), |
| failures, |
| ImmutableList.builder(), |
| false); |
| } |
| |
| 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)))); |
| } |
| } |
| |
| /** |
| * Creates a symlink and outputs a {@link ConvenienceSymlink} entry. |
| * |
| * <p>The symlink is created at path {@code name}, relative to {@code base}, creating directories |
| * as needed; it points to {@code target}. Any filesystem errors are appended to {@code failures}. |
| * |
| * <p>A {@code ConvenienceSymlink} entry is added to {@code symlinksBuilder} describing the |
| * symlink. {@code outputBase} is used to determine the relative target path for this entry. |
| * |
| * <p>If {@code logOnly} is true, the {@code ConvenienceSymlink} entry is added but no actual |
| * filesystem operations are performed. |
| * |
| * @return true iff there were no filesystem errors. |
| */ |
| private static boolean createLink( |
| Path base, |
| String name, |
| Path outputBase, |
| Path target, |
| List<String> failures, |
| ImmutableList.Builder<ConvenienceSymlink> symlinksBuilder, |
| boolean logOnly) { |
| // Usually the symlink target falls under the output base, and the path in the BEP event should |
| // be relative to that output base. In rare cases where the symlink points elsewhere, use the |
| // absolute path as a fallback. |
| String targetForEvent = |
| target.startsWith(outputBase) |
| ? target.relativeTo(outputBase).getPathString() |
| : target.getPathString(); |
| symlinksBuilder.add( |
| ConvenienceSymlink.newBuilder() |
| .setPath(name) |
| .setTarget(targetForEvent) |
| .setAction(Action.CREATE) |
| .build()); |
| if (logOnly) { |
| return true; |
| } |
| Path link = base.getRelative(name); |
| 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(link, target); |
| } catch (IOException e) { |
| failures.add(String.format("cannot create symbolic link %s -> %s: %s", |
| name, target.getPathString(), e.getMessage())); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| /** |
| * Deletes a symlink and outputs a {@link ConvenienceSymlink} entry. |
| * |
| * <p>The symlink to be deleted is at path {@code name}, relative to {@code base}. Any filesystem |
| * errors are appended to {@code failures}. |
| * |
| * <p>A {@code ConvenienceSymlink} entry is added to {@code symlinksBuilder} describing the |
| * symlink to be deleted. |
| * |
| * <p>If {@code logOnly} is true, the {@code ConvenienceSymlink} entry is added but no actual |
| * filesystem operations are performed. |
| * |
| * @return true iff there were no filesystem errors. |
| */ |
| private static boolean removeLink( |
| Path base, |
| String name, |
| List<String> failures, |
| ImmutableList.Builder<ConvenienceSymlink> symlinksBuilder, |
| boolean logOnly) { |
| symlinksBuilder.add( |
| ConvenienceSymlink.newBuilder().setPath(name).setAction(Action.DELETE).build()); |
| if (logOnly) { |
| return true; |
| } |
| Path link = base.getRelative(name); |
| try { |
| if (link.isSymbolicLink()) { |
| // TODO(b/146885821): Consider also removing empty ancestor directories, to allow for |
| // cleaning up directories generated by --symlink_prefix=dir1/dir2/... |
| // Might be undesireable since it could also remove manually-created directories. |
| logger.atFinest().log("Removing %s", link); |
| link.delete(); |
| } |
| return true; |
| } catch (IOException e) { |
| failures.add(String.format("%s: %s", name, e.getMessage())); |
| return false; |
| } |
| } |
| } |