diff --git a/src/main/java/com/google/devtools/build/lib/remote/ApiVersion.java b/src/main/java/com/google/devtools/build/lib/remote/ApiVersion.java index ee4ed593a81827..d49980f2feb8c5 100644 --- a/src/main/java/com/google/devtools/build/lib/remote/ApiVersion.java +++ b/src/main/java/com/google/devtools/build/lib/remote/ApiVersion.java @@ -23,12 +23,21 @@ public class ApiVersion implements Comparable { public final int patch; public final String prerelease; + // The version of the Remote Execution API that Bazel supports initially. + public static final ApiVersion twoPointZero = + new ApiVersion(SemVer.newBuilder().setMajor(2).setMinor(0).build()); + // The version of the Remote Execution API that starts supporting the + // Command.output_paths and ActionResult.output_symlinks fields. + public static final ApiVersion twoPointOne = + new ApiVersion(SemVer.newBuilder().setMajor(2).setMinor(1).build()); + // The latest version of the Remote Execution API that Bazel is compatible with. + public static final ApiVersion twoPointTwo = + new ApiVersion(SemVer.newBuilder().setMajor(2).setMinor(2).build()); + // The current lowest/highest versions (inclusive) of the Remote Execution API that Bazel // supports. These fields will need to be updated together with all version changes. - public static final ApiVersion low = - new ApiVersion(SemVer.newBuilder().setMajor(2).setMinor(0).build()); - public static final ApiVersion high = - new ApiVersion(SemVer.newBuilder().setMajor(2).setMinor(0).build()); + public static final ApiVersion low = twoPointZero; + public static final ApiVersion high = twoPointTwo; public ApiVersion(int major, int minor, int patch, String prerelease) { this.major = major; diff --git a/src/main/java/com/google/devtools/build/lib/remote/RemoteExecutionService.java b/src/main/java/com/google/devtools/build/lib/remote/RemoteExecutionService.java index 8f7eeac7790b9b..fbe1c4e717082f 100644 --- a/src/main/java/com/google/devtools/build/lib/remote/RemoteExecutionService.java +++ b/src/main/java/com/google/devtools/build/lib/remote/RemoteExecutionService.java @@ -47,6 +47,7 @@ import build.bazel.remote.execution.v2.OutputSymlink; import build.bazel.remote.execution.v2.Platform; import build.bazel.remote.execution.v2.RequestMetadata; +import build.bazel.remote.execution.v2.ServerCapabilities; import build.bazel.remote.execution.v2.SymlinkNode; import build.bazel.remote.execution.v2.Tree; import com.github.benmanes.caffeine.cache.Cache; @@ -239,6 +240,7 @@ public RemoteExecutionService( } private Command buildCommand( + boolean useOutputPaths, Collection outputs, List arguments, ImmutableMap env, @@ -246,24 +248,30 @@ private Command buildCommand( RemotePathResolver remotePathResolver, @Nullable SpawnScrubber spawnScrubber) { Command.Builder command = Command.newBuilder(); - ArrayList outputFiles = new ArrayList<>(); - ArrayList outputDirectories = new ArrayList<>(); - ArrayList outputPaths = new ArrayList<>(); - for (ActionInput output : outputs) { - String pathString = decodeBytestringUtf8(remotePathResolver.localPathToOutputPath(output)); - if (output.isDirectory()) { - outputDirectories.add(pathString); - } else { - outputFiles.add(pathString); + if (useOutputPaths) { + var outputPaths = new ArrayList(); + for (ActionInput output : outputs) { + String pathString = decodeBytestringUtf8(remotePathResolver.localPathToOutputPath(output)); + outputPaths.add(pathString); } - outputPaths.add(pathString); + Collections.sort(outputPaths); + command.addAllOutputPaths(outputPaths); + } else { + var outputFiles = new ArrayList(); + var outputDirectories = new ArrayList(); + for (ActionInput output : outputs) { + String pathString = decodeBytestringUtf8(remotePathResolver.localPathToOutputPath(output)); + if (output.isDirectory()) { + outputDirectories.add(pathString); + } else { + outputFiles.add(pathString); + } + } + Collections.sort(outputFiles); + Collections.sort(outputDirectories); + command.addAllOutputFiles(outputFiles); + command.addAllOutputDirectories(outputDirectories); } - Collections.sort(outputFiles); - Collections.sort(outputDirectories); - Collections.sort(outputPaths); - command.addAllOutputFiles(outputFiles); - command.addAllOutputDirectories(outputDirectories); - command.addAllOutputPaths(outputPaths); if (platform != null) { command.setPlatform(platform); @@ -590,8 +598,23 @@ public RemoteAction buildRemoteAction(Spawn spawn, SpawnExecutionContext context platform = PlatformUtils.getPlatformProto(spawn, remoteOptions); } + var readCachePolicy = getReadCachePolicy(spawn); + var writeCachePolicy = getWriteCachePolicy(spawn); + ServerCapabilities capabilities = + mayBeExecutedRemotely(spawn) + ? remoteExecutor.getServerCapabilities() + : writeCachePolicy.allowRemoteCache() ? remoteCache.getServerCapabilities : null; + var useOutputPaths = true; + if (capabilities != null) { + var supportStatus = ClientApiVersion.current.checkServerSupportedVersions(capabilities); + if (supportStatus.isSupported()) { + useOutputPaths = + supportStatus.getHighestSupportedVersion().compareTo(ApiVersion.twoPointOne) >= 0; + } + } Command command = buildCommand( + useOutputPaths, spawn.getOutputFiles(), spawn.getArguments(), spawn.getEnvironment(), @@ -615,7 +638,7 @@ public RemoteAction buildRemoteAction(Spawn spawn, SpawnExecutionContext context buildRequestId, commandId, actionKey.getDigest().getHash(), spawn.getResourceOwner()); RemoteActionExecutionContext remoteActionExecutionContext = RemoteActionExecutionContext.create( - spawn, context, metadata, getWriteCachePolicy(spawn), getReadCachePolicy(spawn)); + spawn, context, metadata, writeCachePolicy, readCachePolicy); return new RemoteAction( spawn, diff --git a/src/test/java/com/google/devtools/build/lib/remote/RemoteExecutionServiceTest.java b/src/test/java/com/google/devtools/build/lib/remote/RemoteExecutionServiceTest.java index 34ef1aa0010517..a49e04c59fbeab 100644 --- a/src/test/java/com/google/devtools/build/lib/remote/RemoteExecutionServiceTest.java +++ b/src/test/java/com/google/devtools/build/lib/remote/RemoteExecutionServiceTest.java @@ -38,6 +38,7 @@ import build.bazel.remote.execution.v2.Digest; import build.bazel.remote.execution.v2.Directory; import build.bazel.remote.execution.v2.DirectoryNode; +import build.bazel.remote.execution.v2.ExecutionCapabilities; import build.bazel.remote.execution.v2.FileNode; import build.bazel.remote.execution.v2.NodeProperties; import build.bazel.remote.execution.v2.NodeProperty; @@ -46,9 +47,11 @@ import build.bazel.remote.execution.v2.OutputSymlink; import build.bazel.remote.execution.v2.Platform; import build.bazel.remote.execution.v2.RequestMetadata; +import build.bazel.remote.execution.v2.ServerCapabilities; import build.bazel.remote.execution.v2.SymlinkAbsolutePathStrategy; import build.bazel.remote.execution.v2.SymlinkNode; import build.bazel.remote.execution.v2.Tree; +import build.bazel.semver.SemVer; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableClassToInstanceMap; import com.google.common.collect.ImmutableList; @@ -153,6 +156,23 @@ public class RemoteExecutionServiceTest { private final Reporter reporter = new Reporter(new EventBus()); private final StoredEventHandler eventHandler = new StoredEventHandler(); + // In the past, Bazel only supports RemoteApi version 2.0. + // Use this to ensure we are backward compatible with Servers that only support 2.0. + private final ApiVersion oldApiVersion = new ApiVersion(SemVer.newBuilder().setMajor(2).build()); + private final ServerCapabilities legacyRemoteExecutorCapabilities = + ServerCapabilities.newBuilder() + .setLowApiVersion(oldApiVersion.toSemVer()) + .setHighApiVersion(oldApiVersion.toSemVer()) + .setExecutionCapabilities(ExecutionCapabilities.newBuilder().setExecEnabled(true).build()) + .build(); + + private final ServerCapabilities remoteExecutorCapabilities = + ServerCapabilities.newBuilder() + .setLowApiVersion(ApiVersion.low.toSemVer()) + .setHighApiVersion(ApiVersion.high.toSemVer()) + .setExecutionCapabilities(ExecutionCapabilities.newBuilder().setExecEnabled(true).build()) + .build(); + RemoteOptions remoteOptions; private FileSystem fs; private Path execRoot; @@ -197,6 +217,7 @@ public final void setUp() throws Exception { cache = spy(new InMemoryRemoteCache(spy(new InMemoryCacheClient()), remoteOptions, digestUtil)); executor = mock(RemoteExecutionClient.class); + when(executor.getServerCapabilities()).thenReturn(remoteExecutorCapabilities); RequestMetadata metadata = TracingMetadataUtils.buildMetadata("none", "none", "action-id", null); @@ -215,9 +236,27 @@ public void buildRemoteAction_withRegularFileAsOutput() throws Exception { RemoteAction remoteAction = service.buildRemoteAction(spawn, context); - assertThat(remoteAction.getCommand().getOutputFilesList()).containsExactly(execPath.toString()); + assertThat(remoteAction.getCommand().getOutputFilesList()).isEmpty(); + assertThat(remoteAction.getCommand().getOutputDirectoriesList()).isEmpty(); assertThat(remoteAction.getCommand().getOutputPathsList()).containsExactly(execPath.toString()); + } + + @Test + public void legacy_buildRemoteAction_withRegularFileAsOutput() throws Exception { + when(executor.getServerCapabilities()).thenReturn(legacyRemoteExecutorCapabilities); + PathFragment execPath = execRoot.getRelative("path/to/tree").asFragment(); + Spawn spawn = + new SpawnBuilder("dummy") + .withOutput(ActionsTestUtil.createArtifactWithExecPath(artifactRoot, execPath)) + .build(); + FakeSpawnExecutionContext context = newSpawnExecutionContext(spawn); + RemoteExecutionService service = newRemoteExecutionService(); + + RemoteAction remoteAction = service.buildRemoteAction(spawn, context); + + assertThat(remoteAction.getCommand().getOutputFilesList()).containsExactly(execPath.toString()); assertThat(remoteAction.getCommand().getOutputDirectoriesList()).isEmpty(); + assertThat(remoteAction.getCommand().getOutputPathsList()).isEmpty(); } @Test @@ -233,8 +272,28 @@ public void buildRemoteAction_withTreeArtifactAsOutput() throws Exception { RemoteAction remoteAction = service.buildRemoteAction(spawn, context); + assertThat(remoteAction.getCommand().getOutputFilesList()).isEmpty(); + assertThat(remoteAction.getCommand().getOutputDirectoriesList()).isEmpty(); + assertThat(remoteAction.getCommand().getOutputPathsList()).containsExactly("path/to/dir"); + } + + @Test + public void legacy_buildRemoteAction_withTreeArtifactAsOutput() throws Exception { + when(executor.getServerCapabilities()).thenReturn(legacyRemoteExecutorCapabilities); + Spawn spawn = + new SpawnBuilder("dummy") + .withOutput( + ActionsTestUtil.createTreeArtifactWithGeneratingAction( + artifactRoot, PathFragment.create("path/to/dir"))) + .build(); + FakeSpawnExecutionContext context = newSpawnExecutionContext(spawn); + RemoteExecutionService service = newRemoteExecutionService(); + + RemoteAction remoteAction = service.buildRemoteAction(spawn, context); + assertThat(remoteAction.getCommand().getOutputFilesList()).isEmpty(); assertThat(remoteAction.getCommand().getOutputDirectoriesList()).containsExactly("path/to/dir"); + assertThat(remoteAction.getCommand().getOutputPathsList()).isEmpty(); } @Test @@ -250,11 +309,30 @@ public void buildRemoteAction_withUnresolvedSymlinkAsOutput() throws Exception { RemoteAction remoteAction = service.buildRemoteAction(spawn, context); - assertThat(remoteAction.getCommand().getOutputFilesList()).containsExactly("path/to/link"); + assertThat(remoteAction.getCommand().getOutputFilesList()).isEmpty(); assertThat(remoteAction.getCommand().getOutputDirectoriesList()).isEmpty(); assertThat(remoteAction.getCommand().getOutputPathsList()).containsExactly("path/to/link"); } + @Test + public void legacy_buildRemoteAction_withUnresolvedSymlinkAsOutput() throws Exception { + when(executor.getServerCapabilities()).thenReturn(legacyRemoteExecutorCapabilities); + Spawn spawn = + new SpawnBuilder("dummy") + .withOutput( + ActionsTestUtil.createUnresolvedSymlinkArtifactWithExecPath( + artifactRoot, PathFragment.create("path/to/link"))) + .build(); + FakeSpawnExecutionContext context = newSpawnExecutionContext(spawn); + RemoteExecutionService service = newRemoteExecutionService(); + + RemoteAction remoteAction = service.buildRemoteAction(spawn, context); + + assertThat(remoteAction.getCommand().getOutputFilesList()).containsExactly("path/to/link"); + assertThat(remoteAction.getCommand().getOutputDirectoriesList()).isEmpty(); + assertThat(remoteAction.getCommand().getOutputPathsList()).isEmpty(); + } + @Test public void buildRemoteAction_withActionInputAsOutput() throws Exception { Spawn spawn = @@ -266,8 +344,42 @@ public void buildRemoteAction_withActionInputAsOutput() throws Exception { RemoteAction remoteAction = service.buildRemoteAction(spawn, context); + assertThat(remoteAction.getCommand().getOutputFilesList()).isEmpty(); + assertThat(remoteAction.getCommand().getOutputDirectoriesList()).isEmpty(); + assertThat(remoteAction.getCommand().getOutputPathsList()).containsExactly("path/to/file"); + } + + @Test + public void legacy_buildRemoteAction_withActionInputFileAsOutput() throws Exception { + when(executor.getServerCapabilities()).thenReturn(legacyRemoteExecutorCapabilities); + Spawn spawn = + new SpawnBuilder("dummy") + .withOutput(ActionInputHelper.fromPath(PathFragment.create("path/to/file"))) + .build(); + FakeSpawnExecutionContext context = newSpawnExecutionContext(spawn); + RemoteExecutionService service = newRemoteExecutionService(); + + RemoteAction remoteAction = service.buildRemoteAction(spawn, context); + assertThat(remoteAction.getCommand().getOutputFilesList()).containsExactly("path/to/file"); assertThat(remoteAction.getCommand().getOutputDirectoriesList()).isEmpty(); + assertThat(remoteAction.getCommand().getOutputPathsList()).isEmpty(); + } + + @Test + public void buildRemoteAction_withActionInputDirectoryAsOutput() throws Exception { + Spawn spawn = + new SpawnBuilder("dummy") + .withOutput(ActionInputHelper.fromPath(PathFragment.create("path/to/dir"))) + .build(); + FakeSpawnExecutionContext context = newSpawnExecutionContext(spawn); + RemoteExecutionService service = newRemoteExecutionService(); + + RemoteAction remoteAction = service.buildRemoteAction(spawn, context); + + assertThat(remoteAction.getCommand().getOutputFilesList()).isEmpty(); + assertThat(remoteAction.getCommand().getOutputDirectoriesList()).isEmpty(); + assertThat(remoteAction.getCommand().getOutputPathsList()).containsExactly("path/to/dir"); } @Test @@ -2373,10 +2485,8 @@ public void buildRemoteActionWithPathMapping(@TestParameter boolean remoteMerkle .containsExactly( PathFragment.create("outputs/bin/input1"), mappedInput, PathFragment.create("outputs/bin/input2"), unmappedInput); - assertThat(remoteAction.getCommand().getOutputFilesList()) - .containsExactly("outputs/bin/dir/output1", "outputs/bin/other_dir/output2"); - assertThat(remoteAction.getCommand().getOutputDirectoriesList()) - .containsExactly("outputs/bin/output_dir"); + assertThat(remoteAction.getCommand().getOutputFilesList()).isEmpty(); + assertThat(remoteAction.getCommand().getOutputDirectoriesList()).isEmpty(); assertThat(remoteAction.getCommand().getOutputPathsList()) .containsExactly( "outputs/bin/dir/output1", "outputs/bin/other_dir/output2", "outputs/bin/output_dir"); diff --git a/src/test/java/com/google/devtools/build/lib/remote/RemoteSpawnRunnerWithGrpcRemoteExecutorTest.java b/src/test/java/com/google/devtools/build/lib/remote/RemoteSpawnRunnerWithGrpcRemoteExecutorTest.java index 1b02b2e5240781..fa99e7a84847b0 100644 --- a/src/test/java/com/google/devtools/build/lib/remote/RemoteSpawnRunnerWithGrpcRemoteExecutorTest.java +++ b/src/test/java/com/google/devtools/build/lib/remote/RemoteSpawnRunnerWithGrpcRemoteExecutorTest.java @@ -311,6 +311,8 @@ public Single create() { .build(); ServerCapabilities caps = ServerCapabilities.newBuilder() + .setLowApiVersion(ApiVersion.low.toSemVer()) + .setHighApiVersion(ApiVersion.high.toSemVer()) .setExecutionCapabilities( ExecutionCapabilities.newBuilder().setExecEnabled(true).build()) .build(); @@ -371,7 +373,6 @@ public int maxConcurrency() { .setName("VARIABLE") .setValue("value") .build()) - .addAllOutputFiles(ImmutableList.of("bar", "foo")) .addAllOutputPaths(ImmutableList.of("bar", "foo")) .build(); cmdDigest = DIGEST_UTIL.compute(command); diff --git a/src/test/shell/bazel/remote/remote_execution_test.sh b/src/test/shell/bazel/remote/remote_execution_test.sh index 1b56f814c21c60..23d68ab785759e 100755 --- a/src/test/shell/bazel/remote/remote_execution_test.sh +++ b/src/test/shell/bazel/remote/remote_execution_test.sh @@ -178,6 +178,46 @@ function test_credential_helper_clear_cache() { expect_credential_helper_calls 10 } +function test_remote_grpc_cache_with_legacy_api() { + # Test if Bazel works with Remote Cache using Remote Api version 2.0. + stop_worker + start_worker --legacy_api + + mkdir -p a + cat > a/BUILD < a/BUILD < responseObserver) { DigestFunction.Value df = digestUtil.getDigestFunction(); + + var builder = ServerCapabilities.newBuilder(); + if (workerOptions.legacyApi) { + builder + .setLowApiVersion(ApiVersion.twoPointZero.toSemVer()) + .setHighApiVersion(ApiVersion.twoPointZero.toSemVer()); + } else { + builder + .setLowApiVersion(ApiVersion.low.toSemVer()) + .setHighApiVersion(ApiVersion.high.toSemVer()); + } ServerCapabilities.Builder response = - ServerCapabilities.newBuilder() - .setLowApiVersion(ApiVersion.low.toSemVer()) - .setHighApiVersion(ApiVersion.high.toSemVer()) - .setCacheCapabilities( - CacheCapabilities.newBuilder() - .addDigestFunctions(df) - .setSymlinkAbsolutePathStrategy(SymlinkAbsolutePathStrategy.Value.DISALLOWED) - .setActionCacheUpdateCapabilities( - ActionCacheUpdateCapabilities.newBuilder().setUpdateEnabled(true).build()) - .setMaxBatchTotalSizeBytes(CasServer.MAX_BATCH_SIZE_BYTES) - .build()); + builder.setCacheCapabilities( + CacheCapabilities.newBuilder() + .addDigestFunctions(df) + .setSymlinkAbsolutePathStrategy(SymlinkAbsolutePathStrategy.Value.DISALLOWED) + .setActionCacheUpdateCapabilities( + ActionCacheUpdateCapabilities.newBuilder().setUpdateEnabled(true).build()) + .setMaxBatchTotalSizeBytes(CasServer.MAX_BATCH_SIZE_BYTES) + .build()); if (execEnabled) { response.setExecutionCapabilities( ExecutionCapabilities.newBuilder().setDigestFunction(df).setExecEnabled(true).build()); diff --git a/src/tools/remote/src/main/java/com/google/devtools/build/remote/worker/ExecutionServer.java b/src/tools/remote/src/main/java/com/google/devtools/build/remote/worker/ExecutionServer.java index cac99e3adc05f6..56900e76df4377 100644 --- a/src/tools/remote/src/main/java/com/google/devtools/build/remote/worker/ExecutionServer.java +++ b/src/tools/remote/src/main/java/com/google/devtools/build/remote/worker/ExecutionServer.java @@ -284,27 +284,44 @@ private ActionResult execute( Path workingDirectory = execRoot.getRelative(command.getWorkingDirectory()); workingDirectory.createDirectoryAndParents(); - List outputs = - new ArrayList<>(command.getOutputDirectoriesCount() + command.getOutputFilesCount()); - - for (String output : command.getOutputFilesList()) { - Path file = workingDirectory.getRelative(output); - if (file.exists()) { - throw new FileAlreadyExistsException("Output file already exists: " + file); + List outputs; + if (workerOptions.legacyApi) { + outputs = + new ArrayList<>(command.getOutputDirectoriesCount() + command.getOutputFilesCount()); + for (String output : command.getOutputFilesList()) { + var file = workingDirectory.getRelative(output); + if (file.exists()) { + throw new FileAlreadyExistsException("Output file already exists: " + file); + } + file.getParentDirectory().createDirectoryAndParents(); + outputs.add(file); } - file.getParentDirectory().createDirectoryAndParents(); - outputs.add(file); - } - for (String output : command.getOutputDirectoriesList()) { - Path file = workingDirectory.getRelative(output); - if (file.exists()) { - if (!file.isDirectory()) { - throw new FileAlreadyExistsException( - "Non-directory exists at output directory path: " + file); + for (String output : command.getOutputDirectoriesList()) { + Path file = workingDirectory.getRelative(output); + if (file.exists()) { + if (!file.isDirectory()) { + throw new FileAlreadyExistsException( + "Non-directory exists at output directory path: " + file); + } + } + file.getParentDirectory().createDirectoryAndParents(); + outputs.add(file); + } + } else { + outputs = new ArrayList<>(command.getOutputPathsCount()); + for (String output : command.getOutputPathsList()) { + var file = workingDirectory.getRelative(output); + // Since https://github.com/bazelbuild/bazel/pull/15818, + // Bazel includes all expected output directories as part of Action's inputs. + // + // Ensure no output file exists before execution happen. + // Ignore if output directories pre-exist. + if (file.exists() && !file.isDirectory()) { + throw new FileAlreadyExistsException("Output file already exists: " + file); } + file.getParentDirectory().createDirectoryAndParents(); + outputs.add(file); } - file.getParentDirectory().createDirectoryAndParents(); - outputs.add(file); } // TODO(ulfjack): This is basically a copy of LocalSpawnRunner. Ideally, we'd use that @@ -432,8 +449,8 @@ private static long getUid() throws InterruptedException { com.google.devtools.build.lib.shell.Command cmd = new com.google.devtools.build.lib.shell.Command( new String[] {"id", "-u"}, - /*environmentVariables=*/ null, - /*workingDirectory=*/ null, + /* environmentVariables= */ null, + /* workingDirectory= */ null, uidTimeout); try { ByteArrayOutputStream stdout = new ByteArrayOutputStream(); diff --git a/src/tools/remote/src/main/java/com/google/devtools/build/remote/worker/RemoteWorker.java b/src/tools/remote/src/main/java/com/google/devtools/build/remote/worker/RemoteWorker.java index 7336879f246fe0..b1ea7725953e78 100644 --- a/src/tools/remote/src/main/java/com/google/devtools/build/remote/worker/RemoteWorker.java +++ b/src/tools/remote/src/main/java/com/google/devtools/build/remote/worker/RemoteWorker.java @@ -188,7 +188,7 @@ public RemoteWorker( } else { execServer = null; } - this.capabilitiesServer = new CapabilitiesServer(digestUtil, execServer != null); + this.capabilitiesServer = new CapabilitiesServer(digestUtil, execServer != null, workerOptions); } public Server startServer() throws IOException { diff --git a/src/tools/remote/src/main/java/com/google/devtools/build/remote/worker/RemoteWorkerOptions.java b/src/tools/remote/src/main/java/com/google/devtools/build/remote/worker/RemoteWorkerOptions.java index e1bc7cb490607f..55f0a50ecc3a0a 100644 --- a/src/tools/remote/src/main/java/com/google/devtools/build/remote/worker/RemoteWorkerOptions.java +++ b/src/tools/remote/src/main/java/com/google/devtools/build/remote/worker/RemoteWorkerOptions.java @@ -67,6 +67,15 @@ public class RemoteWorkerOptions extends OptionsBase { + "work directory will be preserved in the case of failure.") public boolean debug; + @Option( + name = "legacy_api", + defaultValue = "false", + category = "build_worker", + documentationCategory = OptionDocumentationCategory.UNCATEGORIZED, + effectTags = {OptionEffectTag.UNKNOWN}, + help = "Restrict worker to RemoteApi version 2.0 capabilities") + public boolean legacyApi; + @Option( name = "pid_file", defaultValue = "null",