Skip to content

Commit 844e4e2

Browse files
meteorcloudycopybara-github
authored andcommitted
Make SymlinkForest simpler and more efficient
Bazel rebuilds the symlink tree under the execution root before every build to ensure source files from main repo and external repos are available and up to date. But the SymlinkForest has accumulated many legacy behaviors that is currently not necessary and inefficient. This change tries to simplify the logic and make it much more faster. The main improvement is that instead of linking every file and dir under the top-level directory for every external repo, we only create a link to the top-level directory of the external repo. This will reduce a large amount of symlink create operations, which speeds up the preparing phase a lot on Windows. RELNOTES: None PiperOrigin-RevId: 246520821
1 parent ba4862d commit 844e4e2

File tree

3 files changed

+142
-374
lines changed

3 files changed

+142
-374
lines changed

src/main/java/com/google/devtools/build/lib/buildtool/ExecutionTool.java

+1-2
Original file line numberDiff line numberDiff line change
@@ -453,8 +453,7 @@ private void prepare(PackageRoots packageRoots)
453453

454454
// Plant the symlink forest.
455455
try (SilentCloseable c = Profiler.instance().profile("plantSymlinkForest")) {
456-
new SymlinkForest(
457-
packageRootMap.get(), getExecRoot(), runtime.getProductName(), env.getWorkspaceName())
456+
new SymlinkForest(packageRootMap.get(), getExecRoot(), runtime.getProductName())
458457
.plantSymlinkForest();
459458
} catch (IOException e) {
460459
throw new ExecutorInitException("Source forest creation failed", e);

src/main/java/com/google/devtools/build/lib/buildtool/SymlinkForest.java

+59-194
Original file line numberDiff line numberDiff line change
@@ -16,251 +16,116 @@
1616

1717
import com.google.common.annotations.VisibleForTesting;
1818
import com.google.common.collect.ImmutableMap;
19-
import com.google.common.collect.ImmutableSet;
2019
import com.google.common.collect.Maps;
2120
import com.google.common.collect.Sets;
2221
import com.google.devtools.build.lib.cmdline.LabelConstants;
2322
import com.google.devtools.build.lib.cmdline.PackageIdentifier;
23+
import com.google.devtools.build.lib.cmdline.RepositoryName;
2424
import com.google.devtools.build.lib.concurrent.ThreadSafety;
25-
import com.google.devtools.build.lib.vfs.FileSystemUtils;
2625
import com.google.devtools.build.lib.vfs.Path;
2726
import com.google.devtools.build.lib.vfs.PathFragment;
2827
import com.google.devtools.build.lib.vfs.Root;
2928
import java.io.IOException;
30-
import java.util.ArrayList;
31-
import java.util.Collections;
3229
import java.util.Map;
3330
import java.util.Set;
34-
import java.util.logging.Level;
35-
import java.util.logging.Logger;
3631

3732
/**
3833
* Creates a symlink forest based on a package path map.
3934
*/
4035
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-
4536
private final ImmutableMap<PackageIdentifier, Root> packageRoots;
4637
private final Path execroot;
47-
private final String workspaceName;
48-
private final String productName;
49-
private final String[] prefixes;
38+
private final String prefix;
5039

5140
SymlinkForest(
52-
ImmutableMap<PackageIdentifier, Root> packageRoots,
53-
Path execroot,
54-
String productName,
55-
String workspaceName) {
41+
ImmutableMap<PackageIdentifier, Root> packageRoots, Path execroot, String productName) {
5642
this.packageRoots = packageRoots;
5743
this.execroot = execroot;
58-
this.workspaceName = workspaceName;
59-
this.productName = productName;
60-
this.prefixes = new String[] { ".", "_", productName + "-"};
44+
this.prefix = productName + "-";
6145
}
6246

6347
/**
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
69-
*/
70-
@VisibleForTesting
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;
77-
}
78-
}
79-
return null;
80-
}
81-
82-
/**
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.
48+
* Delete all dir trees under a given 'dir' that don't start with a given 'prefix'. Does not
49+
* follow any symbolic links.
8550
*/
8651
@VisibleForTesting
8752
@ThreadSafety.ThreadSafe
88-
static void deleteTreesBelowNotPrefixed(Path dir, String[] prefixes) throws IOException {
89-
dirloop:
53+
static void deleteTreesBelowNotPrefixed(Path dir, String prefix) throws IOException {
9054
for (Path p : dir.getDirectoryEntries()) {
91-
String name = p.getBaseName();
92-
for (String prefix : prefixes) {
93-
if (name.startsWith(prefix)) {
94-
continue dirloop;
95-
}
55+
if (!p.getBaseName().startsWith(prefix)) {
56+
p.deleteTree();
9657
}
97-
p.deleteTree();
9858
}
9959
}
10060

61+
/**
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>
67+
*/
10168
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-
}
69+
deleteTreesBelowNotPrefixed(execroot, prefix);
70+
71+
Path mainRepoRoot = null;
72+
Map<Path, Path> mainRepoLinks = Maps.newHashMap();
73+
Set<Path> externalRepoLinks = Sets.newHashSet();
11574

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<>();
12075
for (Map.Entry<PackageIdentifier, Root> entry : packageRoots.entrySet()) {
12176
PackageIdentifier pkgId = entry.getKey();
12277
if (pkgId.equals(LabelConstants.EXTERNAL_PACKAGE_IDENTIFIER)) {
12378
// This isn't a "real" package, don't add it to the symlink tree.
12479
continue;
12580
}
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);
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());
13789
}
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));
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);
14994
}
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()));
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;
161104
}
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.
105+
if (externalRepoLinks.isEmpty()) {
106+
execroot.getRelative(LabelConstants.EXTERNAL_PACKAGE_NAME).createDirectoryAndParents();
217107
}
108+
externalRepoLinks.add(execrootLink);
109+
execrootLink.createSymbolicLink(sourcePath);
218110
}
219111
}
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()) {
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()) {
235116
String baseName = target.getBaseName();
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()) {
117+
Path execPath = execroot.getRelative(baseName);
118+
// Create any links that don't start with bazel-.
119+
if (!baseName.startsWith(prefix)) {
239120
execPath.createSymbolicLink(target);
240121
}
241122
}
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+
}
242129
}
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);
265130
}
266131
}

0 commit comments

Comments
 (0)