/*
 * SessionBuild.cpp
 *
 * Copyright (C) 2022 by Posit Software, PBC
 *
 * Unless you have received this program directly from Posit Software pursuant
 * to the terms of a commercial license agreement with Posit Software, then
 * this program is licensed to you under the terms of version 3 of the
 * GNU Affero General Public License. This program is distributed WITHOUT
 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
 *
 */

#include "SessionBuild.hpp"

#include "session-config.h"

#include <vector>

#include <boost/utility.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/format.hpp>
#include <boost/scope_exit.hpp>
#include <boost/enable_shared_from_this.hpp>
#include <boost/algorithm/string/split.hpp>
#include <boost/algorithm/string/join.hpp>

#include <core/Exec.hpp>
#include <core/FileSerializer.hpp>
#include <core/Version.hpp>
#include <core/text/DcfParser.hpp>
#include <core/system/Process.hpp>
#include <core/system/Environment.hpp>
#include <core/system/ShellUtils.hpp>
#include <core/r_util/RPackageInfo.hpp>

#include <session/SessionOptions.hpp>
#include "../modules/rmarkdown/SessionRMarkdown.hpp"

#ifdef _WIN32
# include <core/r_util/RToolsInfo.hpp>
#endif

#include <r/RExec.hpp>
#include <r/ROptions.hpp>
#include <r/RRoutines.hpp>
#include <r/RUtil.hpp>
#include <r/session/RSessionUtils.hpp>
#include <r/session/RConsoleHistory.hpp>

#include <session/projects/SessionProjects.hpp>
#include <session/SessionConsoleOutput.hpp>
#include <session/SessionModuleContext.hpp>
#include <session/SessionQuarto.hpp>
#include <session/prefs/UserPrefs.hpp>

#include "SessionBuildErrors.hpp"
#include "SessionSourceCpp.hpp"
#include "SessionInstallRtools.hpp"

#ifdef _WIN32
# define kPathSep ";"
#else
# define kPathSep ":"
#endif


#define kAnsiEscapeRegex "(?:\033\\[\\d+m)*"
#define kAnsiUrlRegex "(?:\u001B]8;.*?(?:\u0007|\033\\\\))*"


using namespace rstudio::core;
using namespace boost::placeholders;

namespace rstudio {
namespace session {

namespace {

static bool s_canBuildCpp = false;

std::string preflightPackageBuildErrorMessage(
      const std::string& message,
      const FilePath& buildDirectory)
{
   std::string fmt =
R"EOF(ERROR: Package build failed.

%1%

Build directory: %2%
)EOF";
   
