blob: 76a5f3793600c65ca8c236660ed05db1bb401481 [file] [log] [blame]
// Copyright 2019 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.exec;
import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.ArtifactPathResolver;
import com.google.devtools.build.lib.actions.ExecException;
import com.google.devtools.build.lib.actions.RunfilesSupplier;
import com.google.devtools.build.lib.analysis.RunfilesSupport;
import com.google.devtools.build.lib.profiler.Profiler;
import com.google.devtools.build.lib.profiler.ProfilerTask;
import com.google.devtools.build.lib.util.io.OutErr;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
/**
* Utility used in local execution to create a runfiles tree if {@code --nobuild_runfile_links} has
* been specified.
*
* <p>It is safe to call {@link #updateRunfilesDirectory} concurrently.
*/
@ThreadSafe
public class RunfilesTreeUpdater {
public static final RunfilesTreeUpdater INSTANCE = new RunfilesTreeUpdater();
private final Object lock = new Object();
private static final class LockWithRefcnt {
int refcnt = 1;
}
/**
* Poor man's reference counted object pool.
*
* <p>Maintains a mapping of runfiles directory to a monitor. The monitor maintains a counter that
* tracks how many threads it is acquired by at the moment. If the count drops to zero the mapping
* is removed.
*/
@GuardedBy("lock")
private final Map<PathFragment, LockWithRefcnt> locksWithRefcnt = new HashMap<>();
private RunfilesTreeUpdater() {}
private static void updateRunfilesTree(
Path execRoot,
PathFragment runfilesDir,
BinTools binTools,
ImmutableMap<String, String> env,
OutErr outErr,
boolean enableRunfiles)
throws IOException, ExecException {
Path runfilesDirPath = execRoot.getRelative(runfilesDir);
Path inputManifest = RunfilesSupport.inputManifestPath(runfilesDirPath);
if (!inputManifest.exists()) {
return;
}
Path outputManifest = RunfilesSupport.outputManifestPath(runfilesDirPath);
try {
// Avoid rebuilding the runfiles directory if the manifest in it matches the input manifest,
// implying the symlinks exist and are already up to date. If the output manifest is a
// symbolic link, it is likely a symbolic link to the input manifest, so we cannot trust it as
// an up-to-date check.
if (!outputManifest.isSymbolicLink()
&& Arrays.equals(outputManifest.getDigest(), inputManifest.getDigest())) {
return;
}
} catch (IOException e) {
// Ignore it - we will just try to create runfiles directory.
}
if (!runfilesDirPath.exists()) {
runfilesDirPath.createDirectoryAndParents();
}
SymlinkTreeHelper helper =
new SymlinkTreeHelper(inputManifest, runfilesDirPath, /* filesetTree= */ false);
helper.createSymlinks(execRoot, outErr, binTools, env, enableRunfiles);
}
private LockWithRefcnt getLockAndIncrementRefcnt(PathFragment runfilesDirectory) {
synchronized (lock) {
LockWithRefcnt lock = locksWithRefcnt.get(runfilesDirectory);
if (lock != null) {
lock.refcnt++;
return lock;
}
lock = new LockWithRefcnt();
locksWithRefcnt.put(runfilesDirectory, lock);
return lock;
}
}
private void decrementRefcnt(PathFragment runfilesDirectory) {
synchronized (lock) {
LockWithRefcnt lock = locksWithRefcnt.get(runfilesDirectory);
lock.refcnt--;
if (lock.refcnt == 0) {
if (!locksWithRefcnt.remove(runfilesDirectory, lock)) {
throw new IllegalStateException(
String.format(
"Failed to remove lock for dir '%s'." + " This is a bug.", runfilesDirectory));
}
}
}
}
public void updateRunfilesDirectory(
Path execRoot,
RunfilesSupplier runfilesSupplier,
ArtifactPathResolver pathResolver,
BinTools binTools,
ImmutableMap<String, String> env,
OutErr outErr)
throws ExecException, IOException {
for (Map.Entry<PathFragment, Map<PathFragment, Artifact>> runfiles :
runfilesSupplier.getMappings(pathResolver).entrySet()) {
PathFragment runfilesDir = runfiles.getKey();
if (runfilesSupplier.isBuildRunfileLinks(runfilesDir)) {
continue;
}
try {
long startTime = Profiler.nanoTimeMaybe();
// Synchronize runfiles tree generation on the runfiles directory in order to prevent
// concurrent modifications of the runfiles tree. In particular this can happen for sharded
// tests and --runs_per_test > 1 in which case multiple actions use the same runfiles tree.
synchronized (getLockAndIncrementRefcnt(runfilesDir)) {
Profiler.instance()
.logSimpleTask(startTime, ProfilerTask.WAIT, "Waiting to create runfiles tree");
updateRunfilesTree(
execRoot,
runfilesDir,
binTools,
env,
outErr,
runfilesSupplier.isRunfileLinksEnabled(runfilesDir));
}
} finally {
decrementRefcnt(runfilesDir);
}
}
}
}