/*
 * Copyright 2019 Bloomberg Finance LP
 *
 * 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.
 */

#include <buildboxrun_bubblewrap.h>

#include <buildboxcommon_fileutils.h>
#include <buildboxcommon_grpcerror.h>
#include <buildboxcommon_logging.h>
#include <buildboxcommon_mergeutil.h>
#include <buildboxcommon_platformutils.h>
#include <buildboxcommon_remoteexecutionclient.h>
#include <buildboxcommon_runner.h>
#include <buildboxcommon_stringutils.h>
#include <buildboxcommon_systemutils.h>

#ifdef OCI
#include <buildboxcommon_ociclient.h>
#include <ThreadPool.h>
#endif

#include <grpcpp/grpcpp.h>

#include <filesystem>
#include <stdlib.h>
#include <sys/utsname.h>
#include <tuple>
#include <unistd.h>

/* Not defined in any header per POSIX */
extern "C" char **environ;
#ifdef OCI
constexpr int DEFAULT_MAX_NUM_LAYER_THREADS = 8;
#endif

namespace buildboxcommon {
namespace buildboxrun {
namespace bubblewrap {

void BubbleWrapRunner::fetchChrootIntoLocalCas(const Digest &digest) const
{
    BUILDBOX_RUNNER_LOG(INFO, "Calling LocalCAS.FetchTree(chrootDigest="
                                  << digest
                                  << ") to make sure that the chroot is "
                                     "available from Local CAS");

    try {
        const bool fetchFiles = true;
        d_casClient->fetchTree(digest, fetchFiles);
    }
    catch (const buildboxcommon::GrpcError &e) {
        BUILDBOX_RUNNER_LOG(ERROR, "LocalCAS.FetchTree(chrootDigest="
                                       << digest
                                       << ") failed: " << e.status.error_code()
                                       << ": " << e.status.error_message());
        throw e;
    }
}

void BubbleWrapRunner::uploadMissingBlobs(const digest_string_map &data) const
{
    auto casClient = d_casClient;
    std::vector<Digest> digests;
    digests.reserve(data.size());
    for (const auto &digestBlob : data) {
        digests.push_back(digestBlob.first);
    }

    const std::vector<Digest> missingDigests =
        casClient->findMissingBlobs(digests);

    if (missingDigests.empty()) {
        return;
    }

    BUILDBOX_RUNNER_LOG(DEBUG, "Uploading " << missingDigests.size()
                                            << " missing blob(s) out of "
                                            << digests.size() << " total");

    std::vector<CASClient::UploadRequest> uploadRequests;
    uploadRequests.reserve(missingDigests.size());
    for (const auto &digest : missingDigests) {
        uploadRequests.emplace_back(digest, data.at(digest));
    }

    const std::vector<CASClient::UploadResult> results =
        casClient->uploadBlobs(uploadRequests);

    // verify that all blobs have been successfully uploaded
    // throw on any failures
    bool uploadSuccess = true;
    std::ostringstream oss;
    for (const auto &result : results) {
        if (!result.status.ok()) {
            oss << "Failed to upload a merged digest(" << result.digest
                << "), status = [" << result.status.error_code() << ": \""
                << result.status.error_message() << "\"]\n";
            uploadSuccess = false;
        }
    }

    if (!uploadSuccess) {
        BUILDBOXCOMMON_THROW_EXCEPTION(std::runtime_error, oss.str());
    }
}

void BubbleWrapRunner::mergeDigests(
    const Digest &inputDigest, const Digest &chrootDigest,
    Digest *mergedDigest,
    const Command_OutputDirectoryFormat &chrootDirectoryFormat)
{
    // acquire the input trees of both digests
    // getTree and fetchMessage will throw on error
    const DirectoryTree inputTree = this->d_casClient->getTree(inputDigest);

    DirectoryTree chrootTree;
    // If the chroot is avalble as a Tree Message download that
    if (chrootDirectoryFormat == Command::TREE_ONLY) {
        chrootTree = this->d_casClient->getTreeFromTreeMessage(chrootDigest);
    }
    // If not use getTree - Internally this will recursively download starting
    // from the root directory to build up the tree.
    else {
        chrootTree = this->d_casClient->getTree(chrootDigest);
    }

    digest_string_map mergedDirectoryBlobs;
    const Digest rootDigest =
        mergeTrees(inputTree, chrootTree, &mergedDirectoryBlobs);

    if (rootDigest == Digest()) {
        std::ostringstream errMsg;
        errMsg << "Error merging inputDigest(" << inputDigest
               << ") with chrootDigest(" << chrootDigest << ")";
        const grpc::Status grpcStatus(grpc::StatusCode::FAILED_PRECONDITION,
                                      errMsg.str());
        // Written to errorStatusFile in Runner::main()
        GrpcError::throwGrpcError(grpcStatus);
    }

    // Upload any missing digests to CAS so they can be staged
    uploadMissingBlobs(mergedDirectoryBlobs);

    *mergedDigest = rootDigest;
}

BubbleWrapRunner::BubbleWrapRunner(const std::string &bwrap_path) : Runner()
{
    if (bwrap_path != "") {
        d_bubblewrapBinPath = bwrap_path;
    }
    else {
        const std::string bubblewrapBin = "bwrap";

        /* Find the path to the bubblewrap binary. */
        d_bubblewrapBinPath = SystemUtils::getPathToCommand(bubblewrapBin);
        if (d_bubblewrapBinPath.empty()) {
            std::cerr << "Could not find bubblewrap command \""
                      << bubblewrapBin << "\"\n";
            exit(1);
        }
    }

    d_linux32Path = SystemUtils::getPathToCommand("linux32");

    d_userNamespaceAvailable = userNamespaceAvailable();

    d_enableTmpOutputs = false;

#ifdef OCI
    d_numParallelLayerThreads =
        std::min(static_cast<int>(std::thread::hardware_concurrency()),
                 DEFAULT_MAX_NUM_LAYER_THREADS);
#endif
}

bool BubbleWrapRunner::userNamespaceAvailable()
{
    const auto maxUserNamespacesPath = "/proc/sys/user/max_user_namespaces";

    if (!FileUtils::isRegularFile("/proc/self/ns/user") ||
        !FileUtils::isRegularFile(maxUserNamespacesPath)) {
        /* Kernel too old or user namespace support disabled in config */
        return false;
    }

    /* Check whether user namespaces are disabled by the distro or admin */
    const auto maxUserNamespaces =
        FileUtils::getFileContents(maxUserNamespacesPath);
    return maxUserNamespaces != "0\n";
}

const std::vector<std::string>
BubbleWrapRunner::generateCommandLine(const Command &command,
                                      const std::string &root_path,
                                      const Platform &platform)
{
    std::vector<std::string> bwrap_argv;

    std::string uid("0");
    std::string gid("0");
    bool network = false;
    bool linux32 = false;

    BindPaths roBinds;
    for (const auto &[srcPath, chrootPath] : d_roBindPaths) {
        roBinds.emplace_back(srcPath, chrootPath);
    }

    /* Process platform properties */
    for (const auto &property : platform.properties()) {
        if (property.name() == "OSFamily") {
            if (property.value() != PlatformUtils::getHostOSFamily()) {
                BUILDBOXCOMMON_THROW_EXCEPTION(std::runtime_error,
                                               "Unsupported OSFamily \"" +
                                                   property.value() + "\"");
            }
        }
        else if (property.name() == "ISA") {
            auto support = PlatformUtils::getISASupport(property.value());
            bool supported = false;

            if (support.supported) {
                switch (support.action) {
                    case PlatformUtils::ISA_SUPPORT_ACTION_NONE:
                        supported = true;
                        break;
                    case PlatformUtils::ISA_SUPPORT_ACTION_RUN_LINUX32:
                        supported = true;
                        linux32 = true;
                        break;
                    default:
                        break;
                }
            }
            if (!supported) {
                BUILDBOXCOMMON_THROW_EXCEPTION(std::runtime_error,
                                               "Unsupported ISA \"" +
                                                   property.value() + "\"");
            }
        }
        else if (property.name() == "unixUID") {
            if (!d_userNamespaceAvailable) {
                BUILDBOXCOMMON_THROW_EXCEPTION(
                    std::runtime_error, "User namespaces are not available. "
                                        "Cannot support unixUID.");
            }
            uid = property.value();
        }
        else if (property.name() == "unixGID") {
            if (!d_userNamespaceAvailable) {
                BUILDBOXCOMMON_THROW_EXCEPTION(
                    std::runtime_error, "User namespaces are not available. "
                                        "Cannot support unixGID.");
            }
            gid = property.value();
        }
        else if (property.name() == "network") {
            network = property.value() == "on";
        }
        // Injecting files indexed by platform properties
        else if (property.name() == "inject") {
            const auto &content = property.value();
            if (d_roBindPathsByProperties.find(content) ==
                d_roBindPathsByProperties.end()) {
                BUILDBOX_LOG_WARNING(
                    "Unrecognized content by property to inject: " << content);
                continue;
            }
            for (const auto &[srcPath, chrootPath] :
                 d_roBindPathsByProperties.at(content)) {
                BUILDBOX_LOG_DEBUG("Injecting content by property: "
                                   << content << ":" << srcPath << ":"
                                   << chrootPath);
                roBinds.emplace_back(srcPath, chrootPath);
            }
        }
        else if (property.name() == "capability") {
            if (!d_userNamespaceAvailable) {
                BUILDBOXCOMMON_THROW_EXCEPTION(
                    std::runtime_error, "User namespaces are not available. "
                                        "Cannot support adding capabilities.");
            }
            if (d_supportedCapabilities.find(property.value()) ==
                d_supportedCapabilities.end()) {
                BUILDBOXCOMMON_THROW_EXCEPTION(std::runtime_error,
                                               "Unsupported capability \"" +
                                                   property.value() + "\"");
            }
            d_addedCapabilities.push_back(property.value());
        }
    }

    if (linux32) {
        if (d_linux32Path.empty()) {
            BUILDBOXCOMMON_THROW_EXCEPTION(std::runtime_error,
                                           "Could not find linux32 command");
        }

        bwrap_argv.push_back(d_linux32Path);
    }

    bwrap_argv.push_back(d_bubblewrapBinPath);

    /* Create a new pid namespace, this also ensures that any subprocesses
     * are cleaned up when the bwrap process exits.
     */
    bwrap_argv.emplace_back("--unshare-pid");

    /* Ensure subprocesses are cleaned up when the bwrap parent dies. */
    bwrap_argv.emplace_back("--die-with-parent");

    /* Mount sandbox rootfs */
    bwrap_argv.emplace_back("--bind");
    bwrap_argv.push_back(root_path);
    bwrap_argv.emplace_back("/");

    if (!network) {
        /* Disable network access */
        bwrap_argv.emplace_back("--unshare-net");
        bwrap_argv.emplace_back("--unshare-uts");
        bwrap_argv.emplace_back("--hostname");
        bwrap_argv.emplace_back("buildbox");
        bwrap_argv.emplace_back("--unshare-ipc");
    }

    if (command.working_directory().compare("") != 0) {
        bwrap_argv.emplace_back("--dir");
        bwrap_argv.push_back(command.working_directory());
        bwrap_argv.emplace_back("--chdir");
        bwrap_argv.push_back(command.working_directory());
    }
    else {
        bwrap_argv.emplace_back("--chdir");
        bwrap_argv.emplace_back("/");
    }

    if (d_userNamespaceAvailable) {
        bwrap_argv.emplace_back("--unshare-user");
        bwrap_argv.emplace_back("--uid");
        bwrap_argv.push_back(uid);
        bwrap_argv.emplace_back("--gid");
        bwrap_argv.push_back(gid);
    }

    /* Don't leave any Linux capabilities to the sandboxed command when running
     * as privileged user, improving consistency when running in different host
     * environments. Contrary to the documentation of bubblewrap, this is not
     * the default. */
    bwrap_argv.emplace_back("--cap-drop");
    bwrap_argv.emplace_back("ALL");

    /* Add back any explicitly requested capabilities */
    for (const auto &capability : d_addedCapabilities) {
        bwrap_argv.emplace_back("--cap-add");
        bwrap_argv.emplace_back(capability);
    }

    /* Clear environment */
    // NOLINTBEGIN (cppcoreguidelines-pro-bounds-pointer-arithmetic)
    for (char **envp = environ; *envp; envp++) {
        const char *assign = strchr(*envp, '=');
        assert(assign);
        const std::string key(*envp, static_cast<size_t>(assign - *envp));

        bwrap_argv.emplace_back("--unsetenv");
        bwrap_argv.emplace_back(key);
    }
    // NOLINTEND (cppcoreguidelines-pro-bounds-pointer-arithmetic)

    /* Set environment for command process */
    for (const auto &env_var : command.environment_variables()) {
        bwrap_argv.emplace_back("--setenv");
        bwrap_argv.push_back(env_var.name());
        bwrap_argv.push_back(env_var.value());
    }

    /* Support proc */
    bwrap_argv.emplace_back("--proc");
    bwrap_argv.emplace_back("/proc");

    /* If /tmp has no contents and isn't referenced by an
       output path, mount it as tmpfs.
    */
    bool tmpCapture = false;
    if (d_enableTmpOutputs) {
        for (const auto &path : command.output_paths()) {
            const auto fullOutputPath = (std::filesystem::path{"/"} /
                                         command.working_directory() / path)
                                            .lexically_normal();
            // Skip past '/'
            const auto top_dir = ++(fullOutputPath.begin());
            if (*top_dir == "tmp") {
                tmpCapture = true;
                break;
            }
        }
    }
    const auto tmpPath = std::filesystem::path{root_path} / "tmp";
    if ((std::filesystem::is_directory(tmpPath) &&
         !std::filesystem::is_empty(tmpPath)) ||
        tmpCapture) {
        /* /tmp is used as input and/or output, use a regular directory */
        bwrap_argv.emplace_back("--dir");
        bwrap_argv.emplace_back("/tmp");
    }
    else {
        bwrap_argv.emplace_back("--tmpfs");
        bwrap_argv.emplace_back("/tmp");
    }

    /* Path injections via ro-bind  */
    for (const auto &[srcPath, chrootPath] : roBinds) {
        bwrap_argv.emplace_back("--ro-bind");
        bwrap_argv.push_back(srcPath);
        bwrap_argv.push_back(chrootPath);
    }

    /* Support devices */
    bwrap_argv.emplace_back("--dev");
    bwrap_argv.emplace_back("/dev");

    bwrap_argv.insert(bwrap_argv.end(), this->d_bindArguments.cbegin(),
                      this->d_bindArguments.cend());

    /* append command */
    auto &arguments = command.arguments();
    bwrap_argv.insert(bwrap_argv.end(), arguments.begin(), arguments.end());

    return bwrap_argv;
}

bool BubbleWrapRunner::parseArg(std::string_view arg_view)
{
    if (arg_view.size() >= 2 && arg_view.substr(0, 2) == "--") {
        arg_view.remove_prefix(2);
        size_t assign_pos = arg_view.find('=');
        if (assign_pos != std::string_view::npos) {
            std::string_view key = arg_view.substr(0, assign_pos);
            std::string_view valueView = arg_view.substr(assign_pos + 1);
            if (key == "bind-mount") {
                size_t colonPos = valueView.find(':');
                if (colonPos == std::string::npos) {
                    std::cerr << "Missing `:` in option " << arg_view << '\n';
                    return false;
                }

                std::string src(valueView.substr(0, colonPos));
                std::string dest(valueView.substr(colonPos + 1));

                /* Use --dev-bind to allow binding device nodes.
                 * It can be used for regular files and directories as
                 * well.
                 */
                this->d_bindArguments.emplace_back("--dev-bind");
                this->d_bindArguments.push_back(src);
                this->d_bindArguments.push_back(dest);

                return true;
            }
            else if (key == "ro-bind-mount") {
                const auto parts = StringUtils::split(valueView, ":");
                if (parts.size() == 2) {
                    std::filesystem::path src{parts[0]}, target{parts[1]};
                    if (std::filesystem::is_symlink(src)) {
                        src = std::filesystem::read_symlink(src);
                    }
                    this->d_roBindPaths.emplace_back(src, target);
                    return true;
                }
                else if (parts.size() == 3) {
                    const auto &content = parts[0];
                    std::filesystem::path src{parts[1]}, target{parts[2]};
                    if (std::filesystem::is_symlink(src)) {
                        src = std::filesystem::read_symlink(src);
                    }
                    this->d_roBindPathsByProperties[std::string(content)]
                        .emplace_back(src, target);
                    return true;
                }
                else {
                    BUILDBOX_LOG_WARNING(
                        "Invalid --ro-bind-mount option: " << valueView);
                    return false;
                }
            }
#ifdef OCI
            else if (key == "oci-auth-token") {
                d_ociAuthTokenPath = std::string(valueView);
                return true;
            }
            else if (key == "num-parallel-layer-threads") {
                try {
                    d_numParallelLayerThreads =
                        std::stoi(std::string(valueView));
                    return true;
                }
                catch (const std::exception &e) {
                    std::cerr
                        << "Invalid value for --num-parallel-layer-threads: "
                        << valueView << '\n';
                    return false;
                }
            }
#endif
        }
        else {
            if (arg_view == "enable-tmp-outputs") {
                this->d_enableTmpOutputs = true;
                return true;
            }
        }
    }

    return false;
}

void BubbleWrapRunner::printSpecialUsage()
{
    std::clog << "    --bind-mount=HOSTPATH:PATH  Bind mount file or "
                 "directory from host into sandbox\n";
    std::clog << "    --enable-tmp-outputs  Allow capturing outputs under "
                 "/tmp directory in the sandbox\n";
    std::clog
        << "    --ro-bind-mount=[CONTENT:]HOST_PATH:CHROOT_PATH   Bind a "
           "*read-only* path from HOST_PATH on the host filesystem to "
           "CHROOT_PATH inside the constructed chroot. If CONTENT is "
           "set, the bind only happens if the action has a property "
           "`inject=CONTENT`\n";
#ifdef OCI
    std::clog << "    --oci-auth-token=PATH  Path to authentication token "
                 "file for private registries\n";
    std::clog << "    --num-parallel-layer-threads=N  Set the number of "
                 "parallel threads for OCI layer processing. Default: min("
                 "hardware-concurrency, 8)\n";
#endif
}

void BubbleWrapRunner::printSpecialCapabilities()
{
    std::unordered_set<std::string> supported;
    std::cout << "bind-mount\n";
    std::cout << "platform:OSFamily=" << PlatformUtils::getHostOSFamily()
              << "\n";

    supported = PlatformUtils::getSupportedISAs();
    for (const auto &isa : supported) {
        std::cout << "platform:ISA=" << isa << "\n";
    }

    if (d_userNamespaceAvailable) {
        std::cout << "platform:unixUID\n";
        std::cout << "platform:unixGID\n";
        std::cout << "platform:capability\n";
    }

    std::cout << "platform:remoteApisSocketPath\n";
}

ActionResult BubbleWrapRunner::execute(const Command &command,
                                       const Digest &inputRootDigest,
                                       const Platform &platform)
{
    Digest digest(inputRootDigest);
    std::optional<Digest> chrootTreeDigest = hasChrootTreeDigest(platform);
    std::optional<Digest> chrootRootDigest = hasChrootRootDigest(platform);
    std::optional<std::string> containerImage = hasContainerImage(platform);

    bool hasChrootDigest =
        chrootTreeDigest.has_value() || chrootRootDigest.has_value();
    bool hasContainerImage = containerImage.has_value();

    if (hasChrootDigest && hasContainerImage) {
        BUILDBOXCOMMON_THROW_EXCEPTION(
            std::runtime_error,
            "Platform properties 'chrootTreeDigest', 'chrootRootDigest', and "
            "'container-image' are mutually exclusive");
    }

    ActionResult result;

    auto *result_metadata = result.mutable_execution_metadata();
    Runner::metadata_mark_input_download_start(result_metadata);
    if (chrootTreeDigest) {
        // fetchChrootIntoLocalCas() has been omitted here as it doesn't work
        // with tree digests and will be removed from the chrootRootDigest case
        // above in a future commit because pre-loading the chroot into local
        // CAS no longer provides the desired performance optimisation from
        // commit 60126ba9 as the tree cache was expanded to also cover
        // subdirectories in commit 3dca1f5d.

        Digest mergedDigest;
        mergeDigests(inputRootDigest, chrootTreeDigest.value(), &mergedDigest,
                     Command::TREE_ONLY);
        BUILDBOX_RUNNER_LOG(DEBUG, "inputRootDigest = "
                                       << inputRootDigest
                                       << ", chrootTreeDigest = "
                                       << chrootTreeDigest.value()
                                       << ", mergedDigest = " << mergedDigest);

        digest.Swap(&mergedDigest);
    }
    else if (chrootRootDigest) {
        if (d_use_localcas_protocol) {
            // Making sure that the chroot is pre-loaded in local CAS if
            // connected to one.
            fetchChrootIntoLocalCas(chrootRootDigest.value());
        }

        Digest mergedDigest;
        mergeDigests(inputRootDigest, chrootRootDigest.value(), &mergedDigest,
                     Command::DIRECTORY_ONLY);
        BUILDBOX_RUNNER_LOG(DEBUG, "inputRootDigest = "
                                       << inputRootDigest
                                       << ", chrootDigest = "
                                       << chrootRootDigest.value()
                                       << ", mergedDigest = " << mergedDigest);
        digest.Swap(&mergedDigest);
    }
    else if (containerImage) {
#ifdef OCI
        if (!d_assetClient) {
            BUILDBOXCOMMON_THROW_EXCEPTION(
                std::runtime_error,
                "Container image '"
                    << containerImage.value()
                    << "' specified but "
                       "no asset server configured. Asset server is required "
                       "for OCI image "
                       "support. Please configure --ra-remote in worker CLI.");
        }
        // The layer captures need localcas
        if (!d_use_localcas_protocol) {
            BUILDBOXCOMMON_THROW_EXCEPTION(
                std::runtime_error,
                "Container image '"
                    << containerImage.value()
                    << "' specified but "
                       "local CAS protocol is disabled. Local CAS is required "
                       "for OCI image support.");
        }
        // We only support linux and amd64/x86-64 in the ociclient
        // TODO: update to pass platform info to ociclient when we support it
        std::string platformOs = "linux";
        std::string platformArch = "x86-64";

        for (const auto &property : platform.properties()) {
            if (property.name() == "OSFamily") {
                platformOs = property.value();
            }
            else if (property.name() == "ISA") {
                platformArch = property.value();
            }
        }

        if (platformOs != "linux") {
            BUILDBOXCOMMON_THROW_EXCEPTION(std::runtime_error,
                                           "Unsupported OSFamily \""
                                               << platformOs
                                               << "\" for container image");
        }

        if (platformArch != "x86-64") {
            BUILDBOXCOMMON_THROW_EXCEPTION(std::runtime_error,
                                           "Unsupported ISA \""
                                               << platformArch
                                               << "\" for container image");
        }

        auto ociClient = createOciClient();

        // Create ThreadPool for parallel layer processing
        std::unique_ptr<ThreadPool> layerThreadPool;
        if (d_numParallelLayerThreads > 1) {
            layerThreadPool =
                std::make_unique<ThreadPool>(d_numParallelLayerThreads, "oci");
        }

        auto [containerTreeDigest, containerRootDigest] =
            ociClient->getImageDigests(containerImage.value(),
                                       layerThreadPool.get());
        std::ignore = containerRootDigest;
        Digest mergedDigest;
        mergeDigests(inputRootDigest, containerTreeDigest, &mergedDigest,
                     Command::TREE_ONLY);

        BUILDBOX_RUNNER_LOG(
            DEBUG, "inputRootDigest = "
                       << inputRootDigest
                       << ", containerImage = " << containerImage.value()
                       << ", containerTreeDigest = " << containerTreeDigest
                       << ", mergedDigest = " << mergedDigest);

        digest.Swap(&mergedDigest);
#else
        BUILDBOXCOMMON_THROW_EXCEPTION(
            std::runtime_error,
            "Container image support is not available. "
            "This build was compiled without OCI support.");
#endif
    }

    const auto staged_dir = this->stageDirectory(digest);
    Runner::metadata_mark_input_download_end(result_metadata);

    std::vector<std::string> commandLine =
        generateCommandLine(command, staged_dir->getPath(), platform);

    std::ostringstream working_directory;
    working_directory << staged_dir->getPath() << "/"
                      << command.working_directory();
    BUILDBOX_LOG_DEBUG("Running in " << working_directory.str());

    {
        createOutputDirectories(command, working_directory.str());
        BUILDBOX_LOG_DEBUG("Executing "
                           << logging::printableCommandLine(commandLine))
        executeAndStore(commandLine, &result);
    }

    if (!getSignalStatus()) {
        BUILDBOX_LOG_DEBUG("Capturing command outputs...");
        Runner::metadata_mark_output_upload_start(result_metadata);
        staged_dir->captureAllOutputs(command, &result);
        Runner::metadata_mark_output_upload_end(result_metadata);
        BUILDBOX_LOG_DEBUG("Finished capturing command outputs");
    }

    return result;
}

#ifdef OCI
std::unique_ptr<OciClient> BubbleWrapRunner::createOciClient() const
{
    auto httpClient = std::make_shared<HTTPClient>();

    return std::make_unique<OciClient>(httpClient, d_casClient, d_assetClient,
                                       std::nullopt, d_ociAuthTokenPath);
}
#endif

} // namespace bubblewrap
} // namespace buildboxrun
} // namespace buildboxcommon
