// Copyright 2020 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 "llvm/ADT/StringExtras.h"
#include "llvm/IR/Function.h"
#include "llvm/IR/IRBuilder.h"
#include "llvm/Support/FormatVariadic.h"
#include "llvm/TargetParser/Host.h"
#include "llvm/TargetParser/Triple.h"

#define DEBUG_TYPE "llvm-linker"

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

using llvm::Triple;

// Returns the canonical host name for the Android NDK prebuilt versions:
//   https://developer.android.com/ndk/guides/other_build_systems
//
// If we want to support self-built variants we'll need an env var (or just make
// the user set IREE_LLVM_SYSTEM_LINKER_PATH).
static const char *getNDKHostPlatform() {
  auto hostTriple = Triple(llvm::sys::getProcessTriple());
  if (hostTriple.isOSLinux() && hostTriple.getArch() == Triple::x86_64) {
    return "linux-x86_64";
  } else if (hostTriple.isMacOSX() && hostTriple.getArch() == Triple::x86_64) {
    return "darwin-x86_64";
  } else if (hostTriple.isOSWindows() &&
             hostTriple.getArch() == Triple::x86_64) {
    return "windows-x86_64";
  } else if (hostTriple.isOSWindows() && hostTriple.getArch() == Triple::x86) {
    return "windows";
  } else {
    llvm::errs()
        << "No (known) Android NDK prebuilt name for this host platform ('"
        << hostTriple.str() << "')";
    return "";
  }
}

// Returns the canonical target name for the Android NDK prebuilt versions.
static const char *getNDKTargetPlatform(const Triple &targetTriple) {
  switch (targetTriple.getArch()) {
  case Triple::arm:
    return "armv7a";
  case Triple::aarch64:
    return "aarch64";
  case Triple::x86:
    return "i686";
  case Triple::x86_64:
    return "x86_64";
  default:
    llvm::errs()
        << "No (known) Android NDK prebuilt name for this target platform ('"
        << targetTriple.str() << "')";
    return "";
  }
}

// Android linker using the Android NDK toolchain.
//
// Specifically, using the clang drivers specific to a target architecture and
// API version, e.g. aarch64-linux-android29-clang.
//
// Do we really need to bother using the NDK and using that specific clang
// driver? What if we just used a standard ld driver --- what if we just used
// UnixLinkerTool for Android too? At least when the host is Linux?
// At first glance, that seems to just work, but the following suggests to
// still bother with Android NDK clang drivers:
//
// While such drivers end up exec'ing just one standard clang driver, which in
// turn ends up exec'ing just one standard ld driver, at each stage some
// significant flags are passed, so it seems wise to rely on the NDK to set
// all these flags for us.
//
// Example: with strace, we find that as of NDK r23, the driver
//
//   android-ndk-r23/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android29-clang
//
// execs
//
//   android-ndk-r23/toolchains/llvm/prebuilt/linux-x86_64/bin/clang
//     --target=aarch64-linux-android29
//
// which in turn execs
//
//   android-ndk-r23/toolchains/llvm/prebuilt/linux-x86_64/bin/ld
//     -pie -z noexecstack -EL --fix-cortex-a53-843419
//     --warn-shared-textrel -z now -z relro -z max-page-size=4096
//     --hash-style=gnu --enable-new-dtags --eh-frame-hdr
//     -m aarch64linux -dynamic-linker /system/bin/linker64
//
// And that ld driver really is LLD:
//
//   $ android-ndk-r23/toolchains/llvm/prebuilt/linux-x86_64/bin/ld --version
//   LLD 12.0.5 (/buildbot/src/android/llvm-toolchain/out/llvm-project/lld ...
class AndroidLinkerTool : public LinkerTool {
public:
  using LinkerTool::LinkerTool;

