// 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 { private static interface SymlinkDefinition { String getLinkName(String symlinkPrefix, String productName, String workspaceBaseName); Optional getLinkPath( Set 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 getLinkPath( Set targetConfigs, RepositoryName repositoryName, Path outputPath, Path execRoot) { Set 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 getLinkPath( Set 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 compatiblity, 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 getLinkPath( Set 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 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 getOutputSymlinkNames(String productName, String symlinkPrefix) { ImmutableSet.Builder builder = ImmutableSet.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. * *

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 targetConfigs, String symlinkPrefix, String productName) { if (NO_CREATE_SYMLINKS_PREFIX.equals(symlinkPrefix)) { return; } List failures = new ArrayList<>(); List missingLinks = new ArrayList<>(); Set 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 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)))); } } /** * Returns a convenient path to the specified file, relativizing it and using output-dir symlinks * if possible. Otherwise, return the absolute path. * *

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, Path workingDirectory, String symlinkPrefix, String productName) { if (NO_CREATE_SYMLINKS_PREFIX.equals(symlinkPrefix)) { return file.asFragment(); } String workspaceBaseName = workspaceDirectory.getBaseName(); for (SymlinkDefinition link : LINK_DEFINITIONS) { PathFragment result = relativize( file, workspaceDirectory, workingDirectory, link.getLinkName(symlinkPrefix, productName, workspaceBaseName)); 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, Path workingDirectory, 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))) { PathFragment outputLink = workingDirectory.equals(workspaceDirectory) ? link : workspaceDirectory.getRelative(link).asFragment(); return outputLink.getRelative(file.relativeTo(dir)); } } catch (IOException e) { /* ignore */ } return null; } /** * Attempts to remove the convenience symlinks in the workspace directory. * *

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