diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxUtil.java b/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxUtil.java index c5830c71b4be15..4b4519bdbb522f 100644 --- a/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxUtil.java +++ b/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxUtil.java @@ -14,7 +14,7 @@ package com.google.devtools.build.lib.sandbox; -import com.google.devtools.build.lib.runtime.CommandEnvironment; +import com.google.devtools.build.lib.runtime.BlazeWorkspace; import com.google.devtools.build.lib.util.OsUtils; import com.google.devtools.build.lib.vfs.Path; @@ -23,14 +23,14 @@ public final class LinuxSandboxUtil { private static final String LINUX_SANDBOX = "linux-sandbox" + OsUtils.executableExtension(); /** Returns whether using the {@code linux-sandbox} is supported in the command environment. */ - public static boolean isSupported(CommandEnvironment cmdEnv) { + public static boolean isSupported(BlazeWorkspace blazeWorkspace) { // We can only use the linux-sandbox if the linux-sandbox exists in the embedded tools. // This might not always be the case, e.g. while bootstrapping. - return getLinuxSandbox(cmdEnv) != null; + return getLinuxSandbox(blazeWorkspace) != null; } /** Returns the path of the {@code linux-sandbox} binary, or null if it doesn't exist. */ - public static Path getLinuxSandbox(CommandEnvironment cmdEnv) { - return cmdEnv.getBlazeWorkspace().getBinTools().getEmbeddedPath(LINUX_SANDBOX); + public static Path getLinuxSandbox(BlazeWorkspace blazeWorkspace) { + return blazeWorkspace.getBinTools().getEmbeddedPath(LINUX_SANDBOX); } } diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedSpawnRunner.java b/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedSpawnRunner.java index 1da4ee5b2dfa6b..573064244445f7 100644 --- a/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedSpawnRunner.java +++ b/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedSpawnRunner.java @@ -74,10 +74,10 @@ public static boolean isSupported(final CommandEnvironment cmdEnv) throws Interr if (OS.getCurrent() != OS.LINUX) { return false; } - if (!LinuxSandboxUtil.isSupported(cmdEnv)) { + if (!LinuxSandboxUtil.isSupported(cmdEnv.getBlazeWorkspace())) { return false; } - Path linuxSandbox = LinuxSandboxUtil.getLinuxSandbox(cmdEnv); + Path linuxSandbox = LinuxSandboxUtil.getLinuxSandbox(cmdEnv.getBlazeWorkspace()); Boolean isSupported; synchronized (isSupportedMap) { isSupported = isSupportedMap.get(linuxSandbox); @@ -156,7 +156,7 @@ private static boolean computeIsSupported(CommandEnvironment cmdEnv, Path linuxS this.blazeDirs = cmdEnv.getDirectories(); this.execRoot = cmdEnv.getExecRoot(); this.allowNetwork = helpers.shouldAllowNetwork(cmdEnv.getOptions()); - this.linuxSandbox = LinuxSandboxUtil.getLinuxSandbox(cmdEnv); + this.linuxSandbox = LinuxSandboxUtil.getLinuxSandbox(cmdEnv.getBlazeWorkspace()); this.sandboxBase = sandboxBase; this.inaccessibleHelperFile = inaccessibleHelperFile; this.inaccessibleHelperDir = inaccessibleHelperDir; diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxHelpers.java b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxHelpers.java index 33f0c17676ef9f..cc5974ada91f9b 100644 --- a/src/main/java/com/google/devtools/build/lib/sandbox/SandboxHelpers.java +++ b/src/main/java/com/google/devtools/build/lib/sandbox/SandboxHelpers.java @@ -177,7 +177,6 @@ public static void cleanExisting( parent = parent.getParentDirectory(); } } - cleanRecursively(root, inputs, inputsToCreate, dirsToCreate, workDir, prefixDirs); } diff --git a/src/main/java/com/google/devtools/build/lib/worker/BUILD b/src/main/java/com/google/devtools/build/lib/worker/BUILD index d18410a59e4dcd..28ad22aa1cf360 100644 --- a/src/main/java/com/google/devtools/build/lib/worker/BUILD +++ b/src/main/java/com/google/devtools/build/lib/worker/BUILD @@ -37,6 +37,7 @@ java_library( ":worker_protocol", "//src/main/java/com/google/devtools/build/lib/actions", "//src/main/java/com/google/devtools/build/lib/events", + "//src/main/java/com/google/devtools/build/lib/sandbox:linux_sandbox_command_line_builder", "//src/main/java/com/google/devtools/build/lib/sandbox:sandbox_helpers", "//src/main/java/com/google/devtools/build/lib/shell", "//src/main/java/com/google/devtools/build/lib/vfs", @@ -144,6 +145,7 @@ java_library( "//src/main/java/com/google/devtools/build/lib/exec:spawn_strategy_registry", "//src/main/java/com/google/devtools/build/lib/exec/local", "//src/main/java/com/google/devtools/build/lib/runtime/commands/events", + "//src/main/java/com/google/devtools/build/lib/sandbox", "//src/main/java/com/google/devtools/build/lib/sandbox:sandbox_helpers", "//src/main/java/com/google/devtools/build/lib/vfs", "//src/main/java/com/google/devtools/common/options", diff --git a/src/main/java/com/google/devtools/build/lib/worker/SandboxedWorker.java b/src/main/java/com/google/devtools/build/lib/worker/SandboxedWorker.java index c99cf2d8b5aa41..fff498a95cdda2 100644 --- a/src/main/java/com/google/devtools/build/lib/worker/SandboxedWorker.java +++ b/src/main/java/com/google/devtools/build/lib/worker/SandboxedWorker.java @@ -14,22 +14,76 @@ package com.google.devtools.build.lib.worker; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Maps; import com.google.common.flogger.GoogleLogger; +import com.google.devtools.build.lib.sandbox.LinuxSandboxCommandLineBuilder; import com.google.devtools.build.lib.sandbox.SandboxHelpers.SandboxInputs; import com.google.devtools.build.lib.sandbox.SandboxHelpers.SandboxOutputs; +import com.google.devtools.build.lib.shell.Subprocess; +import com.google.devtools.build.lib.shell.SubprocessBuilder; +import com.google.devtools.build.lib.vfs.FileSystem; import com.google.devtools.build.lib.vfs.Path; import com.google.devtools.build.lib.vfs.PathFragment; +import java.io.File; import java.io.IOException; import java.util.Set; +import java.util.SortedMap; +import javax.annotation.Nullable; /** A {@link SingleplexWorker} that runs inside a sandboxed execution root. */ final class SandboxedWorker extends SingleplexWorker { + + public static final String TMP_DIR_MOUNT_NAME = "_tmp"; + + @AutoValue + public abstract static class WorkerSandboxOptions { + // Need to have this data class because we can't depend on SandboxOptions in here. + abstract boolean fakeHostname(); + + abstract boolean fakeUsername(); + + abstract boolean debugMode(); + + abstract ImmutableList tmpfsPath(); + + abstract ImmutableList writablePaths(); + + abstract Path sandboxBinary(); + + public static WorkerSandboxOptions create( + Path sandboxBinary, + boolean fakeHostname, + boolean fakeUsername, + boolean debugMode, + ImmutableList tmpfsPath, + ImmutableList writablePaths) { + return new AutoValue_SandboxedWorker_WorkerSandboxOptions( + fakeHostname, fakeUsername, debugMode, tmpfsPath, writablePaths, sandboxBinary); + } + } + private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); private final WorkerExecRoot workerExecRoot; + /** Options specific to hardened sandbox, null if not using that. */ + @Nullable private final WorkerSandboxOptions hardenedSandboxOptions; - SandboxedWorker(WorkerKey workerKey, int workerId, Path workDir, Path logFile) { + SandboxedWorker( + WorkerKey workerKey, + int workerId, + Path workDir, + Path logFile, + @Nullable WorkerSandboxOptions hardenedSandboxOptions) { super(workerKey, workerId, workDir, logFile); - this.workerExecRoot = new WorkerExecRoot(workDir); + this.workerExecRoot = + new WorkerExecRoot( + workDir, + hardenedSandboxOptions != null + ? ImmutableList.of(PathFragment.create("../" + TMP_DIR_MOUNT_NAME)) + : ImmutableList.of()); + this.hardenedSandboxOptions = hardenedSandboxOptions; } @Override @@ -37,6 +91,110 @@ public boolean isSandboxed() { return true; } + private ImmutableSet getWritableDirs(Path sandboxExecRoot) throws IOException { + // We have to make the TEST_TMPDIR directory writable if it is specified. + ImmutableSet.Builder writableDirs = + ImmutableSet.builder().add(sandboxExecRoot).add(sandboxExecRoot.getRelative("/tmp")); + + FileSystem fileSystem = sandboxExecRoot.getFileSystem(); + for (String writablePath : hardenedSandboxOptions.writablePaths()) { + Path path = fileSystem.getPath(writablePath); + writableDirs.add(path); + if (path.isSymbolicLink()) { + writableDirs.add(path.resolveSymbolicLinks()); + } + } + + FileSystem fs = sandboxExecRoot.getFileSystem(); + writableDirs.add(fs.getPath("/dev/shm").resolveSymbolicLinks()); + writableDirs.add(fs.getPath("/tmp")); + + return writableDirs.build(); + } + + private SortedMap getBindMounts(Path sandboxExecRoot, @Nullable Path sandboxTmp) { + Path tmpPath = sandboxExecRoot.getFileSystem().getPath("/tmp"); + final SortedMap bindMounts = Maps.newTreeMap(); + // Mount a fresh, empty temporary directory as /tmp for each sandbox rather than reusing the + // host filesystem's /tmp. Since we're in a worker, we clean this dir between requests. + bindMounts.put(tmpPath, sandboxTmp); + // TODO(larsrc): Apply InaccessiblePaths + // for (Path inaccessiblePath : getInaccessiblePaths()) { + // if (inaccessiblePath.isDirectory(Symlinks.NOFOLLOW)) { + // bindMounts.put(inaccessiblePath, inaccessibleHelperDir); + // } else { + // bindMounts.put(inaccessiblePath, inaccessibleHelperFile); + // } + // } + // validateBindMounts(bindMounts); + return bindMounts; + } + + @Override + protected Subprocess createProcess() throws IOException { + // TODO(larsrc): Check that execRoot and outputBase are not under /tmp + // TODO(larsrc): Maybe deduplicate this code copied from super.createProcess() + if (hardenedSandboxOptions != null) { + this.shutdownHook = + new Thread( + () -> { + this.shutdownHook = null; + this.destroy(); + }); + Runtime.getRuntime().addShutdownHook(shutdownHook); + + // TODO(larsrc): Figure out what of the environment rewrite needs doing. + // ImmutableMap environment = + // localEnvProvider.rewriteLocalEnv(spawn.getEnvironment(), binTools, "/tmp"); + + // TODO(larsrc): Figure out which things can change and make sure workers get restarted + // ImmutableSet writableDirs = getWritableDirs(workerExecRoot, environment); + + ImmutableList args = workerKey.getArgs(); + File executable = new File(args.get(0)); + if (!executable.isAbsolute() && executable.getParent() != null) { + args = + ImmutableList.builderWithExpectedSize(args.size()) + .add(new File(workDir.getPathFile(), args.get(0)).getAbsolutePath()) + .addAll(args.subList(1, args.size())) + .build(); + } + + // In hardened mode, we bindmount a temp dir. We put the mount dir in the parent directory to + // avoid clashes with workspace files. + Path sandboxTmp = workDir.getParentDirectory().getRelative(TMP_DIR_MOUNT_NAME); + sandboxTmp.createDirectoryAndParents(); + + // TODO(larsrc): Need to make error messages go to stderr. + LinuxSandboxCommandLineBuilder commandLineBuilder = + LinuxSandboxCommandLineBuilder.commandLineBuilder( + this.hardenedSandboxOptions.sandboxBinary(), args) + .setWritableFilesAndDirectories(getWritableDirs(workDir)) + // Need all the sandbox options passed in here? + .setTmpfsDirectories(ImmutableSet.copyOf(this.hardenedSandboxOptions.tmpfsPath())) + .setBindMounts(getBindMounts(workDir, sandboxTmp)) + .setUseFakeHostname(this.hardenedSandboxOptions.fakeHostname()) + // Mostly tests require network, and some blaze run commands + .setCreateNetworkNamespace(true) + .setUseDebugMode(hardenedSandboxOptions.debugMode()); + + if (this.hardenedSandboxOptions.fakeUsername()) { + commandLineBuilder.setUseFakeUsername(true); + } + + SubprocessBuilder processBuilder = new SubprocessBuilder(); + ImmutableList argv = commandLineBuilder.build(); + processBuilder.setArgv(argv); + processBuilder.setWorkingDirectory(workDir.getPathFile()); + processBuilder.setStderr(logFile.getPathFile()); + processBuilder.setEnv(workerKey.getEnv()); + + return processBuilder.start(); + } else { + return super.createProcess(); + } + } + @Override public void prepareExecution( SandboxInputs inputFiles, SandboxOutputs outputs, Set workerFiles) diff --git a/src/main/java/com/google/devtools/build/lib/worker/SingleplexWorker.java b/src/main/java/com/google/devtools/build/lib/worker/SingleplexWorker.java index 094607722487d8..79adf3280c7706 100644 --- a/src/main/java/com/google/devtools/build/lib/worker/SingleplexWorker.java +++ b/src/main/java/com/google/devtools/build/lib/worker/SingleplexWorker.java @@ -64,14 +64,14 @@ class SingleplexWorker extends Worker { * zombie processes. Unfortunately, shutdown hooks are not guaranteed to be called, but this is * the best we can do. This must be set when a process is created. */ - private Thread shutdownHook; + protected Thread shutdownHook; SingleplexWorker(WorkerKey workerKey, int workerId, final Path workDir, Path logFile) { super(workerKey, workerId, logFile); this.workDir = workDir; } - Subprocess createProcess() throws IOException { + protected Subprocess createProcess() throws IOException { this.shutdownHook = new Thread( () -> { diff --git a/src/main/java/com/google/devtools/build/lib/worker/WorkerExecRoot.java b/src/main/java/com/google/devtools/build/lib/worker/WorkerExecRoot.java index 610ad16970415a..a3b241ba5a5090 100644 --- a/src/main/java/com/google/devtools/build/lib/worker/WorkerExecRoot.java +++ b/src/main/java/com/google/devtools/build/lib/worker/WorkerExecRoot.java @@ -23,14 +23,24 @@ import com.google.devtools.build.lib.vfs.PathFragment; import java.io.IOException; import java.util.LinkedHashSet; +import java.util.List; import java.util.Set; /** Creates and manages the contents of a working directory of a persistent worker. */ final class WorkerExecRoot { private final Path workDir; + private final List extraDirs; - public WorkerExecRoot(Path workDir) { + /** + * Creates a new WorkerExecRoot. + * + * @param workDir The directory (workspace dir) that the worker will be executing in. + * @param extraDirs Directories that must survive sandbox cleanup, e.g. for things that are + * bind-mounted. + */ + public WorkerExecRoot(Path workDir, List extraDirs) { this.workDir = workDir; + this.extraDirs = extraDirs; } public void createFileSystem( @@ -41,7 +51,7 @@ public void createFileSystem( // First compute all the inputs and directories that we need. This is based only on // `workerFiles`, `inputs` and `outputs` and won't do any I/O. Set inputsToCreate = new LinkedHashSet<>(); - LinkedHashSet dirsToCreate = new LinkedHashSet<>(); + LinkedHashSet dirsToCreate = new LinkedHashSet<>(extraDirs); SandboxHelpers.populateInputsAndDirsToCreate( ImmutableSet.of(), inputsToCreate, diff --git a/src/main/java/com/google/devtools/build/lib/worker/WorkerFactory.java b/src/main/java/com/google/devtools/build/lib/worker/WorkerFactory.java index 1ce0866762d660..22e71cb2feedb7 100644 --- a/src/main/java/com/google/devtools/build/lib/worker/WorkerFactory.java +++ b/src/main/java/com/google/devtools/build/lib/worker/WorkerFactory.java @@ -19,6 +19,7 @@ import com.google.devtools.build.lib.events.Reporter; import com.google.devtools.build.lib.vfs.Path; import com.google.devtools.build.lib.vfs.PathFragment; +import com.google.devtools.build.lib.worker.SandboxedWorker.WorkerSandboxOptions; import java.io.IOException; import java.util.Arrays; import java.util.Locale; @@ -45,9 +46,19 @@ public class WorkerFactory extends BaseKeyedPooledObjectFactory getFlagFiles() { } } } - diff --git a/src/test/java/com/google/devtools/build/lib/worker/BUILD b/src/test/java/com/google/devtools/build/lib/worker/BUILD index 97e51585e2eeca..cb10baf4f0af92 100644 --- a/src/test/java/com/google/devtools/build/lib/worker/BUILD +++ b/src/test/java/com/google/devtools/build/lib/worker/BUILD @@ -92,6 +92,7 @@ java_library( "//src/main/java/com/google/devtools/build/lib/clock", "//src/main/java/com/google/devtools/build/lib/collect/nestedset", "//src/main/java/com/google/devtools/build/lib/events", + "//src/main/java/com/google/devtools/build/lib/exec:bin_tools", "//src/main/java/com/google/devtools/build/lib/exec:spawn_runner", "//src/main/java/com/google/devtools/build/lib/exec/local", "//src/main/java/com/google/devtools/build/lib/sandbox:sandbox_helpers", @@ -111,6 +112,7 @@ java_library( "//src/main/java/com/google/devtools/common/options", "//src/main/protobuf:worker_protocol_java_proto", "//src/test/java/com/google/devtools/build/lib/actions/util", + "//src/test/java/com/google/devtools/build/lib/buildtool/util", "//src/test/java/com/google/devtools/build/lib/exec/util", "//src/test/java/com/google/devtools/build/lib/testutil", "//src/test/java/com/google/devtools/build/lib/testutil:TestUtils", diff --git a/src/test/java/com/google/devtools/build/lib/worker/TestUtils.java b/src/test/java/com/google/devtools/build/lib/worker/TestUtils.java index 3f8ca9e4e0db68..ec0c055a46bd0b 100644 --- a/src/test/java/com/google/devtools/build/lib/worker/TestUtils.java +++ b/src/test/java/com/google/devtools/build/lib/worker/TestUtils.java @@ -229,7 +229,7 @@ static class TestWorker extends SingleplexWorker { } @Override - Subprocess createProcess() { + protected Subprocess createProcess() { return fakeSubprocess; } diff --git a/src/test/java/com/google/devtools/build/lib/worker/WorkerExecRootTest.java b/src/test/java/com/google/devtools/build/lib/worker/WorkerExecRootTest.java index c473f8b90e6c7a..18678ed7b4ae00 100644 --- a/src/test/java/com/google/devtools/build/lib/worker/WorkerExecRootTest.java +++ b/src/test/java/com/google/devtools/build/lib/worker/WorkerExecRootTest.java @@ -15,6 +15,7 @@ import static com.google.common.truth.Truth.assertThat; +import com.google.common.collect.ImmutableList; import com.google.devtools.build.lib.testutil.TestUtils; import com.google.devtools.build.lib.vfs.DigestHashFunction; import com.google.devtools.build.lib.vfs.FileSystem; @@ -59,7 +60,7 @@ public void cleanFileSystem() throws Exception { .addWorkerFile("worker.sh"); Path workerSh = execRoot.getRelative("worker.sh"); - WorkerExecRoot workerExecRoot = new WorkerExecRoot(workDir); + WorkerExecRoot workerExecRoot = new WorkerExecRoot(workDir, ImmutableList.of()); workerExecRoot.createFileSystem( sandboxHelper.getWorkerFiles(), sandboxHelper.getSandboxInputs(), @@ -99,7 +100,7 @@ public void createsAndCleansInputSymlinks() throws Exception { .addSymlink("dir/input_symlink_1", "new_content") .addSymlink("dir/input_symlink_2", "unchanged"); - WorkerExecRoot workerExecRoot = new WorkerExecRoot(workDir); + WorkerExecRoot workerExecRoot = new WorkerExecRoot(workDir, ImmutableList.of()); // This should update the `input_symlink_{1,2,3}` according to `SandboxInputs`, i.e., update the // first/second (alternatively leave the second unchanged) and delete the third. @@ -126,7 +127,7 @@ public void createsOutputDirs() throws Exception { .addOutputDir("dir/foo/_kotlinc/bar_kt_jvm/bar_kt_classes") .addOutputDir("dir/foo/_kotlinc/bar_kt_jvm/bar_kt_temp") .addOutputDir("dir/foo/_kotlinc/bar_kt_jvm/bar_kt_generated_classes"); - WorkerExecRoot workerExecRoot = new WorkerExecRoot(workDir); + WorkerExecRoot workerExecRoot = new WorkerExecRoot(workDir, ImmutableList.of()); workerExecRoot.createFileSystem( sandboxHelper.getWorkerFiles(), sandboxHelper.getSandboxInputs(), @@ -156,7 +157,7 @@ public void workspaceFilesAreNotDeleted() throws Exception { .createSymlink("needed_file", neededWorkspaceFile.getPathString()) .createSymlink("other_file", otherWorkspaceFile.getPathString()); - WorkerExecRoot workerExecRoot = new WorkerExecRoot(workDir); + WorkerExecRoot workerExecRoot = new WorkerExecRoot(workDir, ImmutableList.of()); workerExecRoot.createFileSystem( sandboxHelper.getWorkerFiles(), sandboxHelper.getSandboxInputs(), @@ -184,7 +185,7 @@ public void recreatesEmptyFiles() throws Exception { .createExecRootFile("some_file", "some content") .addInputFile("some_file", null); - WorkerExecRoot workerExecRoot = new WorkerExecRoot(sandboxHelper.workDir); + WorkerExecRoot workerExecRoot = new WorkerExecRoot(sandboxHelper.workDir, ImmutableList.of()); workerExecRoot.createFileSystem( sandboxHelper.getWorkerFiles(), sandboxHelper.getSandboxInputs(), @@ -217,7 +218,7 @@ public void createsAndDeletesSiblingExternalRepoFiles() throws Exception { .addInputFile("../foo/bar/input1", input1.getPathString()) .addInputFile("../foo/input2", input2.getPathString()); - WorkerExecRoot workerExecRoot = new WorkerExecRoot(workDir); + WorkerExecRoot workerExecRoot = new WorkerExecRoot(workDir, ImmutableList.of()); workerExecRoot.createFileSystem( sandboxHelper.getWorkerFiles(), sandboxHelper.getSandboxInputs(), diff --git a/src/test/java/com/google/devtools/build/lib/worker/WorkerModuleTest.java b/src/test/java/com/google/devtools/build/lib/worker/WorkerModuleTest.java index 1e691bd7a1fc73..49c0b092a21aec 100644 --- a/src/test/java/com/google/devtools/build/lib/worker/WorkerModuleTest.java +++ b/src/test/java/com/google/devtools/build/lib/worker/WorkerModuleTest.java @@ -27,10 +27,13 @@ import com.google.devtools.build.lib.analysis.ServerDirectories; import com.google.devtools.build.lib.buildtool.BuildRequest; import com.google.devtools.build.lib.buildtool.buildevent.BuildStartingEvent; +import com.google.devtools.build.lib.buildtool.util.BuildIntegrationTestCase.RecordingExceptionHandler; import com.google.devtools.build.lib.clock.BlazeClock; import com.google.devtools.build.lib.events.Reporter; import com.google.devtools.build.lib.events.StoredEventHandler; +import com.google.devtools.build.lib.exec.BinTools; import com.google.devtools.build.lib.runtime.BlazeRuntime; +import com.google.devtools.build.lib.runtime.BlazeWorkspace; import com.google.devtools.build.lib.runtime.CommandEnvironment; import com.google.devtools.build.lib.util.AbruptExitException; import com.google.devtools.build.lib.vfs.DigestHashFunction; @@ -310,8 +313,20 @@ private void setupEnvironment(String rootDir) throws IOException, AbruptExitExce .setStartupOptionsProvider(startupOptionsProvider) .build(); when(env.getRuntime()).thenReturn(blazeRuntime); - when(env.getDirectories()) - .thenReturn(new BlazeDirectories(serverDirectories, null, null, "blaze")); + BlazeDirectories blazeDirectories = + new BlazeDirectories(serverDirectories, null, null, "blaze"); + BlazeWorkspace blazeWorkspace = + new BlazeWorkspace( + blazeRuntime, + blazeDirectories, + null, + new RecordingExceptionHandler(), + null, + BinTools.forUnitTesting(blazeDirectories, ImmutableList.of()), + null, + null); + when(env.getBlazeWorkspace()).thenReturn(blazeWorkspace); + when(env.getDirectories()).thenReturn(blazeDirectories); EventBus eventBus = new EventBus(); when(env.getEventBus()).thenReturn(eventBus); when(env.getReporter()).thenReturn(new Reporter(eventBus, storedEventHandler));