  std::string getSystemToolPath() const override {
    auto toolPath = LinkerTool::getSystemToolPath();
    if (!toolPath.empty()) {
      return toolPath;
    }

    // ANDROID_NDK must be set for us to infer the tool path.
    char *androidNDKPath = std::getenv("ANDROID_NDK");
    if (!androidNDKPath) {
      llvm::errs() << "ANDROID_NDK environment variable must be set\n";
      return "";
    }

    // Extract the Android version from the `android30` like triple piece.
    llvm::VersionTuple androidEnv = targetTriple.getEnvironmentVersion();
    unsigned androidVersion = androidEnv.getMajor(); // like '30'

    // Select prebuilt toolchain based on both host and target
    // architecture/platform:
    //   https://developer.android.com/ndk/guides/other_build_systems
    return llvm::Twine(androidNDKPath)
        .concat("/toolchains/llvm/prebuilt/")
        .concat(getNDKHostPlatform())
        .concat("/bin/")
        .concat(getNDKTargetPlatform(targetTriple))
        .concat("-linux-android")
        .concat(std::to_string(androidVersion))
        .concat("-clang")
        .str();
  }

  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.size() == 1) {
      artifacts.libraryFile =
          Artifact::createVariant(objectFiles.front().path, "so");
    } else {
      artifacts.libraryFile = Artifact::createTemporary(libraryName, "so");
    }
    artifacts.libraryFile.close();

    SmallVector<std::string> flags = {
        getSystemToolPath(),

        // Avoids including any libc/startup files that initialize the CRT as
        // we don't use any of that. Our shared libraries must be freestanding.
        //
        // It matters that this flag isn't prefixed with --for-linker=. Doing so
        // results in a dlopen error: 'cannot locate symbol "main" referenced by
        // "iree_dylib_foo.so"'
        "-nostdlib", // -nodefaultlibs + -nostartfiles

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

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

    // Since we are using a clang driver, we need to prefix the flags that are
    // meant to be only interpreted by the linker.
    SmallVector<std::string> flagsToPrefixForLinker = {
        // Statically link all dependencies so we don't have any runtime deps.
        // We cannot have any imports in the module we produce.
        "-static",

        // Generate a dynamic library (ELF type: ET_DYN), otherwise dlopen()
        // won't succeed on it. This is not incompatible with -static. The GNU
        // man page for ld, `man ld`, says the following:
        //
        //   -static
        //       Do not link against shared libraries. [...] This option can be
        //       used with -shared. Doing so means that a shared library is
        //       being created but that all of the library's external references
        //       must be resolved by pulling in entries from static libraries.
        //
        // While that much is said in the GNU ld man page, the reality is that
        // out of ld.bfd, ld.gold and ld.lld, only ld.lld actually implements
        // that. Meanwhile, ld.bfd interprets -static -shared as just -static,
        // and ld.gold rejects -static -shared outright as "incompatible".
        //
        // So here we are effectively relying on the linker being ld.lld, which
        // is the case because we are using Android NDK clang, which execs
        // Android NDK ld, which is ld.lld, see strace results mentioned in
        // AndroidLinkerTool class comment.
        "-shared",

        // As seen in the strace results mentioned in the AndroidLinkerTool
        // class comment, when Android NDK clang execs ld, it passes -pie to it.
        // ld considers that to be incompatible with -shared (maybe simply
        // because -pie stands for position independent EXECUTABLE and -shared
        // means generate an ET_DYN, not an ET_EXEC?), so we have to pass
        // this flag now to undo -pie.
        "-no-pie",
    };

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

    // Prefix and fold flagsToPrefixForLinker into flags.
    for (const auto &f : flagsToPrefixForLinker) {
      flags.push_back("--for-linker=" + f);
    }
    flagsToPrefixForLinker.clear();

    auto commandLine = llvm::join(flags, " ");
    if (failed(runLinkCommand(commandLine))) {
      return std::nullopt;
    }
    return artifacts;
  }
};

std::unique_ptr<LinkerTool>
createAndroidLinkerTool(const llvm::Triple &targetTriple,
                        LLVMTargetOptions &targetOptions) {
  assert(targetTriple.isAndroid() &&
         "only use the AndroidLinkerTool for Android targets");
  return std::make_unique<AndroidLinkerTool>(targetTriple, targetOptions);
}

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