| // 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 com.google.devtools.build.lib.vfs.Root; |
| 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).getRoot()) |
| .map(Root::asPath) |
| .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).getRoot()) |
| .map(Root::asPath) |
| .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).getRoot()) |
| .map(Root::asPath) |
| .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) { |
| if (NO_CREATE_SYMLINKS_PREFIX.equals(symlinkPrefix)) { |
| return file.asFragment(); |
| } |
| |
| 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; |
| } |
| } |
| } |