com.facebook.buck.cli.Main.java Source code

Java tutorial

Introduction

Here is the source code for com.facebook.buck.cli.Main.java

Source

/*
 * 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.LogEvent;
import com.facebook.buck.event.listener.AbstractConsoleEventBusListener;
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.java.JavaBuckConfig;
import com.facebook.buck.java.JavaCompilerEnvironment;
import com.facebook.buck.model.BuildId;
import com.facebook.buck.parser.Parser;
import com.facebook.buck.rules.BuildRule;
import com.facebook.buck.rules.CachingBuildEngine;
import com.facebook.buck.rules.KnownBuildRuleTypes;
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.AndroidDirectoryResolver;
import com.facebook.buck.util.Ansi;
import com.facebook.buck.util.Console;
import com.facebook.buck.util.DefaultAndroidDirectoryResolver;
import com.facebook.buck.util.DefaultFileHashCache;
import com.facebook.buck.util.DefaultPropertyFinder;
import com.facebook.buck.util.FileHashCache;
import com.facebook.buck.util.HumanReadableException;
import com.facebook.buck.util.ProcessExecutor;
import com.facebook.buck.util.ProjectFilesystem;
import com.facebook.buck.util.ProjectFilesystemWatcher;
import com.facebook.buck.util.PropertyFinder;
import com.facebook.buck.util.ShutdownException;
import com.facebook.buck.util.Verbosity;
import com.facebook.buck.util.WatchServiceWatcher;
import com.facebook.buck.util.WatchmanWatcher;
import com.facebook.buck.util.concurrent.TimeSpan;
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.nio.file.Paths;
import java.util.Arrays;
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;

    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 static final String BUCKD_COLOR_DEFAULT_ENV_VAR = "BUCKD_COLOR_DEFAULT";

    private static final int ARTIFACT_CACHE_TIMEOUT_IN_SECONDS = 15;

    private static final TimeSpan SUPER_CONSOLE_REFRESH_RATE = new TimeSpan(100, TimeUnit.MILLISECONDS);

    /**
     * Path to a directory of static content that should be served by the {@link WebServer}.
     */
    private static final String STATIC_CONTENT_DIRECTORY = System.getProperty("buck.path_to_static_content",
            "webserver/static");

    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 static final class Daemon implements Closeable {

        private final Parser parser;
        private final AndroidDirectoryResolver androidDirectoryResolver;
        private final DefaultFileHashCache hashCache;
        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, KnownBuildRuleTypes knownBuildRuleTypes,
                AndroidDirectoryResolver androidDirectoryResolver, BuckConfig config, Console console)
                throws IOException {
            this.config = Preconditions.checkNotNull(config);
            this.console = Preconditions.checkNotNull(console);
            this.hashCache = new DefaultFileHashCache(projectFilesystem, console);
            this.parser = new Parser(projectFilesystem, knownBuildRuleTypes, console, config.getPythonInterpreter(),
                    config.getTempFilePatterns(), createRuleKeyBuilderFactory(hashCache));
            this.androidDirectoryResolver = Preconditions.checkNotNull(androidDirectoryResolver);

            this.fileEventBus = new EventBus("file-change-events");
            this.filesystemWatcher = createWatcher(projectFilesystem);
            fileEventBus.register(parser);
            fileEventBus.register(hashCache);
            webServer = createWebServer(config, console, projectFilesystem);
            JavaUtilsLoggingBuildListener.ensureLogFileIsWritten(projectFilesystem);
        }

        private ProjectFilesystemWatcher createWatcher(ProjectFilesystem projectFilesystem) throws IOException {
            if (System.getProperty("buck.buckd_watcher", "WatchService").equals("Watchman")) {
                return new WatchmanWatcher(projectFilesystem, fileEventBus);
            }
            return new WatchServiceWatcher(projectFilesystem, fileEventBus, config.getIgnorePaths(),
                    FileSystems.getDefault().newWatchService());
        }

        private Optional<WebServer> createWebServer(BuckConfig config, Console console,
                ProjectFilesystem projectFilesystem) {
            // 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, projectFilesystem, STATIC_CONTENT_DIRECTORY));
                } 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 AndroidDirectoryResolver getAndroidDirectoryResolver() {
            return androidDirectoryResolver;
        }

        private void watchClient(final NGContext context) {
            context.addClientListener(new NGClientListener() {
                @Override
                public void clientDisconnected() throws InterruptedException {

                    // 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) {
                        // Client should no longer be connected, but printing helps detect false disconnections.
                        context.err.println("Client disconnected.");
                        throw new InterruptedException("Client disconnected.");
                    }
                }
            });
        }

        private void watchFileSystem(Console console, CommandEvent commandEvent, BuckEventBus eventBus)
                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) {
                parser.setConsole(console);
                hashCache.setConsole(console);
                parser.recordParseStartTime(eventBus);
                fileEventBus.post(commandEvent);
                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 volatile Daemon daemon;

    /**
     * Get or create Daemon.
     */
    private Daemon getDaemon(ProjectFilesystem filesystem, BuckConfig config,
            KnownBuildRuleTypes knownBuildRuleTypes, AndroidDirectoryResolver androidDirectoryResolver,
            Console console) throws IOException {
        if (daemon == null) {
            daemon = new Daemon(filesystem, knownBuildRuleTypes, androidDirectoryResolver, 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 or the AndroidDirectoryResolver has changed, invalidate the cache and
            // create a new daemon.
            if (!daemon.getConfig().equals(config)
                    || !daemon.getAndroidDirectoryResolver().equals(androidDirectoryResolver)) {
                daemon.close();
                daemon = new Daemon(filesystem, knownBuildRuleTypes, androidDirectoryResolver, config, console);
            }
        }
        return daemon;
    }

    @VisibleForTesting
    static void resetDaemon() {
        daemon = null;
    }

    @VisibleForTesting
    static void registerFileWatcher(Object watcher) {
        Preconditions.checkNotNull(daemon);
        daemon.fileEventBus.register(watcher);
    }

    @VisibleForTesting
    static void watchFilesystem() throws IOException {
        Preconditions.checkNotNull(daemon);
        daemon.filesystemWatcher.postEvents();
    }

    @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
     */
    public int runMainWithExitCode(File projectRoot, Optional<NGContext> context, String... args)
            throws IOException {
        if (args.length == 0) {
            return usage();
        }

        // Find and execute command.
        int exitCode;
        Command.ParseResult command = Command.parseCommandName(args[0]);
        if (command.getCommand().isPresent()) {
            return executeCommand(projectRoot, command, context, args);
        } else {
            exitCode = new GenericBuckOptions(stdOut, stdErr).execute(args);
            if (exitCode == GenericBuckOptions.SHOW_MAIN_HELP_SCREEN_EXIT_CODE) {
                return usage();
            } else {
                return exitCode;
            }
        }
    }

    /**
     * @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 executeCommand(File projectRoot, Command.ParseResult commandParseResult, Optional<NGContext> context,
            String... args) throws IOException {

        // 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(Paths.get(projectRoot.getPath()),
                createBuckConfig(new ProjectFilesystem(projectRoot), platform).getIgnorePaths());
        BuckConfig config = createBuckConfig(projectFilesystem, platform);
        Verbosity verbosity = VerbosityParser.parse(args);
        Optional<String> color;
        final boolean isDaemon = context.isPresent();
        if (isDaemon && (context.get().getEnv() != null)) {
            String colorString = context.get().getEnv().getProperty(BUCKD_COLOR_DEFAULT_ENV_VAR);
            color = Optional.fromNullable(colorString);
        } else {
            color = Optional.absent();
        }
        final Console console = new Console(verbosity, stdOut, stdErr, config.createAnsi(color));

        if (commandParseResult.getErrorText().isPresent()) {
            console.getStdErr().println(commandParseResult.getErrorText().get());
        }

        // No more early outs: acquire the command semaphore and become the only executing command.
        if (!commandSemaphore.tryAcquire()) {
            return BUSY_EXIT_CODE;
        }

        ProcessExecutor processExecutor = new ProcessExecutor(console);

        int exitCode;
        ImmutableList<BuckEventListener> eventListeners;
        BuildId buildId = new BuildId();
        Clock clock = new DefaultClock();
        ExecutionEnvironment executionEnvironment = new DefaultExecutionEnvironment(processExecutor);

        // Configure the AndroidDirectoryResolver.
        PropertyFinder propertyFinder = new DefaultPropertyFinder(projectFilesystem);
        AndroidDirectoryResolver androidDirectoryResolver = new DefaultAndroidDirectoryResolver(projectFilesystem,
                config.getNdkVersion(), propertyFinder);
        // Look up the javac version.
        JavaBuckConfig javaConfig = new JavaBuckConfig(config);
        JavaCompilerEnvironment javacEnv = javaConfig.getJavaCompilerEnvironment(processExecutor);

        // NOTE:  If any other variable is used when configuring buildRuleTypes, it MUST be passed down
        // to the Daemon and implement equals/hashCode so we can invalidate the Parser if values used
        // for configuring buildRuleTypes have changed between builds.
        KnownBuildRuleTypes buildRuleTypes = KnownBuildRuleTypes.createInstance(config, androidDirectoryResolver,
                javacEnv);

        // The order of resources in the try-with-resources block is important: the BuckEventBus must
        // be the last resource, so that it is closed first and can deliver its queued events to the
        // other resources before they are closed.
        try (AbstractConsoleEventBusListener consoleListener = createConsoleEventListener(clock, console, verbosity,
                executionEnvironment); BuckEventBus buildEventBus = new BuckEventBus(clock, buildId)) {
            Optional<WebServer> webServer = getWebServerIfDaemon(context, projectFilesystem, config, buildRuleTypes,
                    androidDirectoryResolver, console);
            eventListeners = addEventListeners(buildEventBus, projectFilesystem, config, webServer,
                    consoleListener);

            ImmutableList<String> remainingArgs = ImmutableList.copyOf(Arrays.copyOfRange(args, 1, args.length));

            Command executingCommand = commandParseResult.getCommand().get();
            String commandName = executingCommand.name().toLowerCase();

            CommandEvent commandEvent = CommandEvent.started(commandName, remainingArgs, isDaemon);
            buildEventBus.post(commandEvent);

            // 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 LoggingArtifactCacheFactory(executionEnvironment,
                    buildEventBus);

            // Create or get Parser and invalidate cached command parameters.
            Parser parser;

            if (isDaemon) {
                parser = getParserFromDaemon(context, projectFilesystem, config, buildRuleTypes,
                        androidDirectoryResolver, console, commandEvent, buildEventBus);
            } else {
                // Initialize logging and create new Parser for new process.
                JavaUtilsLoggingBuildListener.ensureLogFileIsWritten(projectFilesystem);
                parser = new Parser(projectFilesystem, buildRuleTypes, console, config.getPythonInterpreter(),
                        config.getTempFilePatterns(),
                        createRuleKeyBuilderFactory(new DefaultFileHashCache(projectFilesystem, console)));
            }

            CachingBuildEngine buildEngine = new CachingBuildEngine();
            exitCode = executingCommand.execute(remainingArgs, config,
                    new CommandRunnerParams(console, projectFilesystem, androidDirectoryResolver, buildRuleTypes,
                            buildEngine, artifactCacheFactory, buildEventBus, parser, platform));

            // TODO(user): allocate artifactCacheFactory in the try-with-resources block to avoid leaks.
            artifactCacheFactory.closeCreatedArtifactCaches(ARTIFACT_CACHE_TIMEOUT_IN_SECONDS);

            // If the Daemon is running and serving web traffic, print the URL to the Chrome Trace.
            if (webServer.isPresent()) {
                int port = webServer.get().getPort();
                buildEventBus.post(LogEvent.info("See trace at http://localhost:%s/trace/%s", port, buildId));
            }

            buildEventBus.post(CommandEvent.finished(commandName, remainingArgs, isDaemon, exitCode));
        } finally {
            commandSemaphore.release(); // Allow another command to execute while outputting traces.
        }
        if (isDaemon && !config.getFlushEventsBeforeExit()) {
            context.get().in.close(); // Avoid client exit triggering client disconnection handling.
            context.get().exit(exitCode); // Allow nailgun client to exit while outputting traces.
        }
        for (BuckEventListener eventListener : eventListeners) {
            try {
                eventListener.outputTrace(buildId);
            } catch (RuntimeException e) {
                System.err.println("Skipping over non-fatal error");
                e.printStackTrace();
            }
        }
        return exitCode;
    }

    private Parser getParserFromDaemon(Optional<NGContext> context, ProjectFilesystem projectFilesystem,
            BuckConfig config, KnownBuildRuleTypes knownBuildRuleTypes,
            AndroidDirectoryResolver androidDirectoryResolver, Console console, CommandEvent commandEvent,
            BuckEventBus eventBus) throws IOException {
        // Wire up daemon to new client and console and get cached Parser.
        Daemon daemon = getDaemon(projectFilesystem, config, knownBuildRuleTypes, androidDirectoryResolver,
                console);
        daemon.watchClient(context.get());
        daemon.watchFileSystem(console, commandEvent, eventBus);
        daemon.initWebServer();
        return daemon.getParser();
    }

    private Optional<WebServer> getWebServerIfDaemon(Optional<NGContext> context,
            ProjectFilesystem projectFilesystem, BuckConfig config, KnownBuildRuleTypes knownBuildRuleTypes,
            AndroidDirectoryResolver androidDirectoryResolver, Console console) throws IOException {
        if (context.isPresent()) {
            Daemon daemon = getDaemon(projectFilesystem, config, knownBuildRuleTypes, androidDirectoryResolver,
                    console);
            return daemon.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.getAbsolutifier().apply(Paths.get(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,
            ProjectFilesystem projectFilesystem, BuckConfig config, Optional<WebServer> webServer,
            AbstractConsoleEventBusListener consoleEventBusListener) {
        ImmutableList.Builder<BuckEventListener> eventListenersBuilder = ImmutableList.<BuckEventListener>builder()
                .add(new JavaUtilsLoggingBuildListener())
                .add(new ChromeTraceBuildListener(projectFilesystem, config.getMaxTraces()))
                .add(consoleEventBusListener);

        if (webServer.isPresent()) {
            eventListenersBuilder.add(webServer.get().createListener());
        }

        loadListenersFromBuckConfig(eventListenersBuilder, projectFilesystem, config);

        ImmutableList<BuckEventListener> eventListeners = eventListenersBuilder.build();

        for (BuckEventListener eventListener : eventListeners) {
            buckEvents.register(eventListener);
        }

        return eventListeners;
    }

    private AbstractConsoleEventBusListener createConsoleEventListener(Clock clock, Console console,
            Verbosity verbosity, ExecutionEnvironment executionEnvironment) {
        if (console.getAnsi().isAnsiTerminal() && !verbosity.shouldPrintCommand()
                && verbosity.shouldPrintStandardInformation()) {
            SuperConsoleEventBusListener superConsole = new SuperConsoleEventBusListener(console, clock,
                    executionEnvironment);
            superConsole.startRenderScheduler(SUPER_CONSOLE_REFRESH_RATE.getDuration(),
                    SUPER_CONSOLE_REFRESH_RATE.getUnit());
            return superConsole;
        }
        return new SimpleConsoleEventBusListener(console, clock);
    }

    /**
     * @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 hashCache A cache of file content hashes, used to avoid reading and hashing input files.
     */
    private static RuleKeyBuilderFactory createRuleKeyBuilderFactory(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?
        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;
        } catch (ShutdownException e) {
            stdErr.println(e);
            e.printStackTrace(stdErr);
            return 0;
        }
    }

    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));
    }
}