blob: ba0ca599ba13c132a472e7efc5545c55339ed72e [file] [log] [blame]
// Copyright 2014 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.runtime.commands;
import com.google.devtools.build.lib.actions.ExecException;
import com.google.devtools.build.lib.analysis.NoBuildEvent;
import com.google.devtools.build.lib.buildtool.BuildRequestOptions;
import com.google.devtools.build.lib.buildtool.OutputDirectoryLinksUtils;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.runtime.BlazeCommand;
import com.google.devtools.build.lib.runtime.BlazeCommandDispatcher.ShutdownBlazeServerException;
import com.google.devtools.build.lib.runtime.Command;
import com.google.devtools.build.lib.runtime.CommandEnvironment;
import com.google.devtools.build.lib.shell.CommandException;
import com.google.devtools.build.lib.util.CommandBuilder;
import com.google.devtools.build.lib.util.ExitCode;
import com.google.devtools.build.lib.util.OS;
import com.google.devtools.build.lib.util.ProcessUtils;
import com.google.devtools.build.lib.util.ShellEscaper;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.common.options.Option;
import com.google.devtools.common.options.OptionDocumentationCategory;
import com.google.devtools.common.options.OptionEffectTag;
import com.google.devtools.common.options.OptionsBase;
import com.google.devtools.common.options.OptionsParser;
import com.google.devtools.common.options.OptionsProvider;
import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.logging.LogManager;
import java.util.logging.Logger;
/** Implements 'blaze clean'. */
@Command(
name = "clean",
builds = true, // Does not, but people expect build options to be there
writeCommandLog = false, // Do not create a command.log, otherwise we couldn't delete it.
options = {CleanCommand.Options.class},
help = "resource:clean.txt",
shortDescription = "Removes output files and optionally stops the server.",
// TODO(bazel-team): Remove this - we inherit a huge number of unused options.
inherits = {BuildCommand.class}
)
public final class CleanCommand implements BlazeCommand {
/**
* An interface for special options for the clean command.
*/
public static class Options extends OptionsBase {
@Option(
name = "clean_style",
defaultValue = "",
category = "clean",
documentationCategory = OptionDocumentationCategory.OUTPUT_SELECTION,
effectTags = {OptionEffectTag.HOST_MACHINE_RESOURCE_OPTIMIZATIONS},
help = "Can be 'expunge', 'expunge_async', or 'async'."
)
public String cleanStyle;
@Option(
name = "expunge",
defaultValue = "null",
category = "clean",
expansion = "--clean_style=expunge",
documentationCategory = OptionDocumentationCategory.OUTPUT_SELECTION,
effectTags = {OptionEffectTag.HOST_MACHINE_RESOURCE_OPTIMIZATIONS},
help =
"If specified, clean removes the entire working tree for this %{product} "
+ "instance, which includes all %{product}-created temporary and build output "
+ "files, and stops the %{product} server if it is running."
)
public Void expunge;
@Option(
name = "expunge_async",
defaultValue = "null",
category = "clean",
expansion = "--clean_style=expunge_async",
documentationCategory = OptionDocumentationCategory.OUTPUT_SELECTION,
effectTags = {OptionEffectTag.HOST_MACHINE_RESOURCE_OPTIMIZATIONS},
help =
"If specified, clean asynchronously removes the entire working tree for "
+ "this %{product} instance, which includes all %{product}-created temporary and "
+ "build output files, and stops the %{product} server if it is running. When "
+ "this command completes, it will be safe to execute new commands in the same "
+ "client, even though the deletion may continue in the background."
)
public Void expungeAsync;
@Option(
name = "async",
defaultValue = "null",
category = "clean",
expansion = "--clean_style=async",
documentationCategory = OptionDocumentationCategory.OUTPUT_SELECTION,
effectTags = {OptionEffectTag.HOST_MACHINE_RESOURCE_OPTIMIZATIONS},
help =
"If specified, clean asynchronously removes the entire working tree for "
+ "this %{product} instance, which includes all %{product}-created temporary and "
+ "build output files. When this command completes, it will be safe to execute new "
+ "commands in the same client, even though the deletion may continue in the "
+ "background."
)
public Void async;
}
/**
* Posted on the public event stream to announce that a clean is happening.
*/
public static class CleanStartingEvent {
private final OptionsProvider optionsProvider;
public CleanStartingEvent(OptionsProvider optionsProvider) {
this.optionsProvider = optionsProvider;
}
public OptionsProvider getOptionsProvider() {
return optionsProvider;
}
}
private static final Logger logger = Logger.getLogger(CleanCommand.class.getName());
@Override
public ExitCode exec(CommandEnvironment env, OptionsProvider options)
throws ShutdownBlazeServerException {
Options cleanOptions = options.getOptions(Options.class);
boolean expungeAsync = cleanOptions.cleanStyle.equals("expunge_async");
boolean expunge = cleanOptions.cleanStyle.equals("expunge");
boolean async = cleanOptions.cleanStyle.equals("async");
env.getEventBus().post(new NoBuildEvent());
if (!expunge && !expungeAsync && !async && !cleanOptions.cleanStyle.isEmpty()) {
env.getReporter().handle(Event.error(
null, "Invalid clean_style value '" + cleanOptions.cleanStyle + "'"));
return ExitCode.COMMAND_LINE_ERROR;
}
String asyncName = (expunge || expungeAsync) ? "--expunge_async" : "--async";
// TODO(dmarting): Deactivate expunge_async on non-Linux platform until we completely fix it
// for non-Linux platforms (https://github.com/bazelbuild/bazel/issues/1906).
// MacOS and FreeBSD support setsid(2) but don't have /usr/bin/setsid, so if we wanted to
// support --expunge_async on these platforms, we'd have to write a wrapper that calls setsid(2)
// and exec(2).
if ((expungeAsync || async) && OS.getCurrent() != OS.LINUX) {
String fallbackName = expungeAsync ? "--expunge" : "synchronous clean";
env.getReporter()
.handle(
Event.info(
null /*location*/,
asyncName
+ " cannot be used on non-Linux platforms, falling back to "
+ fallbackName));
expunge = expungeAsync;
expungeAsync = false;
async = false;
cleanOptions.cleanStyle = expunge ? "expunge" : "";
}
String cleanBanner =
(expungeAsync || async)
? "Starting clean."
: "Starting clean (this may take a while). "
+ "Consider using "
+ asyncName
+ " if the clean takes more than several minutes.";
env.getEventBus().post(new CleanStartingEvent(options));
env.getReporter().handle(Event.info(null/*location*/, cleanBanner));
try {
String symlinkPrefix =
options
.getOptions(BuildRequestOptions.class)
.getSymlinkPrefix(env.getRuntime().getProductName());
actuallyClean(env, env.getOutputBase(), expunge, expungeAsync, async, symlinkPrefix);
return ExitCode.SUCCESS;
} catch (IOException e) {
env.getReporter().handle(Event.error(e.getMessage()));
return ExitCode.LOCAL_ENVIRONMENTAL_ERROR;
} catch (CommandException | ExecException e) {
env.getReporter().handle(Event.error(e.getMessage()));
return ExitCode.RUN_FAILURE;
} catch (InterruptedException e) {
env.getReporter().handle(Event.error("clean interrupted"));
return ExitCode.INTERRUPTED;
}
}
private static void asyncClean(CommandEnvironment env, Path path, String pathItemName)
throws IOException, CommandException {
String tempBaseName = path.getBaseName() + "_tmp_" + ProcessUtils.getpid();
// Keeping tempOutputBase in the same directory ensures it remains in the
// same file system, and therefore the mv will be atomic and fast.
Path tempPath = path.getParentDirectory().getChild(tempBaseName);
path.renameTo(tempPath);
env.getReporter()
.handle(Event.info(null, pathItemName + " moved to " + tempPath + " for deletion"));
// Daemonize the shell and use the double-fork idiom to ensure that the shell
// exits even while the "rm -rf" command continues.
String command =
String.format(
"exec >&- 2>&- <&- && (/usr/bin/setsid /bin/rm -rf %s &)&",
ShellEscaper.escapeString(tempPath.getPathString()));
logger.info("Executing shell commmand " + ShellEscaper.escapeString(command));
// Doesn't throw iff command exited and was successful.
new CommandBuilder()
.addArg(command)
.useShell(true)
.setWorkingDir(tempPath.getParentDirectory())
.build()
.execute();
}
private void actuallyClean(
CommandEnvironment env,
Path outputBase,
boolean expunge,
boolean expungeAsync,
boolean async,
String symlinkPrefix)
throws IOException, ShutdownBlazeServerException, CommandException, ExecException,
InterruptedException {
String workspaceDirectory = env.getWorkspace().getBaseName();
if (env.getOutputService() != null) {
env.getOutputService().clean();
}
env.getBlazeWorkspace().clearCaches();
if (expunge) {
logger.info("Expunging...");
env.getRuntime().prepareForAbruptShutdown();
// Close java.log.
LogManager.getLogManager().reset();
// Close the default stdout/stderr.
if (FileDescriptor.out.valid()) {
new FileOutputStream(FileDescriptor.out).close();
}
if (FileDescriptor.err.valid()) {
new FileOutputStream(FileDescriptor.err).close();
}
// Close the redirected stdout/stderr.
System.out.close();
System.err.close();
// Delete the big subdirectories with the important content first--this
// will take the most time. Then quickly delete the little locks, logs
// and links right before we exit. Once the lock file is gone there will
// be a small possibility of a server race if a client is waiting, but
// all significant files will be gone by then.
FileSystemUtils.deleteTreesBelow(outputBase);
FileSystemUtils.deleteTree(outputBase);
} else if (expungeAsync) {
logger.info("Expunging asynchronously...");
env.getRuntime().prepareForAbruptShutdown();
asyncClean(env, outputBase, "Output base");
} else {
logger.info("Output cleaning...");
env.getBlazeWorkspace().resetEvaluator();
Path execroot = outputBase.getRelative("execroot");
if (execroot.exists()) {
logger.finest("Cleaning " + execroot + (async ? " asynchronously..." : ""));
if (async) {
asyncClean(env, execroot, "Output tree");
} else {
FileSystemUtils.deleteTreesBelow(execroot);
}
}
}
// remove convenience links
OutputDirectoryLinksUtils.removeOutputDirectoryLinks(
workspaceDirectory, env.getWorkspace(), env.getReporter(),
symlinkPrefix, env.getRuntime().getProductName());
// shutdown on expunge cleans
if (expunge || expungeAsync) {
throw new ShutdownBlazeServerException(0);
}
System.gc();
}
@Override
public void editOptions(OptionsParser optionsParser) {}
}