|
16 | 16 |
|
17 | 17 | import com.google.common.annotations.VisibleForTesting;
|
18 | 18 | import com.google.common.collect.ImmutableMap;
|
| 19 | +import com.google.common.collect.ImmutableSet; |
19 | 20 | import com.google.common.collect.Maps;
|
20 | 21 | import com.google.common.collect.Sets;
|
21 | 22 | import com.google.devtools.build.lib.cmdline.LabelConstants;
|
22 | 23 | import com.google.devtools.build.lib.cmdline.PackageIdentifier;
|
23 |
| -import com.google.devtools.build.lib.cmdline.RepositoryName; |
24 | 24 | import com.google.devtools.build.lib.concurrent.ThreadSafety;
|
| 25 | +import com.google.devtools.build.lib.vfs.FileSystemUtils; |
25 | 26 | import com.google.devtools.build.lib.vfs.Path;
|
26 | 27 | import com.google.devtools.build.lib.vfs.PathFragment;
|
27 | 28 | import com.google.devtools.build.lib.vfs.Root;
|
28 | 29 | import java.io.IOException;
|
| 30 | +import java.util.ArrayList; |
| 31 | +import java.util.Collections; |
29 | 32 | import java.util.Map;
|
30 | 33 | import java.util.Set;
|
| 34 | +import java.util.logging.Level; |
| 35 | +import java.util.logging.Logger; |
31 | 36 |
|
32 | 37 | /**
|
33 | 38 | * Creates a symlink forest based on a package path map.
|
34 | 39 | */
|
35 | 40 | class SymlinkForest {
|
| 41 | + |
| 42 | + private static final Logger logger = Logger.getLogger(SymlinkForest.class.getName()); |
| 43 | + private static final boolean LOG_FINER = logger.isLoggable(Level.FINER); |
| 44 | + |
36 | 45 | private final ImmutableMap<PackageIdentifier, Root> packageRoots;
|
37 | 46 | private final Path execroot;
|
38 |
| - private final String prefix; |
| 47 | + private final String workspaceName; |
| 48 | + private final String productName; |
| 49 | + private final String[] prefixes; |
39 | 50 |
|
40 | 51 | SymlinkForest(
|
41 |
| - ImmutableMap<PackageIdentifier, Root> packageRoots, Path execroot, String productName) { |
| 52 | + ImmutableMap<PackageIdentifier, Root> packageRoots, |
| 53 | + Path execroot, |
| 54 | + String productName, |
| 55 | + String workspaceName) { |
42 | 56 | this.packageRoots = packageRoots;
|
43 | 57 | this.execroot = execroot;
|
44 |
| - this.prefix = productName + "-"; |
| 58 | + this.workspaceName = workspaceName; |
| 59 | + this.productName = productName; |
| 60 | + this.prefixes = new String[] { ".", "_", productName + "-"}; |
45 | 61 | }
|
46 | 62 |
|
47 | 63 | /**
|
48 |
| - * Delete all dir trees under a given 'dir' that don't start with a given 'prefix'. Does not |
49 |
| - * follow any symbolic links. |
| 64 | + * Returns the longest prefix from a given set of 'prefixes' that are |
| 65 | + * contained in 'path'. I.e the closest ancestor directory containing path. |
| 66 | + * Returns null if none found. |
| 67 | + * @param path |
| 68 | + * @param prefixes |
50 | 69 | */
|
51 | 70 | @VisibleForTesting
|
52 |
| - @ThreadSafety.ThreadSafe |
53 |
| - static void deleteTreesBelowNotPrefixed(Path dir, String prefix) throws IOException { |
54 |
| - for (Path p : dir.getDirectoryEntries()) { |
55 |
| - if (!p.getBaseName().startsWith(prefix)) { |
56 |
| - p.deleteTree(); |
| 71 | + static PackageIdentifier longestPathPrefix( |
| 72 | + PackageIdentifier path, ImmutableSet<PackageIdentifier> prefixes) { |
| 73 | + for (int i = path.getPackageFragment().segmentCount(); i >= 0; i--) { |
| 74 | + PackageIdentifier prefix = createInRepo(path, path.getPackageFragment().subFragment(0, i)); |
| 75 | + if (prefixes.contains(prefix)) { |
| 76 | + return prefix; |
57 | 77 | }
|
58 | 78 | }
|
| 79 | + return null; |
59 | 80 | }
|
60 | 81 |
|
61 | 82 | /**
|
62 |
| - * Plant a symlink forest under execution root to ensure sources file are available and up to |
63 |
| - * date. For the main repo: If root package ("//:") is used, link every file and directory under |
64 |
| - * the top-level directory of the main repo. Otherwise, we only link the directories that are used |
65 |
| - * in presented main repo packages. For every external repo: make a such a directory link: |
66 |
| - * <execroot>/<ws_name>/external/<repo_name> --> <output_base>/external/<repo_name> |
| 83 | + * Delete all dir trees under a given 'dir' that don't start with one of a set |
| 84 | + * of given 'prefixes'. Does not follow any symbolic links. |
67 | 85 | */
|
68 |
| - void plantSymlinkForest() throws IOException { |
69 |
| - deleteTreesBelowNotPrefixed(execroot, prefix); |
| 86 | + @VisibleForTesting |
| 87 | + @ThreadSafety.ThreadSafe |
| 88 | + static void deleteTreesBelowNotPrefixed(Path dir, String[] prefixes) throws IOException { |
| 89 | + dirloop: |
| 90 | + for (Path p : dir.getDirectoryEntries()) { |
| 91 | + String name = p.getBaseName(); |
| 92 | + for (String prefix : prefixes) { |
| 93 | + if (name.startsWith(prefix)) { |
| 94 | + continue dirloop; |
| 95 | + } |
| 96 | + } |
| 97 | + p.deleteTree(); |
| 98 | + } |
| 99 | + } |
70 | 100 |
|
71 |
| - Path mainRepoRoot = null; |
72 |
| - Map<Path, Path> mainRepoLinks = Maps.newHashMap(); |
73 |
| - Set<Path> externalRepoLinks = Sets.newHashSet(); |
| 101 | + void plantSymlinkForest() throws IOException { |
| 102 | + deleteTreesBelowNotPrefixed(execroot, prefixes); |
| 103 | + // TODO(kchodorow): this can be removed once the execution root is rearranged. |
| 104 | + // Current state: symlink tree was created under execroot/$(basename ws) and then |
| 105 | + // execroot/wsname is symlinked to that. The execution root change creates (and cleans up) |
| 106 | + // subtrees for each repository and has been rolled forward and back several times. Thus, if |
| 107 | + // someone was using a with-execroot-change version of bazel and then switched to this one, |
| 108 | + // their execution root would contain a subtree for execroot/wsname that would never be |
| 109 | + // cleaned up by this version of Bazel. |
| 110 | + Path realWorkspaceDir = execroot.getParentDirectory().getRelative(workspaceName); |
| 111 | + if (!workspaceName.equals(execroot.getBaseName()) && realWorkspaceDir.exists() |
| 112 | + && !realWorkspaceDir.isSymbolicLink()) { |
| 113 | + realWorkspaceDir.deleteTree(); |
| 114 | + } |
74 | 115 |
|
| 116 | + // Packages come from exactly one root, but their shared ancestors may come from more. |
| 117 | + Map<PackageIdentifier, Set<Root>> dirRootsMap = Maps.newHashMap(); |
| 118 | + // Elements in this list are added so that parents come before their children. |
| 119 | + ArrayList<PackageIdentifier> dirsParentsFirst = new ArrayList<>(); |
75 | 120 | for (Map.Entry<PackageIdentifier, Root> entry : packageRoots.entrySet()) {
|
76 | 121 | PackageIdentifier pkgId = entry.getKey();
|
77 | 122 | if (pkgId.equals(LabelConstants.EXTERNAL_PACKAGE_IDENTIFIER)) {
|
78 | 123 | // This isn't a "real" package, don't add it to the symlink tree.
|
79 | 124 | continue;
|
80 | 125 | }
|
81 |
| - RepositoryName repository = pkgId.getRepository(); |
82 |
| - if (repository.isMain() || repository.isDefault()) { |
83 |
| - // If root package of the main repo is required, we record the main repo root so that |
84 |
| - // we can later link everything under main repo's top-level directory. And in this case, |
85 |
| - // we don't need to record other links for directories under the top-level directory any |
86 |
| - // more. |
87 |
| - if (pkgId.getPackageFragment().equals(PathFragment.EMPTY_FRAGMENT)) { |
88 |
| - mainRepoRoot = entry.getValue().getRelative(pkgId.getSourceRoot()); |
| 126 | + Root pkgRoot = entry.getValue(); |
| 127 | + ArrayList<PackageIdentifier> newDirs = new ArrayList<>(); |
| 128 | + for (PathFragment fragment = pkgId.getPackageFragment(); |
| 129 | + !fragment.isEmpty(); |
| 130 | + fragment = fragment.getParentDirectory()) { |
| 131 | + PackageIdentifier dirId = createInRepo(pkgId, fragment); |
| 132 | + Set<Root> roots = dirRootsMap.get(dirId); |
| 133 | + if (roots == null) { |
| 134 | + roots = Sets.newHashSet(); |
| 135 | + dirRootsMap.put(dirId, roots); |
| 136 | + newDirs.add(dirId); |
89 | 137 | }
|
90 |
| - if (mainRepoRoot == null) { |
91 |
| - Path execrootLink = execroot.getRelative(pkgId.getPackageFragment().getSegment(0)); |
92 |
| - Path sourcePath = entry.getValue().getRelative(pkgId.getSourceRoot().getSegment(0)); |
93 |
| - mainRepoLinks.putIfAbsent(execrootLink, sourcePath); |
| 138 | + roots.add(pkgRoot); |
| 139 | + } |
| 140 | + Collections.reverse(newDirs); |
| 141 | + dirsParentsFirst.addAll(newDirs); |
| 142 | + } |
| 143 | + // Now add in roots for all non-pkg dirs that are in between two packages, and missed above. |
| 144 | + for (PackageIdentifier dir : dirsParentsFirst) { |
| 145 | + if (!packageRoots.containsKey(dir)) { |
| 146 | + PackageIdentifier pkgId = longestPathPrefix(dir, packageRoots.keySet()); |
| 147 | + if (pkgId != null) { |
| 148 | + dirRootsMap.get(dir).add(packageRoots.get(pkgId)); |
94 | 149 | }
|
95 |
| - } else { |
96 |
| - // For other external repositories, generate a symlink to the external repository |
97 |
| - // directory itself. |
98 |
| - // <output_base>/execroot/<main repo name>/external/<external repo name> --> |
99 |
| - // <output_base>/external/<external repo name> |
100 |
| - Path execrootLink = execroot.getRelative(repository.getPathUnderExecRoot()); |
101 |
| - Path sourcePath = entry.getValue().getRelative(repository.getSourceRoot()); |
102 |
| - if (externalRepoLinks.contains(execrootLink)) { |
103 |
| - continue; |
| 150 | + } |
| 151 | + } |
| 152 | + // Create output dirs for all dirs that have more than one root and need to be split. |
| 153 | + for (PackageIdentifier dir : dirsParentsFirst) { |
| 154 | + if (!dir.getRepository().isMain()) { |
| 155 | + FileSystemUtils.createDirectoryAndParents( |
| 156 | + execroot.getRelative(dir.getRepository().getPathUnderExecRoot())); |
| 157 | + } |
| 158 | + if (dirRootsMap.get(dir).size() > 1) { |
| 159 | + if (LOG_FINER) { |
| 160 | + logger.finer("mkdir " + execroot.getRelative(dir.getPathUnderExecRoot())); |
104 | 161 | }
|
105 |
| - if (externalRepoLinks.isEmpty()) { |
106 |
| - execroot.getRelative(LabelConstants.EXTERNAL_PACKAGE_NAME).createDirectoryAndParents(); |
| 162 | + FileSystemUtils.createDirectoryAndParents( |
| 163 | + execroot.getRelative(dir.getPathUnderExecRoot())); |
| 164 | + } |
| 165 | + } |
| 166 | + |
| 167 | + // Make dir links for single rooted dirs. |
| 168 | + for (PackageIdentifier dir : dirsParentsFirst) { |
| 169 | + Set<Root> roots = dirRootsMap.get(dir); |
| 170 | + // Simple case of one root for this dir. |
| 171 | + if (roots.size() == 1) { |
| 172 | + PathFragment parent = dir.getPackageFragment().getParentDirectory(); |
| 173 | + if (!parent.isEmpty() && dirRootsMap.get(createInRepo(dir, parent)).size() == 1) { |
| 174 | + continue; // skip--an ancestor will link this one in from above |
| 175 | + } |
| 176 | + // This is the top-most dir that can be linked to a single root. Make it so. |
| 177 | + Root root = roots.iterator().next(); // lone root in set |
| 178 | + if (LOG_FINER) { |
| 179 | + logger.finer( |
| 180 | + "ln -s " |
| 181 | + + root.getRelative(dir.getSourceRoot()) |
| 182 | + + " " |
| 183 | + + execroot.getRelative(dir.getPathUnderExecRoot())); |
| 184 | + } |
| 185 | + execroot.getRelative(dir.getPathUnderExecRoot()) |
| 186 | + .createSymbolicLink(root.getRelative(dir.getSourceRoot())); |
| 187 | + } |
| 188 | + } |
| 189 | + // Make links for dirs within packages, skip parent-only dirs. |
| 190 | + for (PackageIdentifier dir : dirsParentsFirst) { |
| 191 | + if (dirRootsMap.get(dir).size() > 1) { |
| 192 | + // If this dir is at or below a package dir, link in its contents. |
| 193 | + PackageIdentifier pkgId = longestPathPrefix(dir, packageRoots.keySet()); |
| 194 | + if (pkgId != null) { |
| 195 | + Root root = packageRoots.get(pkgId); |
| 196 | + try { |
| 197 | + Path absdir = root.getRelative(dir.getSourceRoot()); |
| 198 | + if (absdir.isDirectory()) { |
| 199 | + if (LOG_FINER) { |
| 200 | + logger.finer( |
| 201 | + "ln -s " + absdir + "/* " + execroot.getRelative(dir.getSourceRoot()) + "/"); |
| 202 | + } |
| 203 | + for (Path target : absdir.getDirectoryEntries()) { |
| 204 | + PathFragment p = root.relativize(target); |
| 205 | + if (!dirRootsMap.containsKey(createInRepo(pkgId, p))) { |
| 206 | + //LOG.finest("ln -s " + target + " " + linkRoot.getRelative(p)); |
| 207 | + execroot.getRelative(p).createSymbolicLink(target); |
| 208 | + } |
| 209 | + } |
| 210 | + } else { |
| 211 | + logger.fine("Symlink planting skipping dir '" + absdir + "'"); |
| 212 | + } |
| 213 | + } catch (IOException e) { |
| 214 | + e.printStackTrace(); |
| 215 | + } |
| 216 | + // Otherwise its just an otherwise empty common parent dir. |
107 | 217 | }
|
108 |
| - externalRepoLinks.add(execrootLink); |
109 |
| - execrootLink.createSymbolicLink(sourcePath); |
110 | 218 | }
|
111 | 219 | }
|
112 |
| - if (mainRepoRoot != null) { |
113 |
| - // For the main repo top-level directory, generate symlinks to everything in the directory |
114 |
| - // instead of the directory itself. |
115 |
| - for (Path target : mainRepoRoot.getDirectoryEntries()) { |
| 220 | + |
| 221 | + for (Map.Entry<PackageIdentifier, Root> entry : packageRoots.entrySet()) { |
| 222 | + PackageIdentifier pkgId = entry.getKey(); |
| 223 | + if (!pkgId.getPackageFragment().equals(PathFragment.EMPTY_FRAGMENT)) { |
| 224 | + continue; |
| 225 | + } |
| 226 | + Path execrootDirectory = execroot.getRelative(pkgId.getPathUnderExecRoot()); |
| 227 | + // If there were no subpackages, this directory might not exist yet. |
| 228 | + if (!execrootDirectory.exists()) { |
| 229 | + FileSystemUtils.createDirectoryAndParents(execrootDirectory); |
| 230 | + } |
| 231 | + // For the top-level directory, generate symlinks to everything in the directory instead of |
| 232 | + // the directory itself. |
| 233 | + Path sourceDirectory = entry.getValue().getRelative(pkgId.getSourceRoot()); |
| 234 | + for (Path target : sourceDirectory.getDirectoryEntries()) { |
116 | 235 | String baseName = target.getBaseName();
|
117 |
| - Path execPath = execroot.getRelative(baseName); |
118 |
| - // Create any links that don't start with bazel-. |
119 |
| - if (!baseName.startsWith(prefix)) { |
| 236 | + Path execPath = execrootDirectory.getRelative(baseName); |
| 237 | + // Create any links that don't exist yet and don't start with bazel-. |
| 238 | + if (!baseName.startsWith(productName + "-") && !execPath.exists()) { |
120 | 239 | execPath.createSymbolicLink(target);
|
121 | 240 | }
|
122 | 241 | }
|
123 |
| - } else { |
124 |
| - for (Map.Entry<Path, Path> entry : mainRepoLinks.entrySet()) { |
125 |
| - Path link = entry.getKey(); |
126 |
| - Path target = entry.getValue(); |
127 |
| - link.createSymbolicLink(target); |
128 |
| - } |
129 | 242 | }
|
| 243 | + |
| 244 | + symlinkCorrectWorkspaceName(); |
| 245 | + } |
| 246 | + |
| 247 | + /** |
| 248 | + * Right now, the execution root is under the basename of the source directory, not the name |
| 249 | + * defined in the WORKSPACE file. Thus, this adds a symlink with the WORKSPACE's workspace name |
| 250 | + * to the old-style execution root. |
| 251 | + * TODO(kchodorow): get rid of this once exec root is always under the WORKSPACE's workspace |
| 252 | + * name. |
| 253 | + * @throws IOException |
| 254 | + */ |
| 255 | + private void symlinkCorrectWorkspaceName() throws IOException { |
| 256 | + Path correctDirectory = execroot.getParentDirectory().getRelative(workspaceName); |
| 257 | + if (!correctDirectory.exists()) { |
| 258 | + correctDirectory.createSymbolicLink(execroot); |
| 259 | + } |
| 260 | + } |
| 261 | + |
| 262 | + private static PackageIdentifier createInRepo( |
| 263 | + PackageIdentifier repo, PathFragment packageFragment) { |
| 264 | + return PackageIdentifier.create(repo.getRepository(), packageFragment); |
130 | 265 | }
|
131 | 266 | }
|
0 commit comments