| // Copyright 2016 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.annotations.VisibleForTesting; |
| import com.google.common.base.Preconditions; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.Maps; |
| import com.google.common.collect.Sets; |
| import com.google.devtools.build.lib.cmdline.Label; |
| import com.google.devtools.build.lib.cmdline.PackageIdentifier; |
| import com.google.devtools.build.lib.concurrent.ThreadSafety; |
| 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.Map; |
| import java.util.Set; |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| |
| /** |
| * Creates a symlink forest based on a package path map. |
| */ |
| class SymlinkForest { |
| |
| private static final Logger logger = Logger.getLogger(SymlinkForest.class.getName()); |
| private static final boolean LOG_FINER = logger.isLoggable(Level.FINER); |
| |
| private final ImmutableMap<PackageIdentifier, Root> packageRoots; |
| private final Path execroot; |
| private final String workspaceName; |
| private final String productName; |
| private final String[] prefixes; |
| |
| SymlinkForest( |
| ImmutableMap<PackageIdentifier, Root> packageRoots, |
| Path execroot, |
| String productName, |
| String workspaceName) { |
| this.packageRoots = packageRoots; |
| this.execroot = execroot; |
| this.workspaceName = workspaceName; |
| this.productName = productName; |
| this.prefixes = new String[] { ".", "_", productName + "-"}; |
| } |
| |
| /** |
| * Returns the longest prefix from a given set of 'prefixes' that are |
| * contained in 'path'. I.e the closest ancestor directory containing path. |
| * Returns null if none found. |
| * @param path |
| * @param prefixes |
| */ |
| @VisibleForTesting |
| static PackageIdentifier longestPathPrefix( |
| PackageIdentifier path, ImmutableSet<PackageIdentifier> prefixes) { |
| for (int i = path.getPackageFragment().segmentCount(); i >= 0; i--) { |
| PackageIdentifier prefix = createInRepo(path, path.getPackageFragment().subFragment(0, i)); |
| if (prefixes.contains(prefix)) { |
| return prefix; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Delete all dir trees under a given 'dir' that don't start with one of a set |
| * of given 'prefixes'. Does not follow any symbolic links. |
| */ |
| @VisibleForTesting |
| @ThreadSafety.ThreadSafe |
| static void deleteTreesBelowNotPrefixed(Path dir, String[] prefixes) throws IOException { |
| dirloop: |
| for (Path p : dir.getDirectoryEntries()) { |
| String name = p.getBaseName(); |
| for (String prefix : prefixes) { |
| if (name.startsWith(prefix)) { |
| continue dirloop; |
| } |
| } |
| FileSystemUtils.deleteTree(p); |
| } |
| } |
| |
| void plantSymlinkForest() throws IOException { |
| deleteTreesBelowNotPrefixed(execroot, prefixes); |
| // TODO(kchodorow): this can be removed once the execution root is rearranged. |
| // Current state: symlink tree was created under execroot/$(basename ws) and then |
| // execroot/wsname is symlinked to that. The execution root change creates (and cleans up) |
| // subtrees for each repository and has been rolled forward and back several times. Thus, if |
| // someone was using a with-execroot-change version of bazel and then switched to this one, |
| // their execution root would contain a subtree for execroot/wsname that would never be |
| // cleaned up by this version of Bazel. |
| Path realWorkspaceDir = execroot.getParentDirectory().getRelative(workspaceName); |
| if (!workspaceName.equals(execroot.getBaseName()) && realWorkspaceDir.exists() |
| && !realWorkspaceDir.isSymbolicLink()) { |
| FileSystemUtils.deleteTree(realWorkspaceDir); |
| } |
| |
| // Create a sorted map of all dirs (packages and their ancestors) to sets of their roots. |
| // Packages come from exactly one root, but their shared ancestors may come from more. |
| // The map is maintained sorted lexicographically, so parents are before their children. |
| Map<PackageIdentifier, Set<Root>> dirRootsMap = Maps.newTreeMap(); |
| for (Map.Entry<PackageIdentifier, Root> entry : packageRoots.entrySet()) { |
| PackageIdentifier pkgId = entry.getKey(); |
| if (pkgId.equals(Label.EXTERNAL_PACKAGE_IDENTIFIER)) { |
| // This isn't a "real" package, don't add it to the symlink tree. |
| continue; |
| } |
| Root pkgRoot = entry.getValue(); |
| int segmentCount = pkgId.getPackageFragment().segmentCount(); |
| for (int i = 1; i <= segmentCount; i++) { |
| PackageIdentifier dir = createInRepo(pkgId, pkgId.getPackageFragment().subFragment(0, i)); |
| Set<Root> roots = dirRootsMap.computeIfAbsent(dir, k -> Sets.newHashSet()); |
| roots.add(pkgRoot); |
| } |
| } |
| // Now add in roots for all non-pkg dirs that are in between two packages, and missed above. |
| for (Map.Entry<PackageIdentifier, Set<Root>> entry : dirRootsMap.entrySet()) { |
| PackageIdentifier dir = entry.getKey(); |
| if (!packageRoots.containsKey(dir)) { |
| PackageIdentifier pkgId = longestPathPrefix(dir, packageRoots.keySet()); |
| if (pkgId != null) { |
| entry.getValue().add(packageRoots.get(pkgId)); |
| } |
| } |
| } |
| // Create output dirs for all dirs that have more than one root and need to be split. |
| for (Map.Entry<PackageIdentifier, Set<Root>> entry : dirRootsMap.entrySet()) { |
| PackageIdentifier dir = entry.getKey(); |
| if (!dir.getRepository().isMain()) { |
| FileSystemUtils.createDirectoryAndParents( |
| execroot.getRelative(dir.getRepository().getPathUnderExecRoot())); |
| } |
| if (entry.getValue().size() > 1) { |
| if (LOG_FINER) { |
| logger.finer("mkdir " + execroot.getRelative(dir.getPathUnderExecRoot())); |
| } |
| FileSystemUtils.createDirectoryAndParents( |
| execroot.getRelative(dir.getPathUnderExecRoot())); |
| } |
| } |
| |
| // Make dir links for single rooted dirs. |
| for (Map.Entry<PackageIdentifier, Set<Root>> entry : dirRootsMap.entrySet()) { |
| PackageIdentifier dir = entry.getKey(); |
| Set<Root> roots = entry.getValue(); |
| // Simple case of one root for this dir. |
| if (roots.size() == 1) { |
| if (dir.getPackageFragment().segmentCount() > 1 |
| && dirRootsMap.get(getParent(dir)).size() == 1) { |
| continue; // skip--an ancestor will link this one in from above |
| } |
| // This is the top-most dir that can be linked to a single root. Make it so. |
| Root root = roots.iterator().next(); // lone root in set |
| if (LOG_FINER) { |
| logger.finer( |
| "ln -s " |
| + root.getRelative(dir.getSourceRoot()) |
| + " " |
| + execroot.getRelative(dir.getPathUnderExecRoot())); |
| } |
| execroot.getRelative(dir.getPathUnderExecRoot()) |
| .createSymbolicLink(root.getRelative(dir.getSourceRoot())); |
| } |
| } |
| // Make links for dirs within packages, skip parent-only dirs. |
| for (Map.Entry<PackageIdentifier, Set<Root>> entry : dirRootsMap.entrySet()) { |
| PackageIdentifier dir = entry.getKey(); |
| if (entry.getValue().size() > 1) { |
| // If this dir is at or below a package dir, link in its contents. |
| PackageIdentifier pkgId = longestPathPrefix(dir, packageRoots.keySet()); |
| if (pkgId != null) { |
| Root root = packageRoots.get(pkgId); |
| try { |
| Path absdir = root.getRelative(dir.getSourceRoot()); |
| if (absdir.isDirectory()) { |
| if (LOG_FINER) { |
| logger.finer( |
| "ln -s " + absdir + "/* " + execroot.getRelative(dir.getSourceRoot()) + "/"); |
| } |
| for (Path target : absdir.getDirectoryEntries()) { |
| PathFragment p = root.relativize(target); |
| if (!dirRootsMap.containsKey(createInRepo(pkgId, p))) { |
| //LOG.finest("ln -s " + target + " " + linkRoot.getRelative(p)); |
| execroot.getRelative(p).createSymbolicLink(target); |
| } |
| } |
| } else { |
| logger.fine("Symlink planting skipping dir '" + absdir + "'"); |
| } |
| } catch (IOException e) { |
| e.printStackTrace(); |
| } |
| // Otherwise its just an otherwise empty common parent dir. |
| } |
| } |
| } |
| |
| for (Map.Entry<PackageIdentifier, Root> entry : packageRoots.entrySet()) { |
| PackageIdentifier pkgId = entry.getKey(); |
| if (!pkgId.getPackageFragment().equals(PathFragment.EMPTY_FRAGMENT)) { |
| continue; |
| } |
| Path execrootDirectory = execroot.getRelative(pkgId.getPathUnderExecRoot()); |
| // If there were no subpackages, this directory might not exist yet. |
| if (!execrootDirectory.exists()) { |
| FileSystemUtils.createDirectoryAndParents(execrootDirectory); |
| } |
| // For the top-level directory, generate symlinks to everything in the directory instead of |
| // the directory itself. |
| Path sourceDirectory = entry.getValue().getRelative(pkgId.getSourceRoot()); |
| for (Path target : sourceDirectory.getDirectoryEntries()) { |
| String baseName = target.getBaseName(); |
| Path execPath = execrootDirectory.getRelative(baseName); |
| // Create any links that don't exist yet and don't start with bazel-. |
| if (!baseName.startsWith(productName + "-") && !execPath.exists()) { |
| execPath.createSymbolicLink(target); |
| } |
| } |
| } |
| |
| symlinkCorrectWorkspaceName(); |
| } |
| |
| /** |
| * Right now, the execution root is under the basename of the source directory, not the name |
| * defined in the WORKSPACE file. Thus, this adds a symlink with the WORKSPACE's workspace name |
| * to the old-style execution root. |
| * TODO(kchodorow): get rid of this once exec root is always under the WORKSPACE's workspace |
| * name. |
| * @throws IOException |
| */ |
| private void symlinkCorrectWorkspaceName() throws IOException { |
| Path correctDirectory = execroot.getParentDirectory().getRelative(workspaceName); |
| if (!correctDirectory.exists()) { |
| correctDirectory.createSymbolicLink(execroot); |
| } |
| } |
| |
| private static PackageIdentifier getParent(PackageIdentifier packageIdentifier) { |
| Preconditions.checkArgument( |
| packageIdentifier.getPackageFragment().getParentDirectory() != null); |
| return createInRepo( |
| packageIdentifier, packageIdentifier.getPackageFragment().getParentDirectory()); |
| } |
| |
| private static PackageIdentifier createInRepo( |
| PackageIdentifier repo, PathFragment packageFragment) { |
| return PackageIdentifier.create(repo.getRepository(), packageFragment); |
| } |
| } |