blob: 37446889ab48679551ca8baeefea6ace0ecbaf62 [file] [log] [blame]
/*
* Copyright 2012-present Facebook, Inc.
*
* 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.facebook.buck.cli;
import com.facebook.buck.event.BuckEventBus;
import com.facebook.buck.event.BuckEventListener;
import com.facebook.buck.event.listener.ChromeTraceBuildListener;
import com.facebook.buck.event.listener.JavaUtilsLoggingBuildListener;
import com.facebook.buck.event.listener.SimpleConsoleEventBusListener;
import com.facebook.buck.event.listener.SuperConsoleEventBusListener;
import com.facebook.buck.httpserver.WebServer;
import com.facebook.buck.parser.Parser;
import com.facebook.buck.rules.ArtifactCache;
import com.facebook.buck.rules.ArtifactCacheConnectEvent;
import com.facebook.buck.rules.BuildRule;
import com.facebook.buck.rules.KnownBuildRuleTypes;
import com.facebook.buck.rules.LoggingArtifactCacheDecorator;
import com.facebook.buck.rules.NoopArtifactCache;
import com.facebook.buck.rules.RuleKey;
import com.facebook.buck.rules.RuleKey.Builder;
import com.facebook.buck.rules.RuleKeyBuilderFactory;
import com.facebook.buck.timing.Clock;
import com.facebook.buck.timing.DefaultClock;
import com.facebook.buck.util.Ansi;
import com.facebook.buck.util.Console;
import com.facebook.buck.util.DefaultFileHashCache;
import com.facebook.buck.util.FileHashCache;
import com.facebook.buck.util.HumanReadableException;
import com.facebook.buck.util.MoreStrings;
import com.facebook.buck.util.ProjectFilesystem;
import com.facebook.buck.util.ProjectFilesystemWatcher;
import com.facebook.buck.util.Verbosity;
import com.facebook.buck.util.environment.DefaultExecutionEnvironment;
import com.facebook.buck.util.environment.ExecutionEnvironment;
import com.facebook.buck.util.environment.Platform;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.eventbus.EventBus;
import com.google.common.reflect.ClassPath;
import com.martiansoftware.nailgun.NGClientListener;
import com.martiansoftware.nailgun.NGContext;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.FileSystems;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
public final class Main {
/**
* Trying again won't help.
*/
public static final int FAIL_EXIT_CODE = 1;
/**
* Trying again later might work.
*/
public static final int BUSY_EXIT_CODE = 2;
/**
* Client disconnected.
*/
public static final int CLIENT_DISCONNECT_EXIT_CODE = 3;
private static final String DEFAULT_BUCK_CONFIG_FILE_NAME = ".buckconfig";
private static final String DEFAULT_BUCK_CONFIG_OVERRIDE_FILE_NAME = ".buckconfig.local";
private static final String BUCK_VERSION_UID_KEY = "buck.version_uid";
private static final String BUCK_VERSION_UID = System.getProperty(BUCK_VERSION_UID_KEY, "N/A");
private final PrintStream stdOut;
private final PrintStream stdErr;
private static final Semaphore commandSemaphore = new Semaphore(1);
private final Platform platform;
/**
* Daemon used to monitor the file system and cache build rules between Main() method
* invocations is static so that it can outlive Main() objects and survive for the lifetime
* of the potentially long running Buck process.
*/
private final class Daemon implements Closeable {
private final Parser parser;
private final EventBus fileEventBus;
private final ProjectFilesystemWatcher filesystemWatcher;
private final BuckConfig config;
private final Optional<WebServer> webServer;
private final Console console;
public Daemon(ProjectFilesystem projectFilesystem,
BuckConfig config,
Console console) throws IOException {
this.config = Preconditions.checkNotNull(config);
this.console = Preconditions.checkNotNull(console);
DefaultFileHashCache hashCache = new DefaultFileHashCache(projectFilesystem, console);
this.parser = new Parser(projectFilesystem,
new KnownBuildRuleTypes(),
console,
config.getPythonInterpreter(),
config.getTempFilePatterns(),
createRuleKeyBuilderFactory(config, hashCache));
this.fileEventBus = new EventBus("file-change-events");
this.filesystemWatcher = new ProjectFilesystemWatcher(
projectFilesystem,
fileEventBus,
config.getIgnorePaths(),
FileSystems.getDefault().newWatchService());
fileEventBus.register(parser);
fileEventBus.register(hashCache);
webServer = createWebServer(config, console);
}
private Optional<WebServer> createWebServer(BuckConfig config, Console console) {
// Enable the web httpserver if it is given by command line parameter or specified in
// .buckconfig. The presence of a port number is sufficient.
Optional<String> serverPort = Optional.fromNullable(System.getProperty("buck.httpserver.port"));
if (!serverPort.isPresent()) {
serverPort = config.getValue("httpserver", "port");
}
Optional<WebServer> webServer;
if (serverPort.isPresent()) {
String rawPort = serverPort.get();
try {
int port = Integer.parseInt(rawPort, 10);
webServer = Optional.of(new WebServer(port));
} catch (NumberFormatException e) {
console.printErrorText(String.format("Could not parse port for httpserver: %s.", rawPort));
webServer = Optional.absent();
}
} else {
webServer = Optional.absent();
}
return webServer;
}
public Optional<WebServer> getWebServer() {
return webServer;
}
private Parser getParser() {
return parser;
}
private void watchClient(NGContext context) {
context.addClientListener(new NGClientListener() {
@Override
public void clientDisconnected() {
// Synchronize on parser object so that the main command processing thread is not
// interrupted mid way through a Parser cache update by the Thread.interrupt() call
// triggered by System.exit(). The Parser cache will be reused by subsequent commands
// so needs to be left in a consistent state even if the current command is interrupted
// due to a client disconnection.
synchronized (parser) {
System.exit(CLIENT_DISCONNECT_EXIT_CODE);
}
}
});
}
private void watchFileSystem() throws IOException {
// Synchronize on parser object so that all outstanding watch events are processed
// as a single, atomic Parser cache update and are not interleaved with Parser cache
// invalidations triggered by requests to parse build files or interrupted by client
// disconnections.
synchronized (parser) {
filesystemWatcher.postEvents();
}
}
/** @return true if the web server was started successfully. */
private boolean initWebServer() {
if (webServer.isPresent()) {
try {
webServer.get().start();
return true;
} catch (WebServer.WebServerException e) {
e.printStackTrace(console.getStdErr());
}
}
return false;
}
public BuckConfig getConfig() {
return config;
}
@Override
public void close() throws IOException {
filesystemWatcher.close();
shutdownWebServer();
}
private void shutdownWebServer() {
if (webServer.isPresent()) {
try {
webServer.get().stop();
} catch (WebServer.WebServerException e) {
e.printStackTrace(console.getStdErr());
}
}
}
}
@Nullable private static Daemon daemon;
/**
* Get existing Daemon.
*/
private Daemon getDaemon() {
return Preconditions.checkNotNull(daemon);
}
/**
* Get or create Daemon.
*/
private Daemon getDaemon(ProjectFilesystem filesystem,
BuckConfig config,
Console console) throws IOException {
if (daemon == null) {
daemon = new Daemon(filesystem, config, console);
} else {
// Buck daemons cache build files within a single project root, changing to a different
// project root is not supported and will likely result in incorrect builds. The buck and
// buckd scripts attempt to enforce this, so a change in project root is an error that
// should be reported rather than silently worked around by invalidating the cache and
// creating a new daemon object.
File parserRoot = daemon.getParser().getProjectRoot();
if (!filesystem.getProjectRoot().equals(parserRoot)) {
throw new HumanReadableException(String.format("Unsupported root path change from %s to %s",
filesystem.getProjectRoot(), parserRoot));
}
// If Buck config has changed, invalidate the cache and create a new daemon.
if (!daemon.getConfig().equals(config)) {
daemon.close();
daemon = new Daemon(filesystem, config, console);
}
}
return daemon;
}
@VisibleForTesting
public Main(PrintStream stdOut, PrintStream stdErr) {
this.stdOut = Preconditions.checkNotNull(stdOut);
this.stdErr = Preconditions.checkNotNull(stdErr);
this.platform = Platform.detect();
}
/** Prints the usage message to standard error. */
@VisibleForTesting
int usage() {
stdErr.println("buck build tool");
stdErr.println("usage:");
stdErr.println(" buck [options]");
stdErr.println(" buck command --help");
stdErr.println(" buck command [command-options]");
stdErr.println("available commands:");
int lengthOfLongestCommand = 0;
for (Command command : Command.values()) {
String name = command.name();
if (name.length() > lengthOfLongestCommand) {
lengthOfLongestCommand = name.length();
}
}
for (Command command : Command.values()) {
String name = command.name().toLowerCase();
stdErr.printf(" %s%s %s\n",
name,
Strings.repeat(" ", lengthOfLongestCommand - name.length()),
command.getShortDescription());
}
stdErr.println("options:");
new GenericBuckOptions(stdOut, stdErr).printUsage();
return 1;
}
/**
* @param context an optional NGContext that is present if running inside a Nailgun server.
* @param args command line arguments
* @return an exit code or {@code null} if this is a process that should not exit
*/
@SuppressWarnings("PMD.EmptyCatchBlock")
public int runMainWithExitCode(File projectRoot, Optional<NGContext> context, String... args) throws IOException {
if (args.length == 0) {
return usage();
}
// Create common command parameters. projectFilesystem initialization looks odd because it needs
// ignorePaths from a BuckConfig instance, which in turn needs a ProjectFilesystem (i.e. this
// solves a bootstrapping issue).
ProjectFilesystem projectFilesystem = new ProjectFilesystem(
projectRoot,
createBuckConfig(new ProjectFilesystem(projectRoot), platform).getIgnorePaths());
BuckConfig config = createBuckConfig(projectFilesystem, platform);
Verbosity verbosity = VerbosityParser.parse(args);
final Console console = new Console(verbosity, stdOut, stdErr, config.createAnsi());
KnownBuildRuleTypes knownBuildRuleTypes = new KnownBuildRuleTypes();
// Create or get and invalidate cached command parameters.
Parser parser;
if (context.isPresent()) {
Daemon daemon = getDaemon(projectFilesystem, config, console);
daemon.watchClient(context.get());
daemon.watchFileSystem();
daemon.initWebServer();
parser = daemon.getParser();
} else {
parser = new Parser(projectFilesystem,
knownBuildRuleTypes,
console,
config.getPythonInterpreter(),
config.getTempFilePatterns(),
createRuleKeyBuilderFactory(config,
new DefaultFileHashCache(projectFilesystem, console)));
}
Clock clock = new DefaultClock();
final BuckEventBus buildEventBus = new BuckEventBus(
clock,
/* buildId */ MoreStrings.createRandomString());
// Find and execute command.
Optional<Command> command = Command.getCommandForName(args[0], console);
if (command.isPresent()) {
ImmutableList<BuckEventListener> eventListeners =
addEventListeners(buildEventBus,
clock,
projectFilesystem,
console,
config,
getWebServerIfDaemon(context));
String[] remainingArgs = new String[args.length - 1];
System.arraycopy(args, 1, remainingArgs, 0, remainingArgs.length);
Command executingCommand = command.get();
String commandName = executingCommand.name().toLowerCase();
buildEventBus.post(CommandEvent.started(commandName, context.isPresent()));
// The ArtifactCache is constructed lazily so that we do not try to connect to Cassandra when
// running commands such as `buck clean`.
ArtifactCacheFactory artifactCacheFactory = new ArtifactCacheFactory() {
@Override
public ArtifactCache newInstance(AbstractCommandOptions options) {
if (options.isNoCache()) {
return new NoopArtifactCache();
} else {
buildEventBus.post(ArtifactCacheConnectEvent.started());
ArtifactCache artifactCache = new LoggingArtifactCacheDecorator(buildEventBus)
.decorate(options.getBuckConfig().createArtifactCache(buildEventBus));
buildEventBus.post(ArtifactCacheConnectEvent.finished());
return artifactCache;
}
}
};
int exitCode = executingCommand.execute(remainingArgs, config, new CommandRunnerParams(
console,
projectFilesystem,
new KnownBuildRuleTypes(),
artifactCacheFactory,
buildEventBus,
parser,
platform));
buildEventBus.post(CommandEvent.finished(commandName, context.isPresent(), exitCode));
ExecutorService buildEventBusExecutor = buildEventBus.getExecutorService();
buildEventBusExecutor.shutdown();
try {
buildEventBusExecutor.awaitTermination(15, TimeUnit.SECONDS);
} catch (InterruptedException e) {
// Give the eventBus 15 seconds to finish dispatching all events, but if they should fail
// to finish in that amount of time just eat it, the end user doesn't care.
}
for (BuckEventListener eventListener : eventListeners) {
eventListener.outputTrace();
}
return exitCode;
} else {
int exitCode = new GenericBuckOptions(stdOut, stdErr).execute(args);
if (exitCode == GenericBuckOptions.SHOW_MAIN_HELP_SCREEN_EXIT_CODE) {
return usage();
} else {
return exitCode;
}
}
}
private Optional<WebServer> getWebServerIfDaemon(Optional<NGContext> context) {
if (context.isPresent()) {
return getDaemon().getWebServer();
}
return Optional.absent();
}
private void loadListenersFromBuckConfig(
ImmutableList.Builder<BuckEventListener> eventListeners,
ProjectFilesystem projectFilesystem,
BuckConfig config) {
final ImmutableSet<String> paths = config.getListenerJars();
if (paths.isEmpty()) {
return;
}
URL[] urlsArray = new URL[paths.size()];
try {
int i = 0;
for (String path : paths) {
String urlString = "file://" + projectFilesystem.getPathRelativizer().apply(path);
urlsArray[i] = new URL(urlString);
i++;
}
} catch (MalformedURLException e) {
throw new HumanReadableException(e.getMessage());
}
// This ClassLoader is disconnected to allow searching the JARs (and just the JARs) for classes.
ClassLoader isolatedClassLoader = URLClassLoader.newInstance(urlsArray, null);
ImmutableSet<ClassPath.ClassInfo> classInfos;
try {
ClassPath classPath = ClassPath.from(isolatedClassLoader);
classInfos = classPath.getTopLevelClasses();
} catch (IOException e) {
throw new HumanReadableException(e.getMessage());
}
// This ClassLoader will actually work, because it is joined to the parent ClassLoader.
URLClassLoader workingClassLoader = URLClassLoader.newInstance(urlsArray);
for (ClassPath.ClassInfo classInfo : classInfos) {
String className = classInfo.getName();
try {
Class<?> aClass = Class.forName(className, true, workingClassLoader);
if (BuckEventListener.class.isAssignableFrom(aClass)) {
BuckEventListener listener = aClass.asSubclass(BuckEventListener.class).newInstance();
eventListeners.add(listener);
}
} catch (ReflectiveOperationException e) {
throw new HumanReadableException("Error loading event listener class '%s': %s: %s",
className,
e.getClass(),
e.getMessage());
}
}
}
private ImmutableList<BuckEventListener> addEventListeners(BuckEventBus buckEvents,
Clock clock,
ProjectFilesystem projectFilesystem,
Console console,
BuckConfig config,
Optional<WebServer> webServer) {
ExecutionEnvironment executionEnvironment = new DefaultExecutionEnvironment();
ImmutableList.Builder<BuckEventListener> eventListenersBuilder =
ImmutableList.<BuckEventListener>builder()
.add(new JavaUtilsLoggingBuildListener())
.add(new ChromeTraceBuildListener(projectFilesystem, clock, config.getMaxTraces()));
if (webServer.isPresent()) {
eventListenersBuilder.add(webServer.get().createListener());
}
if (console.getAnsi().isAnsiTerminal()) {
SuperConsoleEventBusListener superConsole =
new SuperConsoleEventBusListener(console, clock, executionEnvironment);
superConsole.startRenderScheduler(100, TimeUnit.MILLISECONDS);
eventListenersBuilder.add(superConsole);
} else {
eventListenersBuilder.add(new SimpleConsoleEventBusListener(console, clock));
}
loadListenersFromBuckConfig(eventListenersBuilder, projectFilesystem, config);
ImmutableList<BuckEventListener> eventListeners = eventListenersBuilder.build();
for (BuckEventListener eventListener : eventListeners) {
buckEvents.register(eventListener);
}
JavaUtilsLoggingBuildListener.ensureLogFileIsWritten();
return eventListeners;
}
/**
* @param projectFilesystem The directory that is the root of the project being built.
*/
private static BuckConfig createBuckConfig(ProjectFilesystem projectFilesystem, Platform platform)
throws IOException {
ImmutableList.Builder<File> configFileBuilder = ImmutableList.builder();
File configFile = projectFilesystem.getFileForRelativePath(DEFAULT_BUCK_CONFIG_FILE_NAME);
if (configFile.isFile()) {
configFileBuilder.add(configFile);
}
File overrideConfigFile = projectFilesystem.getFileForRelativePath(
DEFAULT_BUCK_CONFIG_OVERRIDE_FILE_NAME);
if (overrideConfigFile.isFile()) {
configFileBuilder.add(overrideConfigFile);
}
ImmutableList<File> configFiles = configFileBuilder.build();
return BuckConfig.createFromFiles(projectFilesystem, configFiles, platform);
}
/**
* @param buckConfig This is currently unused, but we plan to use this in the near future so that
* global user configurations can be included when computing keys.
* @param hashCache A cache of file content hashes, used to avoid reading and hashing input files.
*/
@SuppressWarnings("unused")
private static RuleKeyBuilderFactory createRuleKeyBuilderFactory(BuckConfig buckConfig, final FileHashCache hashCache) {
return new RuleKeyBuilderFactory() {
@Override
public Builder newInstance(BuildRule buildRule) {
RuleKey.Builder builder = RuleKey.builder(buildRule, hashCache);
builder.set("buckVersionUid", BUCK_VERSION_UID);
return builder;
}
};
}
@VisibleForTesting
int tryRunMainWithExitCode(File projectRoot, Optional<NGContext> context, String... args)
throws IOException {
// TODO(user): enforce write command exclusion, but allow concurrent read only commands?
if (!commandSemaphore.tryAcquire()) {
return BUSY_EXIT_CODE;
}
try {
return runMainWithExitCode(projectRoot, context, args);
} catch (HumanReadableException e) {
Console console = new Console(Verbosity.STANDARD_INFORMATION,
stdOut,
stdErr,
new Ansi(platform));
console.printBuildFailure(e.getHumanReadableErrorMessage());
return FAIL_EXIT_CODE;
} finally {
commandSemaphore.release();
}
}
private void runMainThenExit(String[] args, Optional<NGContext> context) {
File projectRoot = new File(".");
int exitCode = FAIL_EXIT_CODE;
try {
exitCode = tryRunMainWithExitCode(projectRoot, context, args);
} catch (Throwable t) {
t.printStackTrace();
} finally {
// Exit explicitly so that non-daemon threads (of which we use many) don't
// keep the VM alive.
System.exit(exitCode);
}
}
public static void main(String[] args) {
new Main(System.out, System.err).runMainThenExit(args, Optional.<NGContext>absent());
}
/**
* When running as a daemon in the NailGun server, {@link #nailMain(NGContext)} is called instead
* of {@link #main(String[])} so that the given context can be used to listen for client
* disconnections and interrupt command processing when they occur.
*/
public static void nailMain(final NGContext context) throws InterruptedException {
new Main(context.out, context.err).runMainThenExit(context.getArgs(), Optional.of(context));
}
}