// Copyright 2021 The IREE Authors
//
// Licensed under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

#include "compiler/plugins/target/LLVMCPU/LinkerTool.h"
#include "iree/compiler/Utils/ToolUtils.h"
#include "llvm/ADT/StringExtras.h"
#include "llvm/IR/Function.h"
#include "llvm/IR/IRBuilder.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/FormatVariadic.h"

#define DEBUG_TYPE "llvm-linker"

namespace mlir::iree_compiler::IREE::HAL {

// Embedded ELF linker targeting IREE's ELF loader (or Android/Linux).
// This uses lld exclusively (though it can be overridden) as that lets us
// ensure we are consistently generating ELFs such that they can be used
// across our target platforms and with our loader.
//
// For consistency we follow the Linux ABI rules on all architectures and
// limit what we allow:
// - Triples of the form "{arch}-pc-linux-elf" only.
// - No builtin libraries are available.
// - Extra GNU-style symbol lookups are disabled (sysv only) to save binary
//   size. The loader does not use any hash tables but .hash is mandatory in
//   the spec and included for compatibility.
// - No lazy binding; all symbols must be resolved on load.
// - GNU_RELRO is optional but used here as we don't support lazy binding.
//
// We allow debug information to be included in the ELFs however we don't
// currently have a use for it at runtime. When unstripped we can possibly feed
// it to tools or use it ourselves to generate backtraces but since all release
// usage should be stripped nothing relies upon it.
class EmbeddedLinkerTool : public LinkerTool {
public:
  using LinkerTool::LinkerTool;

  std::string getEmbeddedToolPath() const {
    // Always try to use the tool specified for this exact configuration first.
    // Hopefully some day soon we'll be able to statically link LLD in and call
    // a C function to do the linking instead of needing a separate tool.
    if (!targetOptions.embeddedLinkerPath.empty()) {
      return targetOptions.embeddedLinkerPath;
    }

    // Fall back to check for setting the linker explicitly via environment
    // variables.
    char *envVarPath = std::getenv("IREE_LLVM_EMBEDDED_LINKER_PATH");
    if (envVarPath && envVarPath[0] != '\0')
      return std::string(envVarPath);

    // No explicit linker specified, search the install/build dir or env.
    const SmallVector<std::string> &toolNames{"iree-lld", "lld", "ld.lld",
                                              "lld-link"};
    std::string toolPath = findTool(toolNames);
    if (!toolPath.empty())
      return toolPath;

    llvm::errs()
        << "error: required embedded linker tool (typically `lld`) not found "
           "after searching:\n"
           "  * --iree-llvmcpu-embedded-linker-path= flag\n"
           "  * IREE_LLVM_EMBEDDED_LINKER_PATH environment variable\n"
           "  * common locations at relative file paths\n"
           "  * system PATH\n"
           "Run with --debug-only=llvm-linker for search details\n";
    return "";
  }

  LogicalResult
  configureModule(llvm::Module *llvmModule,
                  ArrayRef<llvm::Function *> exportedFuncs) override {
    for (auto &llvmFunc : *llvmModule) {
      // -fno-plt - prevent PLT on calls to imports.
      llvmFunc.addFnAttr("nonlazybind");

      // No unwind tables are required as we don't support exceptions.
      llvmFunc.setUWTableKind(llvm::UWTableKind::None);
    }

    // Since all exports are fetched via the executable library query function
    // we don't need to make them publicly exported on the module. This has
    // the downside of making some tooling (disassemblers/decompilers/binary
    // size analysis tools/etc) a bit harder to work with, though, so when
    // compiling in debug mode we export all the functions.
    if (targetOptions.target.debugSymbols) {
      for (auto llvmFunc : exportedFuncs) {
        llvmFunc->setVisibility(
            llvm::GlobalValue::VisibilityTypes::DefaultVisibility);
        llvmFunc->setLinkage(llvm::GlobalValue::LinkageTypes::ExternalLinkage);

        // In debug modes we need unwind tables in order to get proper stacks on
        // all platforms. We don't support exceptions so unless debugging is
        // requested we omit them to reduce code size.
        llvmFunc->setUWTableKind(llvm::UWTableKind::Default);
      }
    }

    return success();
  }