   auto formatter = boost::format(fmt)
         % message
         % module_context::createAliasedPath(buildDirectory);
   return boost::str(formatter);
}

std::string quoteString(const std::string& str)
{
   return "'" + str + "'";
}

std::string packageArgsVector(std::string args)
{
   // spilt the string
   boost::algorithm::trim(args);
   std::vector<std::string> argList;
   boost::algorithm::split(argList,
                           args,
                           boost::is_space(),
                           boost::algorithm::token_compress_on);

   // quote the args
   std::vector<std::string> quotedArgs;
   std::transform(argList.begin(),
                  argList.end(),
                  std::back_inserter(quotedArgs),
                  quoteString);

   std::ostringstream ostr;
   ostr << "c(" << boost::algorithm::join(quotedArgs, ",") << ")";
   return ostr.str();
}

bool isPackageBuildError(const std::string& output)
{
   std::string input = boost::algorithm::trim_copy(output);
   return boost::algorithm::istarts_with(input, "warning: ") ||
          boost::algorithm::istarts_with(input, "error: ") ||
          boost::algorithm::ends_with(input, "WARNING");
}


} // anonymous namespace

namespace modules { 
namespace build {

namespace {

// track whether to force a package rebuild. we do this if the user
// saves a header file (since the R CMD INSTALL makefile doesn't
// force a rebuild for those changes)
bool s_forcePackageRebuild = false;

bool isPackageHeaderFile(const FilePath& filePath)
{
   if (projects::projectContext().hasProject() &&
       (projects::projectContext().config().buildType ==
                                              r_util::kBuildTypePackage) &&
       (boost::algorithm::starts_with(filePath.getExtensionLowerCase(), ".h") ||
        filePath.getExtensionLowerCase() == ".stan"))
   {
      FilePath pkgPath = projects::projectContext().buildTargetPath();
      std::string pkgRelative = filePath.getRelativePath(pkgPath);
      if (boost::algorithm::starts_with(pkgRelative, "src"))
         return true;
      else if (boost::algorithm::starts_with(pkgRelative, "inst/include"))
        return true;
   }

   return false;
}

void onFileChanged(FilePath sourceFilePath)
{
   // set package rebuild flag
   if (!s_forcePackageRebuild)
   {
      if (isPackageHeaderFile(sourceFilePath))
         s_forcePackageRebuild = true;
   }
}

void onSourceEditorFileSaved(FilePath sourceFilePath)
{
   onFileChanged(sourceFilePath);

   // see if this is a website file and fire an event if it is
   if (module_context::isWebsiteProject())
   {
      // see if the option is enabled for live preview
      projects::RProjectBuildOptions options;
      Error error = projects::projectContext().readBuildOptions(&options);
      if (error)
      {
         LOG_ERROR(error);
         return;
      }

      FilePath buildTargetPath = projects::projectContext().buildTargetPath();
      if (sourceFilePath.isWithin(buildTargetPath))
      {
         std::string outputDir = module_context::websiteOutputDir();
         FilePath outputDirPath = buildTargetPath.completeChildPath(outputDir);
         if (outputDir.empty() || !sourceFilePath.isWithin(outputDirPath))
         {
            // are we live previewing?
            bool livePreview = options.livePreviewWebsite;

            // force live preview for JS and CSS
            std::string mimeType = sourceFilePath.getMimeContentType();
            if (mimeType == "text/css" || mimeType == "text/javascript")
               livePreview = true;

            if (livePreview)
            {
               json::Object fileJson =
                   module_context::createFileSystemItem(sourceFilePath);
               ClientEvent event(client_events::kWebsiteFileSaved, fileJson);
               module_context::enqueClientEvent(event);
            }
         }
      }
   }
}

void onFilesChanged(const std::vector<core::system::FileChangeEvent>& events)
{
   if (!s_forcePackageRebuild)
   {
      for (const auto &event : events) {
         FilePath filePath(event.fileInfo().absolutePath());
         onFileChanged(filePath);
      }
   }
}

bool collectForcePackageRebuild()
{
   if (s_forcePackageRebuild)
   {
      s_forcePackageRebuild = false;
      return true;
   }
   else
   {
      return false;
   }
}

std::string regexLiteralEscape(std::string str)
{
   static const std::string meta = ".\\+*?[](){}^$|";
   if (str.find_first_of(meta) == std::string::npos)
      return str;

   std::string result;
   result.reserve(str.size() * 2);
   for (char c : str)
   {
      if (meta.find(c) != std::string::npos)
         result.push_back('\\');
      result.push_back(c);
   }
   return result;
}


const char * const kRoxygenizePackage = "roxygenize-package";
const char * const kBuildSourcePackage = "build-source-package";
const char * const kBuildBinaryPackage = "build-binary-package";
const char * const kTestPackage = "test-package";
const char * const kCheckPackage = "check-package";
const char * const kBuildAndReload = "build-all";
const char * const kBuildIncremental = "build-incremental";
const char * const kBuildFull = "build-full";
const char * const kTestFile = "test-file";
const char * const kTestShiny = "test-shiny";
const char * const kTestShinyFile = "test-shiny-file";

class Build : boost::noncopyable,
              public boost::enable_shared_from_this<Build>
{
public:
   static boost::shared_ptr<Build> create(const std::string& type,
                                          const std::string& subType)
   {
      boost::shared_ptr<Build> pBuild(new Build());
      pBuild->start(type, subType);
      return pBuild;
   }

private:
   Build()
      : isRunning_(false), terminationRequested_(false), restartR_(false),
        usedDevtools_(false), openErrorList_(true)
   {
   }

   void start(const std::string& type, const std::string& subType)
   {
      json::Object dataJson;
      dataJson["type"] = type;
      dataJson["sub_type"] = subType;
      ClientEvent event(client_events::kBuildStarted, dataJson);
      module_context::enqueClientEvent(event);

      isRunning_ = true;

      // read build options
      Error error = projects::projectContext().readBuildOptions(&options_);
      if (error)
      {
         terminateWithError("reading build options file", error);
         return;
      }

      // callbacks
      core::system::ProcessCallbacks cb;
      cb.onContinue = boost::bind(&Build::onContinue,
                                  Build::shared_from_this());
      cb.onStdout = boost::bind(&Build::onStandardOutput,
                                Build::shared_from_this(), _2);
      cb.onStderr = boost::bind(&Build::onStandardError,
                                Build::shared_from_this(), _2);
      cb.onExit =  boost::bind(&Build::onCompleted,
                                Build::shared_from_this(),
                                _1);

      // execute build
      executeBuild(type, subType, cb);
   }


   void executeBuild(const std::string& type,
                     const std::string& subType,
                     const core::system::ProcessCallbacks& cb)
   {
      // options
      core::system::ProcessOptions options;

#ifndef _WIN32
      options.terminateChildren = true;
#endif

      // notify build process of build-pane width
      core::system::Options environment;
      core::system::environment(&environment);
      int buildWidth = r::options::getBuildOptionWidth();
      if (buildWidth > 0)
         core::system::setenv(&environment, "RSTUDIO_CONSOLE_WIDTH",
                              safe_convert::numberToString(buildWidth));
      else
         core::system::unsetenv(&environment, "RSTUDIO_CONSOLE_WIDTH");

      // this runs in the build pane as a child process of this process
      core::system::setenv(&environment, "RSTUDIO_CHILD_PROCESS_PANE", "build");
      
      FilePath buildTargetPath = projects::projectContext().buildTargetPath();
      const core::r_util::RProjectConfig& config = projectConfig();
      if (type == kTestFile)
      {
         options.environment = environment;
         options.workingDir = buildTargetPath.getParent();
         FilePath testPath = FilePath(subType);
         executePackageBuild(type, testPath, options, cb);
      }
      else if (type == kTestShiny || type == kTestShinyFile)
      {
         FilePath testPath = FilePath(subType);
         testShiny(testPath, options, cb, type);
      }
      else if (config.buildType == r_util::kBuildTypePackage)
      {
         options.environment = environment;
         options.workingDir = buildTargetPath.getParent();
         executePackageBuild(type, buildTargetPath, options, cb);
      }
      else if (config.buildType == r_util::kBuildTypeMakefile)
      {
         options.environment = environment;
         options.workingDir = buildTargetPath;
         executeMakefileBuild(type, buildTargetPath, options, cb);
      }
      else if (config.buildType == r_util::kBuildTypeWebsite)
      {
         // pass along R_LIBS
         std::string rLibs = module_context::libPathsString();
         if (!rLibs.empty())
            core::system::setenv(&environment, "R_LIBS", rLibs);

         // pass along RSTUDIO_VERSION
         core::system::setenv(&environment, "RSTUDIO_VERSION", modules::rmarkdown::parsableRStudioVersion());
         core::system::setenv(&environment, "RSTUDIO_LONG_VERSION", RSTUDIO_VERSION);

         options.environment = environment;
         options.workingDir = buildTargetPath;
         executeWebsiteBuild(type, subType, buildTargetPath, options, cb);
      }
      else if (config.buildType == r_util::kBuildTypeCustom)
      {
         options.environment = environment;
         options.workingDir = buildTargetPath.getParent();
         executeCustomBuild(type, buildTargetPath, options, cb);
      }
      else if (quarto::quartoConfig().is_project)
      {
         options.environment = environment;
         options.workingDir = projects::projectContext().directory();
         executeQuartoBuild(subType, options, cb);
      }
      else
      {
         terminateWithError("Unrecognized build type: " + config.buildType);
      }
   }
   
   void executePackageBuild(const std::string& type,
                            const FilePath& packagePath,
                            const core::system::ProcessOptions& options,
                            const core::system::ProcessCallbacks& cb)
   {
      if (type == kTestFile)
      {
          // try to read package from /tests/testthat/filename.R,
          // but ignore errors if not within a package
          FilePath maybePackage = module_context::resolveAliasedPath(
             packagePath.getParent().getParent().getParent().getAbsolutePath()
          );

          pkgInfo_.read(maybePackage);
      }
      else
      {
         // validate that this is a package
         if (!packagePath.completeChildPath("DESCRIPTION").exists())
         {
            std::string message =
                  "The build directory does not contain a DESCRIPTION file and so "
                  "cannot be built as a package.";
            
            terminateWithError(preflightPackageBuildErrorMessage(message, packagePath));
            return;
         }

         // get package info
         Error error = pkgInfo_.read(packagePath);
         if (error)
         {
            // check to see if this was a parse error; if so, report that
            std::string parseError = error.getProperty("parse-error");
            if (!parseError.empty())
            {
               std::string message = "Failed to parse DESCRIPTION: " + parseError;
               terminateWithError(preflightPackageBuildErrorMessage(message, packagePath));
            }
            else
            {
               terminateWithError("reading package DESCRIPTION", error);
            }
            
            return;
         }

         // if this package links to Rcpp then we run compileAttributes
         if (pkgInfo_.linkingTo().find("Rcpp") != std::string::npos)
            if (!compileRcppAttributes(packagePath))
               return;
      }

      if (type == kRoxygenizePackage)
      {
         successMessage_ = "Documentation completed";
         roxygenize(packagePath, options, cb);
      }
      else
      {
         // bind a function that can be used to build the package
         boost::function<void()> buildFunction = boost::bind(
                         &Build::buildPackage, Build::shared_from_this(),
                         type, packagePath, options, cb);

         if (roxygenizeRequired(type))
         {
            // special callback for roxygenize result
            core::system::ProcessCallbacks roxygenizeCb = cb;
            roxygenizeCb.onExit =  boost::bind(&Build::onRoxygenizeCompleted,
                                               Build::shared_from_this(),
                                               _1,
                                               buildFunction);

            // run it
            roxygenize(packagePath, options, roxygenizeCb);
         }
         else
         {
            buildFunction();
         }
      }
   }

   bool roxygenizeRequired(const std::string& type)
   {
      if (!projectConfig().packageRoxygenize.empty())
      {
         if (type == kBuildAndReload && options_.autoRoxygenizeForBuildAndReload)
         {
            return true;
         }
         else if (type == kBuildIncremental && options_.autoRoxygenizeForBuildAndReload)
         {
            return true;
         }
         else if (type == kBuildFull && options_.autoRoxygenizeForBuildAndReload)
         {
            return true;
         }
         else if ( (type == kBuildSourcePackage ||
                    type == kBuildBinaryPackage) &&
                   options_.autoRoxygenizeForBuildPackage)
         {
            return true;
         }
         else if ( (type == kCheckPackage) &&
                   options_.autoRoxygenizeForCheck &&
                   !useDevtools())
         {
            return true;
         }
         else
         {
            return false;
         }
      }
      else
      {
         return false;
      }
   }


   std::string buildDocumentCall()
   {
      // get the package roclets
      std::string roclets = projectConfig().packageRoxygenize;
      
      // format nicely and quote
      boost::regex reComma(",");
      roclets = fmt::format("'{}'", boost::regex_replace(roclets, reComma, "', '"));

      std::string documentCall = useDevtools()
            ? fmt::format("devtools::document(roclets = c({}))", roclets)
            : fmt::format("roxygen2::roxygenize('.', roclets = c({}))", roclets);
      
      // show the user the call to document
      enqueCommandString(documentCall);

      return fmt::format(
               "suppressPackageStartupMessages({{ {}; {} }})",
               "Sys.setlocale('LC_COLLATE', 'C')",
               documentCall);
   }

   void onRoxygenizeCompleted(int exitStatus,
                              const boost::function<void()>& buildFunction)
   {
      if (exitStatus == EXIT_SUCCESS)
      {
         std::string msg = "Documentation completed\n\n";
         enqueBuildOutput(module_context::kCompileOutputNormal, msg);
         buildFunction();
      }
      else
      {
         terminateWithErrorStatus(exitStatus);
      }
   }


   void roxygenize(const FilePath& packagePath,
                   core::system::ProcessOptions options,
                   const core::system::ProcessCallbacks& cb)
   {
      FilePath rScriptPath;
      Error error = module_context::rScriptPath(&rScriptPath);
      if (error)
      {
         terminateWithError("Locating R script", error);
         return;
      }

      // check for required version of roxygen
      if (!module_context::isMinimumRoxygenInstalled())
      {
         terminateWithError(
                  "roxygen2 (>= 4.1.0) is required to generate documentation");
      }

      // make a copy of options so we can customize the environment
      core::system::Options childEnv;
      if (options.environment)
         childEnv = *options.environment;
      else
         core::system::environment(&childEnv);

      // allow child process to inherit our R_LIBS
      std::string libPaths = module_context::libPathsString();
      if (!libPaths.empty())
         core::system::setenv(&childEnv, "R_LIBS", libPaths);
      
      options.environment = childEnv;
      options.workingDir = packagePath;
      rExecute(true, buildDocumentCall(), options, cb);
   }

   bool compileRcppAttributes(const FilePath& packagePath)
   {
      if (module_context::haveRcppAttributes())
      {
         core::system::ProcessResult result;
         Error error = module_context::sourceModuleRFileWithResult(
                                             "SessionCompileAttributes.R",
                                             packagePath,
                                             &result);
         if (error)
         {
            LOG_ERROR(error);
            enqueCommandString("Rcpp::compileAttributes()");
            terminateWithError(r::endUserErrorMessage(error));
            return false;
         }
         else if (!result.stdOut.empty() || !result.stdErr.empty())
         {
            enqueCommandString("Rcpp::compileAttributes()");
            enqueBuildOutput(module_context::kCompileOutputNormal,
                             result.stdOut);
            if (!result.stdErr.empty())
               enqueBuildOutput(module_context::kCompileOutputError,
                                result.stdErr);
            enqueBuildOutput(module_context::kCompileOutputNormal, "\n");
            if (result.exitStatus == EXIT_SUCCESS)
            {
               return true;
            }
            else
            {
               terminateWithErrorStatus(result.exitStatus);
               return false;
            }
         }
         else
         {
            return true;
         }
      }
      else
      {
         return true;
      }
   }

   void buildPackage(const std::string& type,
                     const FilePath& packagePath,
                     const core::system::ProcessOptions& options,
                     const core::system::ProcessCallbacks& cb)
   {

      // use both the R and gcc error parsers
      CompileErrorParsers parsers;
      parsers.add(rErrorParser(packagePath.completePath("R")));
      parsers.add(gccErrorParser(packagePath.completePath("src")));

      // track build type
      type_ = type;

      // add testthat result parsers
      if (type == kTestFile)
      {
         openErrorList_ = false;
         if (module_context::isPackageInstalled("testthat"))
         {
            parsers.add(testthatErrorParser(packagePath.getParent()));
         }
         
      }
      else if (type == kTestPackage)
      {
         openErrorList_ = false;
         if (module_context::isPackageInstalled("testthat"))
         {
            parsers.add(testthatErrorParser(packagePath.completePath("tests/testthat")));
         }
      }

      initErrorParser(packagePath, parsers);

      // make a copy of options so we can customize the environment
      core::system::ProcessOptions pkgOptions(options);
      core::system::Options childEnv;
      if (options.environment)
         childEnv = *options.environment;
      else
         core::system::environment(&childEnv);

      // get library paths for build
      std::string libPaths = module_context::libPathsString();
      
      // use a sub-directory for build if configured to do so
      if (prefs::userPrefs().useBuildSubdirectory())
      {
         std::string buildLibraryPath;
         Error libError = r::exec::RFunction(".rs.makeBuildLibraryPath")
               .call(&buildLibraryPath);
         if (libError)
            LOG_ERROR(libError);

         // prepend build library path if its available
         if (!buildLibraryPath.empty())
         {
            builtPackagePath_ = fmt::format("{}/{}", buildLibraryPath, pkgInfo_.name());
            libPaths = fmt::format("{}{}{}", buildLibraryPath, kPathSep, libPaths);
         }
      }
      
      // set this for the child process
      core::system::setenv(&childEnv, "R_LIBS", libPaths);
      
      // record the library paths used when this build was kicked off
      libPaths_ = module_context::getLibPaths();

      // prevent spurious cygwin warnings on windows
#ifdef _WIN32
      core::system::setenv(&childEnv, "CYGWIN", "nodosfilewarning");
#endif

      // set the not cran env var
      core::system::setenv(&childEnv, "NOT_CRAN", "true");

      // turn off external applications launching
      core::system::setenv(&childEnv, "R_BROWSER", "false");
      core::system::setenv(&childEnv, "R_PDFVIEWER", "false");

      // add r tools to path if necessary
      module_context::addRtoolsToPathIfNecessary(&childEnv, &buildToolsWarning_);

      pkgOptions.environment = childEnv;

      // get R bin directory
      FilePath rBinDir;
      Error error = module_context::rBinDir(&rBinDir);
      if (error)
      {
         terminateWithError("attempting to locate R binary", error);
         return;
      }

      // install an error filter (because R package builds produce much
      // of their output on stderr)
      errorOutputFilterFunction_ = isPackageBuildError;

      // build command
      if (type == kBuildAndReload || type == kBuildIncremental || type == kBuildFull)
      {
         // restart R after build is completed
         restartR_ = true;

         // build command
         module_context::RCommand rCmd(rBinDir);
         rCmd << "INSTALL";

         // get extra args
         std::string extraArgs = projectConfig().packageInstallArgs;

         // add --preclean if necessary
         if (type == kBuildFull || collectForcePackageRebuild() || (type == kBuildAndReload && cleanBeforeInstall()) )
         {
            if (!boost::algorithm::contains(extraArgs, "--preclean"))
               rCmd << "--preclean";
         }

         // remove --with-keep.source if this is R < 2.14
         if (!r::util::hasRequiredVersion("2.14"))
         {
            using namespace boost::algorithm;
            replace_all(extraArgs, "--with-keep.source", "");
            replace_all(extraArgs, "--without-keep.source", "");
         }

         // add extra args if provided
         rCmd << extraArgs;

         // add filename as a FilePath so it is escaped
         rCmd << FilePath(packagePath.getFilename());

         // show the user the command
         enqueCommandString(rCmd.commandString());

         // run R CMD INSTALL <package-dir>
         module_context::processSupervisor().runCommand(rCmd.shellCommand(),
                                                        pkgOptions,
                                                        cb);
      }

      else if (type == kBuildSourcePackage)
      {
         if (useDevtools())
         {
            devtoolsBuildPackage(packagePath, false, pkgOptions, cb);
         }
         else
         {
            if (session::options().packageOutputInPackageFolder())
            {
               pkgOptions.workingDir = packagePath;
            }
            buildSourcePackage(rBinDir, packagePath, pkgOptions, cb);
         }
      }

      else if (type == kBuildBinaryPackage)
      {
         if (useDevtools())
         {
            devtoolsBuildPackage(packagePath, true, pkgOptions, cb);
         }
         else
         {
            if (session::options().packageOutputInPackageFolder())
            {
               pkgOptions.workingDir = packagePath;
            }
            buildBinaryPackage(rBinDir, packagePath, pkgOptions, cb);
         }
      }

      else if (type == kCheckPackage)
      {
         if (useDevtools())
         {
            // redirect stderr to stdout for certain build types
            // see: https://github.com/rstudio/rstudio/issues/5126
            pkgOptions.redirectStdErrToStdOut = true;
            
            devtoolsCheckPackage(packagePath, pkgOptions, cb);
         }
         else
         {
            if (session::options().packageOutputInPackageFolder())
            {
               pkgOptions.workingDir = packagePath;
            }
            checkPackage(rBinDir, packagePath, pkgOptions, cb);
         }
      }

      else if (type == kTestPackage)
      {
         if (useDevtools())
         {
            // redirect stderr to stdout for certain build types
            // see: https://github.com/rstudio/rstudio/issues/5126
            pkgOptions.redirectStdErrToStdOut = true;
            
            devtoolsTestPackage(packagePath, pkgOptions, cb);
         }
         else
         {
            testPackage(packagePath, pkgOptions, cb);
         }
      }

      else if (type == kTestFile)
      {
         testFile(packagePath, pkgOptions, cb);
      }
   }

   void buildSourcePackage(const FilePath& rBinDir,
                           const FilePath& packagePath,
                           const core::system::ProcessOptions& pkgOptions,
                           const core::system::ProcessCallbacks& cb)
   {
      // compose the build command
      module_context::RCommand rCmd(rBinDir);
      rCmd << "build";

      // add extra args if provided
      std::string extraArgs = projectConfig().packageBuildArgs;
      rCmd << extraArgs;

      // add filename as a FilePath so it is escaped
      if (session::options().packageOutputInPackageFolder())
         rCmd << FilePath(".");
      else
         rCmd << FilePath(packagePath.getFilename());

      // show the user the command
      enqueCommandString(rCmd.commandString());

      // set a success message
      successMessage_ = buildPackageSuccessMsg("Source");

      // run R CMD build <package-dir>
      module_context::processSupervisor().runCommand(rCmd.shellCommand(),
                                                     pkgOptions,
                                                     cb);

   }


   void buildBinaryPackage(const FilePath& rBinDir,
                           const FilePath& packagePath,
                           const core::system::ProcessOptions& pkgOptions,
                           const core::system::ProcessCallbacks& cb)
   {
      // compose the INSTALL --binary
      module_context::RCommand rCmd(rBinDir);
      rCmd << "INSTALL";
      rCmd << "--build";
      rCmd << "--preclean";

      // add extra args if provided
      std::string extraArgs = projectConfig().packageBuildBinaryArgs;
      rCmd << extraArgs;

      // add filename as a FilePath so it is escaped
      if (session::options().packageOutputInPackageFolder())
         rCmd << FilePath(".");
      else
         rCmd << FilePath(packagePath.getFilename());

      // show the user the command
      enqueCommandString(rCmd.commandString());

      // set a success message
      successMessage_ = "\n" + buildPackageSuccessMsg("Binary");

      // run R CMD INSTALL --build <package-dir>
      module_context::processSupervisor().runCommand(rCmd.shellCommand(),
                                                     pkgOptions,
                                                     cb);
   }

   void checkPackage(const FilePath& rBinDir,
                     const FilePath& packagePath,
                     const core::system::ProcessOptions& pkgOptions,
                     const core::system::ProcessCallbacks& cb)
   {
      // first build then check

      // compose the build command
      module_context::RCommand rCmd(rBinDir);
      rCmd << "build";

      // add extra args if provided
      rCmd << projectConfig().packageBuildArgs;

      // add --no-manual and --no-build-vignettes if they are in the check options
      std::string checkArgs = projectConfig().packageCheckArgs;
      if (checkArgs.find("--no-manual") != std::string::npos)
         rCmd << "--no-manual";
      if (checkArgs.find("--no-build-vignettes") != std::string::npos)
         rCmd << "--no-build-vignettes";

      // add filename as a FilePath so it is escaped
      if (session::options().packageOutputInPackageFolder())
         rCmd << FilePath(".");
      else
         rCmd << FilePath(packagePath.getFilename());

      // compose the check command (will be executed by the onExit
      // handler of the build cmd)
      module_context::RCommand rCheckCmd(rBinDir);
      rCheckCmd << "check";

      // add extra args if provided
      std::string extraArgs = projectConfig().packageCheckArgs;
      rCheckCmd << extraArgs;

      // add filename as a FilePath so it is escaped
      rCheckCmd << FilePath(pkgInfo_.sourcePackageFilename());

      // special callback for build result
      core::system::ProcessCallbacks buildCb = cb;
      buildCb.onExit =  boost::bind(&Build::onBuildForCheckCompleted,
                                    Build::shared_from_this(),
                                    _1,
                                    rCheckCmd,
                                    pkgOptions,
                                    buildCb);

      // show the user the command
      enqueCommandString(rCmd.commandString());

      // set a success message
      successMessage_ = "R CMD check succeeded\n";

      // bind a success function if appropriate
      if (prefs::userPrefs().cleanupAfterRCmdCheck())
      {
         successFunction_ = boost::bind(&Build::cleanupAfterCheck,
                                        Build::shared_from_this(),
                                        pkgInfo_);
      }

      if (prefs::userPrefs().viewDirAfterRCmdCheck())
      {
         failureFunction_ = boost::bind(
                  &Build::viewDirAfterFailedCheck,
                  Build::shared_from_this(),
                  pkgInfo_);
      }

      // run the source build
      module_context::processSupervisor().runCommand(rCmd.shellCommand(),
                                                     pkgOptions,
                                                     buildCb);
   }

   bool rExecute(bool vanilla,
                 const std::string& command,
                 const core::system::ProcessOptions& pkgOptions,
                 const core::system::ProcessCallbacks& cb)
   {
      Error error;
      
      // Find the path to R
      FilePath rProgramPath;
      error = module_context::rScriptPath(&rProgramPath);
      if (error)
      {
         terminateWithError("attempting to locate R binary", error);
         return false;
      }
      
      // build args
      std::vector<std::string> args;
      if (vanilla)
         args.push_back("--vanilla");

      // build script to be executed
      std::string scriptPath;
      error = r::exec::RFunction(".rs.generateCommandScript")
            .addUtf8Param(command)
            .callUtf8(&scriptPath);
      
      if (error)
      {
         // fall back to executing command directly, just in case
         args.push_back("-s");
         args.push_back("-e");
         args.push_back(command);
      }
      else
      {
         // execute command from script
         args.push_back("-s");
         args.push_back("-f");
         args.push_back(scriptPath);
      }

      // run it
      module_context::processSupervisor().runProgram(
               string_utils::utf8ToSystem(rProgramPath.getAbsolutePath()),
               args,
               pkgOptions,
               cb);

      return true;
   }

   bool devtoolsExecute(const std::string& command,
                        const FilePath& packagePath,
                        core::system::ProcessOptions pkgOptions,
                        const core::system::ProcessCallbacks& cb)
   {
      pkgOptions.workingDir = packagePath;
      if (!rExecute(true, command, pkgOptions, cb))
         return false;

      usedDevtools_ = true;
      return true;
   }

   void devtoolsCheckPackage(const FilePath& packagePath,
                             const core::system::ProcessOptions& pkgOptions,
                             const core::system::ProcessCallbacks& cb)
   {
      // build the call to check
      std::ostringstream ostr;
      ostr << "devtools::check(";

      std::vector<std::string> args;

      if (projectConfig().packageRoxygenize.empty() ||
          !options_.autoRoxygenizeForCheck)
         args.push_back("document = FALSE");

      if (!prefs::userPrefs().cleanupAfterRCmdCheck() && 
          !module_context::isPackageVersionInstalled("devtools", "2.4.4"))
         args.push_back("cleanup = FALSE");

      // optional extra check args
      if (!projectConfig().packageCheckArgs.empty())
      {
         args.push_back("args = " +
                        packageArgsVector(projectConfig().packageCheckArgs));
      }

      // optional extra build args
      if (!projectConfig().packageBuildArgs.empty())
      {
         // propagate check vignette args
         // add --no-manual and --no-build-vignettes if they are specified
         std::string buildArgs = projectConfig().packageBuildArgs;
         std::string checkArgs = projectConfig().packageCheckArgs;
         if (checkArgs.find("--no-manual") != std::string::npos)
            buildArgs.append(" --no-manual");
         if (checkArgs.find("--no-build-vignettes") != std::string::npos)
            buildArgs.append(" --no-build-vignettes");

         args.push_back("build_args = " + packageArgsVector(buildArgs));
      }

      // add the args
      ostr << boost::algorithm::join(args, ", ");

      // enque the command string without the check_dir
      enqueCommandString(ostr.str() + ")");

      // now complete the command
      if (session::options().packageOutputInPackageFolder())
         ostr << ", check_dir = getwd())";
      else
         ostr << ", check_dir = dirname(getwd()))";
      std::string command = ostr.str();

      // set a success message
      successMessage_ = "\nR CMD check succeeded\n";

      // bind a success function if appropriate
      if (prefs::userPrefs().cleanupAfterRCmdCheck())
      {
         successFunction_ = boost::bind(&Build::cleanupAfterCheck,
                                        Build::shared_from_this(),
                                        pkgInfo_);
      }

      if (prefs::userPrefs().viewDirAfterRCmdCheck())
      {
         failureFunction_ = boost::bind(&Build::viewDirAfterFailedCheck,
                                        Build::shared_from_this(),
                                        pkgInfo_);
      }

      // run it
      devtoolsExecute(command, packagePath, pkgOptions, cb);
   }

   void devtoolsTestPackage(const FilePath& packagePath,
                            const core::system::ProcessOptions& pkgOptions,
                            const core::system::ProcessCallbacks& cb)
   {
      std::string command = "devtools::test()";
      enqueCommandString(command);
      devtoolsExecute(command, packagePath, pkgOptions, cb);
   }

   void testPackage(const FilePath& packagePath,
                    core::system::ProcessOptions pkgOptions,
                    const core::system::ProcessCallbacks& cb)
   {
      FilePath rScriptPath;
      Error error = module_context::rScriptPath(&rScriptPath);
      if (error)
      {
         terminateWithError("Locating R script", error);
         return;
      }

      // navigate to the tests directory and source all R
      // scripts within
      FilePath testsPath = packagePath.completePath("tests");

      // construct a shell command to execute
      shell_utils::ShellCommand cmd(rScriptPath);
      cmd << "--vanilla";
      cmd << "-s";
      cmd << "-e";
      
      boost::format fmt(
         "setwd('%1%');"
         "files <- list.files(pattern = '[.][rR]$');"
         "invisible(lapply(files, function(x) {"
         "    system(paste(shQuote('%2%'), '--vanilla -s -f', shQuote(x)))"
         "}))"
      );

      cmd << boost::str(fmt %
                           testsPath.getAbsolutePath() %
                        rScriptPath.getAbsolutePath());

      pkgOptions.workingDir = testsPath;
      enqueCommandString("Sourcing R files in 'tests' directory");
      successMessage_ = "\nTests complete";
      module_context::processSupervisor().runCommand(cmd,
                                                     pkgOptions,
                                                     cb);

   }

   void testFile(const FilePath& testPath,
                 core::system::ProcessOptions pkgOptions,
                 const core::system::ProcessCallbacks& cb)
   {
      FilePath rScriptPath;
      Error error = module_context::rScriptPath(&rScriptPath);
      if (error)
      {
         terminateWithError("Locating R script", error);
         return;
      }

      // construct command to execute
      using namespace string_utils;
      std::string command;

      // check if this is a tests/testthat/test-<name>.R file in a package project;
      // if so, use devtools::test(filter = "^<name>$") instead of testthat::test_file().
      // The filter is matched as a regex by testthat::test_dir, so escape any
      // regex metacharacters in <name> and anchor with ^/$ so we run only the
      // current test file (e.g. test-prereg.R must not also run test-mod-prereg.R).
      boost::smatch match;
      boost::regex reTestThat("tests/testthat/test-(.+)\\.[rR]$");
      std::string path = testPath.getAbsolutePath();
      if (!pkgInfo_.empty() &&
          useDevtools() &&
          boost::regex_search(path, match, reTestThat))
      {
         std::string filterName = singleQuotedStrEscape(regexLiteralEscape(match[1]));
         command = fmt::format("devtools::test(filter = '^{}$')", filterName);
         pkgOptions.workingDir = projects::projectContext().buildTargetPath();
         enqueCommandString(command);
      }
      else
      {

         // build default test command
         std::string testPathEscaped =
             singleQuotedStrEscape(utf8ToSystem(testPath.getAbsolutePath()));
         command = fmt::format("testthat::test_file('{}')", testPathEscaped);

         // use devtools::load_all() if this is a package project
         if (!pkgInfo_.empty())
         {
            pkgOptions.workingDir = projects::projectContext().buildTargetPath();
            command = fmt::format("devtools::load_all(); {}", command);
         }

         enqueCommandString("Testing R file using 'testthat'");
      }

      successMessage_ = "\nTest complete";

      rExecute(true, command, pkgOptions, cb);
   }

   void testShiny(FilePath& shinyPath,
                  core::system::ProcessOptions testOptions,
                  const core::system::ProcessCallbacks& cb,
                  const std::string& type)
   {
      // normalize paths between all tests and single test
      // shinytest2 stores tests in tests/testthat/
      std::string shinyTestFile;
      if (type == kTestShinyFile) {
        shinyTestFile = shinyPath.getAbsolutePath();
        shinyPath = shinyPath.getParent();
        if (shinyPath.getFilename() == "testthat")
        {
           shinyPath = shinyPath.getParent();
        }
        if (shinyPath.getFilename() == "tests")
        {
           // Move up from the tests folder to the app folder.
           shinyPath = shinyPath.getParent();
        }
        else
        {
           // If this doesn't look like it's in a tests directory, bail out.
           terminateWithError("Could not find Shiny app for test in " +
              shinyPath.getAbsolutePath());
        }
      }

      // shinytest2 is testthat-based, so use the testthat error parser
      CompileErrorParsers parsers;
      parsers.add(testthatErrorParser(shinyPath.completePath("tests/testthat")));
      initErrorParser(shinyPath, parsers);

      // shinytest2 tests gate themselves on NOT_CRAN — without it they
      // silently skip, so set it here for interactive runs.
      core::system::Options childEnv;
      if (testOptions.environment)
         childEnv = *testOptions.environment;
      else
         core::system::environment(&childEnv);
      core::system::setenv(&childEnv, "NOT_CRAN", "true");
      testOptions.environment = childEnv;

      FilePath rScriptPath;
      Error error = module_context::rScriptPath(&rScriptPath);
      if (error)
      {
         terminateWithError("Locating R script", error);
         return;
      }

      // construct a shell command to execute
      shell_utils::ShellCommand cmd(rScriptPath);
      cmd << "--vanilla";
      cmd << "-s";
      cmd << "-e";

      // escape the paths before interpolating them into the R command, so that
      // a filename containing a single quote cannot break out of the string
      // literal and inject arbitrary R code
      if (type == kTestShiny)
      {
        cmd << fmt::format("shinytest2::test_app('{}')",
                           string_utils::singleQuotedStrEscape(
                              string_utils::utf8ToSystem(shinyPath.getAbsolutePath())));
      }
      else if (type == kTestShinyFile)
      {
        cmd << fmt::format("testthat::test_file('{}')",
                           string_utils::singleQuotedStrEscape(
                              string_utils::utf8ToSystem(shinyTestFile)));
      }
      else
      {
        terminateWithError("Shiny test type is unsupported.");
      }

      enqueCommandString("Testing Shiny application using 'shinytest2'");
      successMessage_ = "\nTest complete";
      module_context::processSupervisor().runCommand(cmd,
                                                     testOptions,
                                                     cb);

   }

   void devtoolsBuildPackage(const FilePath& packagePath,
                             bool binary,
                             const core::system::ProcessOptions& pkgOptions,
                             const core::system::ProcessCallbacks& cb)
   {
      // create the call to build
      std::ostringstream ostr;
      ostr << "devtools::build(";

      // args
      std::vector<std::string> args;

      // binary package?
      if (binary)
         args.push_back("binary = TRUE");

      if (session::options().packageOutputInPackageFolder())
         args.push_back("path = getwd()");

       // add R args
      std::string rArgs = binary ?  projectConfig().packageBuildBinaryArgs :
                                    projectConfig().packageBuildArgs;
      if (binary)
         rArgs.append(" --preclean");
      if (!rArgs.empty())
         args.push_back("args = " + packageArgsVector(rArgs));

      ostr << boost::algorithm::join(args, ", ");
      ostr << ")";

      // set a success message
      std::string type = binary ? "Binary" : "Source";
      successMessage_ = "\n" + buildPackageSuccessMsg(type);

      // execute it
      std::string command = ostr.str();
      enqueCommandString(command);
      devtoolsExecute(command, packagePath, pkgOptions, cb);
   }


   void onBuildForCheckCompleted(
                         int exitStatus,
                         const module_context::RCommand& checkCmd,
                         const core::system::ProcessOptions& checkOptions,
                         const core::system::ProcessCallbacks& checkCb)
   {
      if (exitStatus == EXIT_SUCCESS)
      {
         // show the user the build command
         enqueCommandString(checkCmd.commandString());

         // run the check
         module_context::processSupervisor().runCommand(checkCmd.shellCommand(),
                                                        checkOptions,
                                                        checkCb);
      }
      else
      {
         terminateWithErrorStatus(exitStatus);
      }
   }


   void cleanupAfterCheck(const r_util::RPackageInfo& pkgInfo)
   {
      // compute paths
      FilePath buildPath = projects::projectContext().buildTargetPath();
      if (!session::options().packageOutputInPackageFolder())
         buildPath = buildPath.getParent();
      FilePath srcPkgPath = buildPath.completeChildPath(pkgInfo.sourcePackageFilename());
      FilePath chkDirPath = buildPath.completeChildPath(pkgInfo.name() + ".Rcheck");

      // cleanup
      Error error = srcPkgPath.removeIfExists();
      if (error)
         LOG_ERROR(error);
      error = chkDirPath.removeIfExists();
      if (error)
         LOG_ERROR(error);
   }

   void viewDirAfterFailedCheck(const r_util::RPackageInfo& pkgInfo)
   {
      if (!terminationRequested_)
      {
         FilePath buildPath = projects::projectContext().buildTargetPath();
         if (!session::options().packageOutputInPackageFolder())
            buildPath = buildPath.getParent();
         FilePath chkDirPath = buildPath.completeChildPath(pkgInfo.name() + ".Rcheck");

         json::Object dataJson;
         dataJson["directory"] = module_context::createAliasedPath(chkDirPath);
         dataJson["activate"] = true;
         ClientEvent event(client_events::kDirectoryNavigate, dataJson);

         module_context::enqueClientEvent(event);
      }
   }

   void executeMakefileBuild(const std::string& type,
                             const FilePath& targetPath,
                             const core::system::ProcessOptions& options,
                             const core::system::ProcessCallbacks& cb)
   {
      // validate that there is a Makefile file
      FilePath makefilePath = targetPath.completeChildPath("Makefile");
      if (!makefilePath.exists())
      {
         boost::format fmt ("ERROR: The build directory does "
                            "not contain a Makefile\n"
                            "so the target cannot be built.\n\n"
                            "Build directory: %1%\n");
         terminateWithError(boost::str(
                 fmt % module_context::createAliasedPath(targetPath)));
         return;
      }

      // install the gcc error parser
      initErrorParser(targetPath, gccErrorParser(targetPath));

      std::string make = "make";
      if (!options_.makefileArgs.empty())
         make += " " + options_.makefileArgs;

      std::string makeClean = make + " clean";

      std::string cmd;
      if (type == "build-all")
      {
         // cmd = shell_utils::join_and(makeClean, make);
         cmd = make;
      }
      else if (type == "clean-all")
      {
         cmd = makeClean;
      }
      
      module_context::processSupervisor().runCommand(cmd,
                                                     options,
                                                     cb);
   }

   void executeCustomBuild(const std::string& /*type*/,
                           const FilePath& customScriptPath,
                           const core::system::ProcessOptions& options,
                           const core::system::ProcessCallbacks& cb)
   {
      module_context::processSupervisor().runCommand(
                           shell_utils::ShellCommand(customScriptPath),
                           options,
                           cb);
   }

   void executeQuartoBuild(const std::string& subType,
                           const core::system::ProcessOptions& options,
                           const core::system::ProcessCallbacks& cb)
   {
       auto cmd = shell_utils::ShellCommand("quarto");
       cmd << "render";
       if (!subType.empty())
          cmd << "--to" << subType;
       module_context::processSupervisor().runCommand(cmd, options,cb);
   }


   void executeWebsiteBuild(const std::string& type,
                            const std::string& subType,
                            const FilePath& websitePath,
                            const core::system::ProcessOptions& options,
                            const core::system::ProcessCallbacks& cb)
   {
      std::string command;

      if (type == "build-all")
      {
         if (options_.previewWebsite)
         {
            successFunction_ = boost::bind(&Build::showWebsitePreview,
                                           Build::shared_from_this(),
                                           websitePath);
         }

         // if there is a subType then use it to set the output format
         if (!subType.empty())
         {
            projects::projectContext().setWebsiteOutputFormat(subType);
            options_.websiteOutputFormat = subType;
         }

         boost::format fmt("rmarkdown::render_site(%1%)");
         std::string format;
         if (options_.websiteOutputFormat != "all")
            format = "output_format = '" +
                     string_utils::singleQuotedStrEscape(options_.websiteOutputFormat) +
                     "', ";

         format += ("encoding = '" +
                    string_utils::singleQuotedStrEscape(
                       projects::projectContext().defaultEncoding()) +
                    "'");

         command = boost::str(fmt % format);
      }
      else if (type == "clean-all")
      {
         command = "rmarkdown::clean_site()";
      }

      // execute command
      enqueCommandString(command);
      rExecute(false, command, options, cb);
   }

   void enquePreviewRmdEvent(const FilePath& sourceFile, const FilePath& outputFile)
   {
      json::Object previewRmdJson;
      using namespace module_context;
      previewRmdJson["source_file"] = createAliasedPath(sourceFile);
      previewRmdJson["encoding"] = projects::projectContext().config().encoding;
      previewRmdJson["output_file"] = createAliasedPath(outputFile);
      ClientEvent event(client_events::kPreviewRmd, previewRmdJson);
      enqueClientEvent(event);
   }

   void showWebsitePreview(const FilePath& websitePath)
   {
      // determine source file
      std::string output = outputAsText();
      FilePath sourceFile = websiteSourceFile(websitePath);
      if (sourceFile.isEmpty())
         return;

      // look for Output created message
      FilePath outputFile = module_context::extractOutputFileCreated(sourceFile.getParent(),
                                                                     output);
      if (!outputFile.isEmpty())
      {
         enquePreviewRmdEvent(sourceFile, outputFile);
      }
   }

   void showQuartoSitePreview()
   {
      // determine source file
      auto config = quarto::quartoConfig();
      auto quartoProjectDir = module_context::resolveAliasedPath(config.project_dir);
      std::string output = outputAsText();
      FilePath sourceFile = websiteSourceFile(quartoProjectDir);
      if (sourceFile.isEmpty())
         return;

      // look for Output created message
      FilePath outputFile = module_context::extractOutputFileCreated(
         projects::projectContext().directory(),
         output
      );
      if (!outputFile.isEmpty())
      {
         enquePreviewRmdEvent(sourceFile, outputFile);
      }
   }

   FilePath websiteSourceFile(const FilePath& websiteDir)
   {
      FilePath sourceFile = websiteDir.completeChildPath("index.Rmd");
      if (!sourceFile.exists())
         sourceFile = websiteDir.completeChildPath("index.rmd");
      if (!sourceFile.exists())
         sourceFile = websiteDir.completeChildPath("index.md");
      if (!sourceFile.exists())
         sourceFile = websiteDir.completeChildPath("index.qmd");
      if (sourceFile.exists())
         return sourceFile;
      else
         return FilePath();
   }

   void terminateWithErrorStatus(int exitStatus)
   {
      boost::format fmt("\nExited with status %1%.\n\n");
      enqueBuildOutput(module_context::kCompileOutputError,
                       boost::str(fmt % exitStatus));
      enqueBuildCompleted();
   }
   
   void terminateWithErrorImpl(const std::string& message)
   {
      enqueBuildOutput(module_context::kCompileOutputError, message);
      enqueBuildCompleted();
   }

   void terminateWithError(const std::string& context,
                           const Error& error)
   {
      std::string msg = string_utils::sprintf(
               "Error %s: %s\n",
               context.c_str(),
               error.getSummary().c_str());
      terminateWithErrorImpl(msg);
   }

   void terminateWithError(const std::string& message)
   {
      std::string msg = core::string_utils::sprintf(
               "Error: %s\n",
               message.c_str());
      terminateWithErrorImpl(msg);
   }

   bool useDevtools()
   {
      return projectConfig().packageUseDevtools &&
             module_context::isMinimumDevtoolsInstalled();
   }

   bool cleanBeforeInstall()
   {
      return projectConfig().packageCleanBeforeInstall;
   }

public:
   virtual ~Build() = default;

   bool isRunning() const { return isRunning_; }

   const std::string& errorsBaseDir() const { return errorsBaseDir_; }
   const json::Array& errorsAsJson() const { return errorsJson_; }
   json::Array outputAsJson() const
   {
      json::Array outputJson;
      std::transform(output_.begin(),
                     output_.end(),
                     std::back_inserter(outputJson),
                     module_context::compileOutputAsJson);
      return outputJson;
   }
   const std::string type() const { return type_; }

   std::string outputAsText()
   {
      std::string output;
      for (const module_context::CompileOutput& compileOutput : output_)
      {
         output.append(compileOutput.output);
      }
      return output;
   }

   void terminate()
   {
      enqueBuildOutput(module_context::kCompileOutputNormal, "\n");
      terminationRequested_ = true;
   }

private:
   bool onContinue()
   {
      return !terminationRequested_;
   }

   void outputWithFilter(const std::string& output)
   {
      // split into lines
      std::vector<std::string> lines;
      boost::algorithm::split(lines, output,  boost::algorithm::is_any_of("\n"));

      // apply filter to each line
      size_t size = lines.size();
      for (size_t i = 0; i < size; i++)
      {
         // apply filter
         using namespace module_context;
         std::string line = lines.at(i);
         int type = errorOutputFilterFunction_(line) ?
                                 kCompileOutputError : kCompileOutputNormal;

         // add newline if this wasn't the last line
         if (i != (size-1))
            line.append("\n");

         // enque the output
         enqueBuildOutput(type, line);
      }
   }

   void onStandardOutput(const std::string& output)
   {
      if (errorOutputFilterFunction_)
         outputWithFilter(output);
      else
         enqueBuildOutput(module_context::kCompileOutputNormal, output);
   }

   void onStandardError(const std::string& output)
   {
      if (errorOutputFilterFunction_)
         outputWithFilter(output);
      else
         enqueBuildOutput(module_context::kCompileOutputError, output);
   }

   void onCompleted(int exitStatus)
   {
      using namespace module_context;

      // call the error parser if one has been specified
      if (errorParser_)
      {
         std::string output = outputAsText();
         
         // remove the testthat summary, since that will cause us to duplicate markers
         // we don't compile Boost.Regex with unicode support, so we just use the raw
         // hexadecimal escapes for the characters here
         // https://github.com/rstudio/rstudio/issues/14564
         boost::smatch match;
         boost::regex reResults("(?:\\xe2\\x95\\x90|\\x3d)+\\s+" kAnsiEscapeRegex "Results");
         if (boost::regex_search(output, match, reResults))
         {
            output = output.substr(0, match.position());
         }
         
         std::vector<SourceMarker> errors = errorParser_(output);
         if (!errors.empty())
         {
            errorsJson_ = sourceMarkersAsJson(errors);
            enqueBuildErrors(errorsJson_);
         }
      }

      if (exitStatus != EXIT_SUCCESS)
      {
         boost::format fmt("\nExited with status %1%.\n\n");
         enqueBuildOutput(kCompileOutputError, boost::str(fmt % exitStatus));

         // if this is a package build then check for ability to
         // build C++ code at all
         if (!pkgInfo_.empty() && !module_context::canBuildCpp())
         {
            // prompted install of Rtools on Windows (but don't prompt if
            // we used devtools since it likely has it's own prompt)
#ifdef _WIN32
            if (!usedDevtools_)
               module_context::installRBuildTools("Building R packages");
#endif
         }

         // if this is a package build then try to clean up a left
         // behind 00LOCK directory. note that R uses the directory name
         // and not the actual package name for the lockfile (and these can
         // and do differ in some cases)
         if (!pkgInfo_.empty() && !libPaths_.empty())
         {
            std::string pkgFolder = projects::projectContext().buildTargetPath().getFilename();
            FilePath libPath = libPaths_[0];
            FilePath lockPath = libPath.completeChildPath("00LOCK-" + pkgFolder);
            lockPath.removeIfExists();
         }
         
         // never restart R after a failed build
         restartR_ = false;

         // take other actions
         if (failureFunction_)
            failureFunction_();
      }
      else
      {
         if (!successMessage_.empty())
            enqueBuildOutput(kCompileOutputNormal, successMessage_ + "\n");

         if (successFunction_)
            successFunction_();
      }

      enqueBuildCompleted();
   }

   void enqueBuildOutput(int type, const std::string& output)
   {
      module_context::CompileOutput compileOutput(type, output);

      output_.push_back(compileOutput);

      ClientEvent event(client_events::kBuildOutput,
                        compileOutputAsJson(compileOutput));

      module_context::enqueClientEvent(event);
   }

   void enqueCommandString(const std::string& cmd)
   {
      enqueBuildOutput(module_context::kCompileOutputCommand,
                       "==> " + cmd + "\n\n");
   }

   void enqueBuildErrors(const json::Array& errors)
   {
      json::Object jsonData;
      jsonData["base_dir"] = errorsBaseDir_;
      jsonData["errors"] = errors;
      jsonData["open_error_list"] = openErrorList_;
      jsonData["type"] = type_;

      ClientEvent event(client_events::kBuildErrors, jsonData);
      module_context::enqueClientEvent(event);
   }

   std::string parseLibrarySwitchFromInstallArgs()
   {
      std::string libPath;

      std::string extraArgs = projectConfig().packageInstallArgs;
      std::size_t n = extraArgs.size();
      std::size_t index = extraArgs.find("--library=");

      if (index != std::string::npos &&
          index < n - 2) // ensure some space for path
      {
         std::size_t startIndex = index + std::string("--library=").length();
         std::size_t endIndex = startIndex + 1;

         // The library path can be specified with quotes + spaces, or without
         // quotes (but no spaces), so handle both cases.
         char firstChar = extraArgs[startIndex];
         if (firstChar == '\'' || firstChar == '\"')
         {
            while (++endIndex < n)
            {
               // skip escaped characters
               if (extraArgs[endIndex] == '\\')
               {
                  ++endIndex;
                  continue;
               }

               if (extraArgs[endIndex] == firstChar)
                  break;
            }

            libPath = extraArgs.substr(startIndex + 1, endIndex - startIndex - 1);
         }
         else
         {
            while (++endIndex < n)
            {
               if (isspace(extraArgs[endIndex]))
                  break;
            }
            libPath = extraArgs.substr(startIndex, endIndex - startIndex + 1);
         }
      }
      return libPath;
   }
   
   void enqueBuildCompleted()
   {
      isRunning_ = false;

      if (!buildToolsWarning_.empty())
      {
         enqueBuildOutput(module_context::kCompileOutputError,
                          buildToolsWarning_ + "\n\n");
      }

      // enque event
      std::string afterRestartCommand;
      if (restartR_)
      {
         afterRestartCommand = "library(" + pkgInfo_.name();
         
         // if --library="" was specified and we're not in devmode,
         // use it
         if (!(r::session::utils::isPackratModeOn() ||
               r::session::utils::isDevtoolsDevModeOn()))
         {
            std::string libPath = parseLibrarySwitchFromInstallArgs();
            if (!libPath.empty())
               afterRestartCommand += ", lib.loc = \"" + libPath + "\"";
         }
         
         afterRestartCommand += ")";
      }
      json::Object dataJson;
      dataJson["restart_r"] = restartR_;
      dataJson["after_restart_command"] = afterRestartCommand;
      dataJson["built_package_path"] = builtPackagePath_;
      ClientEvent event(client_events::kBuildCompleted, dataJson);
      module_context::enqueClientEvent(event);
   }

   const r_util::RProjectConfig& projectConfig()
   {
      return projects::projectContext().config();
   }

   std::string buildPackageSuccessMsg(const std::string& type)
   {
      FilePath writtenPath = projects::projectContext().buildTargetPath();
      if (!session::options().packageOutputInPackageFolder())
         writtenPath = writtenPath.getParent();
      std::string written = module_context::createAliasedPath(writtenPath);
      if (written == "~")
         written = writtenPath.getAbsolutePath();

      return type + " package written to " + written;
   }

   void initErrorParser(const FilePath& baseDir, CompileErrorParser parser)
   {
      // set base dir -- make sure it ends with a / so the slash is
      // excluded from error display
      errorsBaseDir_ = module_context::createAliasedPath(baseDir);
      if (!errorsBaseDir_.empty() &&
          !boost::algorithm::ends_with(errorsBaseDir_, "/"))
      {
         errorsBaseDir_.append("/");
      }

      errorParser_ = parser;
   }

private:
   bool isRunning_;
   bool terminationRequested_;
   std::vector<module_context::CompileOutput> output_;
   CompileErrorParser errorParser_;
   std::string errorsBaseDir_;
   json::Array errorsJson_;
   r_util::RPackageInfo pkgInfo_;
   projects::RProjectBuildOptions options_;
   std::string builtPackagePath_;
   std::vector<FilePath> libPaths_;
   std::string successMessage_;
   std::string buildToolsWarning_;
   boost::function<void()> successFunction_;
   boost::function<void()> failureFunction_;
   boost::function<bool(const std::string&)> errorOutputFilterFunction_;
   bool restartR_;
   bool usedDevtools_;
   bool openErrorList_;
   std::string type_;
};

boost::shared_ptr<Build> s_pBuild;


bool isBuildRunning()
{
   return s_pBuild && s_pBuild->isRunning();
}

Error startBuild(const json::JsonRpcRequest& request,
                 json::JsonRpcResponse* pResponse)
{
   // get type
   std::string type, subType;
   Error error = json::readParams(request.params, &type, &subType);
   if (error)
      return error;

   // if we have a build already running then just return false
   if (isBuildRunning())
   {
      pResponse->setResult(false);
   }
   else
   {
      s_pBuild = Build::create(type, subType);
      pResponse->setResult(true);
   }

   return Success();
}



Error terminateBuild(const json::JsonRpcRequest& /*request*/,
                     json::JsonRpcResponse* pResponse)
{
   if (isBuildRunning())
      s_pBuild->terminate();

   pResponse->setResult(true);

   return Success();
}

Error getCppCapabilities(const json::JsonRpcRequest& /*request*/,
                         json::JsonRpcResponse* pResponse)
{
   json::Object capsJson;
   capsJson["can_build"] = module_context::canBuildCpp();
   capsJson["can_source_cpp"] = module_context::haveRcppAttributes();
   pResponse->setResult(capsJson);

   return Success();
}

Error installBuildTools(const json::JsonRpcRequest& request,
                        json::JsonRpcResponse* pResponse)
{
   // get param
   std::string action;
   Error error = json::readParam(request.params, 0, &action);
   if (error)
      return error;

   pResponse->setResult(module_context::installRBuildTools(action));

   return Success();
}

Error devtoolsLoadAllPath(const json::JsonRpcRequest& /*request*/,
                     json::JsonRpcResponse* pResponse)
{
   pResponse->setResult(module_context::pathRelativeTo(
            module_context::safeCurrentPath(),
            projects::projectContext().buildTargetPath()));

   return Success();
}


struct BuildContext
{
   bool empty() const { return errors.isEmpty() && outputs.isEmpty(); }
   std::string errorsBaseDir;
   json::Array errors;
   json::Array outputs;
   std::string type;
};

BuildContext s_suspendBuildContext;


void writeBuildContext(const BuildContext& buildContext,
                       core::Settings* pSettings)
{
   pSettings->set("build-last-outputs", buildContext.outputs.write());
   pSettings->set("build-last-errors", buildContext.errors.write());
   pSettings->set("build-last-errors-base-dir", buildContext.errorsBaseDir);
}

void onSuspend(core::Settings* pSettings)
{
   if (s_pBuild)
   {
      BuildContext buildContext;
      buildContext.outputs = s_pBuild->outputAsJson();
      buildContext.errors = s_pBuild->errorsAsJson();
      buildContext.errorsBaseDir = s_pBuild->errorsBaseDir();
      buildContext.type = s_pBuild->type();
      writeBuildContext(buildContext, pSettings);
   }
   else if (!s_suspendBuildContext.empty())
   {
      writeBuildContext(s_suspendBuildContext, pSettings);
   }
   else
   {
      BuildContext emptyBuildContext;
      writeBuildContext(emptyBuildContext, pSettings);
   }
}

void onResume(const core::Settings& settings)
{
   std::string buildLastOutputs = settings.get("build-last-outputs");
   if (!buildLastOutputs.empty())
   {
      json::Value outputsJson;
      if (!outputsJson.parse(buildLastOutputs) &&
          json::isType<json::Array>(outputsJson))
      {
         s_suspendBuildContext.outputs = outputsJson.getValue<json::Array>();
      }
   }

   s_suspendBuildContext.errorsBaseDir = settings.get("build-last-errors-base-dir");
   std::string buildLastErrors = settings.get("build-last-errors");
   if (!buildLastErrors.empty())
   {
      json::Value errorsJson;
      if (!errorsJson.parse(buildLastErrors) &&
          json::isType<json::Array>(errorsJson))
      {
         s_suspendBuildContext.errors = errorsJson.getValue<json::Array>();
      }
   }
}


SEXP rs_canBuildCpp()
{
   r::sexp::Protect rProtect;
   return r::sexp::create(module_context::canBuildCpp(), &rProtect);
}

std::string s_previousPath;
SEXP rs_restorePreviousPath()
{
#ifdef _WIN32
    if (!s_previousPath.empty())
        core::system::setenv("PATH", s_previousPath);
    s_previousPath.clear();
#endif
    return R_NilValue;
}

SEXP rs_addRToolsToPath()
{
#ifdef _WIN32
    s_previousPath = core::system::getenv("PATH");
    std::string newPath = s_previousPath;
    std::string warningMsg;
    bool result = module_context::addRtoolsToPathIfNecessary(&newPath, &warningMsg);
    if (!warningMsg.empty())
    {
       console_output::writeLine(
          console_output::OutputStreamStderr,
          warningMsg,
          console_output::OutputTypeWarning);
    }
    core::system::setenv("PATH", newPath);
    r::sexp::Protect protect;
    return r::sexp::create(result, &protect);
#endif
    return R_NilValue;
}

#ifdef _WIN32

SEXP rs_installBuildTools()
{
   Error error = installRtools();
   if (error)
      LOG_ERROR(error);

   return R_NilValue;
}

#elif __APPLE__

SEXP rs_installBuildTools()
{
   if (module_context::isMacOS())
   {
      if (!module_context::hasMacOSCommandLineTools())
      {
         core::system::ProcessResult result;
         Error error = core::system::runCommand(
                  "/usr/bin/xcode-select --install",
                  core::system::ProcessOptions(),
                  &result);
         if (error)
            LOG_ERROR(error);
      }
   }
   else
   {
      ClientEvent event = browseUrlEvent(
          "https://www.rstudio.org/links/install_osx_build_tools");
      module_context::enqueClientEvent(event);
   }
   return R_NilValue;
}

#else

SEXP rs_installBuildTools()
{
   return R_NilValue;
}

#endif


SEXP rs_installPackage(SEXP pkgPathSEXP, SEXP libPathSEXP)
{
   using namespace rstudio::r::sexp;
   Error error = module_context::installPackage(safeAsString(pkgPathSEXP),
                                                safeAsString(libPathSEXP));
   if (error)
   {
      std::string desc = error.getProperty("description");
      if (!desc.empty())
         module_context::consoleWriteError(desc + "\n");
      LOG_ERROR(error);
   }

   return R_NilValue;
}

Error getBookdownFormats(const json::JsonRpcRequest& /*request*/,
                         json::JsonRpcResponse* pResponse)
{
   json::Object responseJson;
   responseJson["output_format"] = projects::projectContext().buildOptions().websiteOutputFormat;
   responseJson["website_output_formats"] = projects::websiteOutputFormatsJson();
   pResponse->setResult(responseJson);
   return Success();
}



} // anonymous namespace

json::Value buildStateAsJson()
{
   if (s_pBuild)
   {
      json::Object stateJson;
      stateJson["running"] = s_pBuild->isRunning();
      stateJson["outputs"] = s_pBuild->outputAsJson();
      stateJson["errors_base_dir"] = s_pBuild->errorsBaseDir();
      stateJson["type"] = s_pBuild->type();
      stateJson["errors"] = s_pBuild->errorsAsJson();
      return std::move(stateJson);
   }
   else if (!s_suspendBuildContext.empty())
   {
      json::Object stateJson;
      stateJson["running"] = false;
      stateJson["outputs"] = s_suspendBuildContext.outputs;
      stateJson["errors_base_dir"] = s_suspendBuildContext.errorsBaseDir;
      stateJson["type"] = s_suspendBuildContext.type;
      stateJson["errors"] = s_suspendBuildContext.errors;
      return std::move(stateJson);
   }
   else
   {
      return json::Value();
   }
}

namespace {

void manageUserMakevars()
{
   // NOTE: previously, when Apple machines were transitioning from the
   // use of gcc to clang, we wrote a custom ~/.R/Makevars file for users
   // that would ensure R uses clang during compilation. unfortunately,
   // this causes issues with newer releases of R as those releases will
   // encode extra flags (e.g. the default C++ standard) directly into
   // the CXX make variable. to recover, we now remove ~/.R/Makevars if
   // it exists and contains only the stubs that we added
   // https://github.com/rstudio/rstudio/issues/8800
   // to clang if necessary
   using namespace module_context;
   
   // nothing to do on non-macOS platforms
   if (!isMacOS())
      return;
   
   // check for existing ~/.R/Makevars file
   FilePath makevarsPath = userHomePath().completeChildPath(".R/Makevars");
   if (makevarsPath.exists())
   {
      std::string contents;
      Error error = core::readStringFromFile(makevarsPath, &contents);
      if (error)
         LOG_ERROR(error);
      
      // trim whitespace
      contents = core::string_utils::trimWhitespace(contents);
      
      // if this is the old stub generated by RStudio, remove it
      std::string makevars = "CC=clang\nCXX=clang++";
      if (contents == makevars)
      {
         error = makevarsPath.remove();
         if (error)
            LOG_ERROR(error);
      }
   }
   
}
   
} // end anonymous namespace

void onDeferredInit(bool newSession)
{
   manageUserMakevars();
}

Error initialize()
{
   // register .Call methods
   RS_REGISTER_CALL_METHOD(rs_canBuildCpp);
   RS_REGISTER_CALL_METHOD(rs_addRToolsToPath);
   RS_REGISTER_CALL_METHOD(rs_restorePreviousPath);
   RS_REGISTER_CALL_METHOD(rs_installPackage);
   RS_REGISTER_CALL_METHOD(rs_installBuildTools);

   // subscribe to deferredInit for build tools fixup
   module_context::events().onDeferredInit.connect(onDeferredInit);

   // subscribe to file monitor and source editor file saved so we
   // can tickle a flag to indicates when we should force an R
   // package rebuild
   session::projects::FileMonitorCallbacks cb;
   cb.onFilesChanged = onFilesChanged;
   projects::projectContext().subscribeToFileMonitor("", cb);
   module_context::events().onSourceEditorFileSaved.connect(onSourceEditorFileSaved);

   // add suspend handler
   addSuspendHandler(module_context::SuspendHandler(boost::bind(onSuspend, _2),
                                                    onResume));

   // install rpc methods
   using boost::bind;
   using namespace module_context;
   ExecBlock initBlock;
   initBlock.addFunctions()
      (bind(registerRpcMethod, "start_build", startBuild))
      (bind(registerRpcMethod, "terminate_build", terminateBuild))
      (bind(registerRpcMethod, "get_cpp_capabilities", getCppCapabilities))
      (bind(registerRpcMethod, "install_build_tools", installBuildTools))
      (bind(registerRpcMethod, "devtools_load_all_path", devtoolsLoadAllPath))
      (bind(registerRpcMethod, "get_bookdown_formats", getBookdownFormats))
      (bind(sourceModuleRFile, "SessionBuild.R"))
      (bind(sourceModuleRFile, "SessionInstallRtools.R"))
      (bind(source_cpp::initialize));
   return initBlock.execute();
}


} // namespace build
} // namespace modules

namespace module_context {

#ifdef __APPLE__
namespace {

bool usingSystemMake()
{
   return findProgram("make").getAbsolutePath() == "/usr/bin/make";
}

} // anonymous namespace
#endif

bool haveRcppAttributes()
{
   return module_context::isPackageVersionInstalled("Rcpp", "0.10.1");
}

bool canBuildCpp()
{
   if (s_canBuildCpp)
      return true;

#ifdef __APPLE__
   // NOTE: on macOS, R normally requests user install and use its own
   // LLVM toolchain; however, that toolchain still needs to re-use
   // system headers provided by the default macOS toolchain, and so
   // we still want to check for macOS command line tools here
   if (isMacOS() &&
       usingSystemMake() &&
       !hasMacOSCommandLineTools())
   {
      return false;
   }
#endif

   // try to build a simple c file to test whether we have build tools available
   FilePath cppPath = module_context::tempFile("test", "c");
   Error error = core::writeStringToFile(cppPath, "void test() {}\n");
   if (error)
   {
      LOG_ERROR(error);
      return false;
   }

   // get R bin directory
   FilePath rBinDir;
   error = module_context::rBinDir(&rBinDir);
   if (error)
   {
      LOG_ERROR(error);
      return false;
   }

   // try to run build tools
   RCommand rCmd(rBinDir);
   rCmd << "SHLIB";
   rCmd << cppPath.getFilename();

   core::system::ProcessOptions options;
   options.workingDir = cppPath.getParent();
   core::system::Options childEnv;
   core::system::environment(&childEnv);
   std::string warningMsg;
   module_context::addRtoolsToPathIfNecessary(&childEnv, &warningMsg);
   options.environment = childEnv;

   core::system::ProcessResult result;
   error = core::system::runCommand(rCmd.shellCommand(), options, &result);
   if (error)
   {
      LOG_ERROR(error);
      return false;
   }

   if (result.exitStatus != EXIT_SUCCESS)
   {
      checkXcodeLicense();
      return false;
   }
   
   s_canBuildCpp = true;
   return true;
}

bool installRBuildTools(const std::string& action)
{
#if defined(_WIN32) || defined(__APPLE__)
   r::exec::RFunction check(".rs.installBuildTools", action);
   bool userConfirmed = false;
   Error error = check.call(&userConfirmed);
   if (error)
      LOG_ERROR(error);
   return userConfirmed;
#else
   return false;
#endif
}

}

} // namespace session
} // namespace rstudio