  std::optional<Artifacts>
  linkDynamicLibrary(StringRef libraryName,
                     ArrayRef<Artifact> objectFiles) override {
    Artifacts artifacts;

    // Create the shared object name; if we only have a single input object we
    // can just reuse that.
    if (!objectFiles.empty()) {
      artifacts.libraryFile =
          Artifact::createVariant(objectFiles.front().path, "so");
    } else {
      artifacts.libraryFile = Artifact::createTemporary(libraryName, "so");
    }
    artifacts.libraryFile.close();

    std::string embeddedToolPath = getEmbeddedToolPath();
    if (embeddedToolPath.empty())
      return std::nullopt;

    SmallVector<std::string, 8> flags = {
        embeddedToolPath,

        // Forces LLD to act like gnu ld and produce ELF files.
        // If not specified then lld tries to figure out what it is by progname
        // (ld, ld64, link, etc).
        // NOTE: must be first because lld sniffs argv[1]/argv[2].
        "-flavor gnu",

        "-o " + artifacts.libraryFile.path,
    };

    // Hide build info that makes files unreproducable.
    flags.push_back("--build-id=none");

    // Avoids including any libc/startup files that initialize the CRT as
    // we don't use any of that. Our shared libraries must be freestanding.
    flags.push_back("-nostdlib"); // -nodefaultlibs + -nostartfiles

    // Statically link all dependencies so we don't have any runtime deps.
    // We cannot have any imports in the module we produce.
    flags.push_back("-static");

    // Creating a hermetic shared library.
    flags.push_back("-shared");
    flags.push_back("--no-undefined");
    flags.push_back("--no-allow-shlib-undefined");

    // Workaround for LLD weirdness on Windows; for some reason we get symbol
    // conflicts only on Windows starting after
    // https://github.com/llvm/llvm-project/commit/83d59e05b201760e3f364ff6316301d347cbad95
    flags.push_back("--allow-multiple-definition");

    // Drop unused sections.
    flags.push_back("--gc-sections");

    // Hardening (that also makes runtime linking easier):
    // - bind all import symbols during load
    // - make all relocations readonly.
    // See: https://blog.quarkslab.com/clang-hardening-cheat-sheet.html
    flags.push_back("-z now");
    flags.push_back("-z relro");

    // Strip local symbols; we only care about the global ones for lookup.
    // This shrinks the .symtab to a single entry.
    flags.push_back("--discard-all");

    // Identical code folding.
    flags.push_back("--icf=all");

    // To aid ICF we allow functions and data to be aliased - we never expose
    // pointers to our internal functions and don't care if they alias.
    flags.push_back("--ignore-data-address-equality");
    flags.push_back("--ignore-function-address-equality");

    // Use sysv .hash lookup table only; we have literally a single symbol and
    // the .gnu.hash overhead is not worth it (either in the ELF or in the
    // runtime loader).
    flags.push_back("--hash-style=sysv");

    // Strip debug information (only, no relocations) when not requested.
    if (!targetOptions.target.debugSymbols) {
      flags.push_back("--strip-debug");
    }

    // Link all input objects. Note that we are not linking whole-archive as
    // we want to allow dropping of unused codegen outputs.
    for (auto &objectFile : objectFiles) {
      flags.push_back(objectFile.path);
    }

    // LLD inserts its own identifier unless the LLD_VERSION env var is set:
    // third_party/llvm-project/lld/ELF/SyntheticSections.cpp
    if (failed(runLinkCommand(llvm::join(flags, " "), "LLD_VERSION=IREE"))) {
      // Ensure we save inputs if we fail so that the user can replicate the
      // command themselves.
      if (targetOptions.keepLinkerArtifacts) {
        for (auto &objectFile : objectFiles) {
          if (objectFile.outputFile) {
            llvm::errs() << "linker input preserved: "
                         << objectFile.outputFile->getFilename();
            objectFile.keep();
          }
        }
      }
      return std::nullopt;
    }
    return artifacts;
  }
};

std::unique_ptr<LinkerTool>
createEmbeddedLinkerTool(const llvm::Triple &targetTriple,
                         LLVMTargetOptions &targetOptions) {
  return std::make_unique<EmbeddedLinkerTool>(targetTriple, targetOptions);
}

} // namespace mlir::iree_compiler::IREE::HAL
