// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
// Exceptions. See /LICENSE for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

#include "toolchain/driver/link_subcommand.h"

#include "llvm/TargetParser/Triple.h"
#include "toolchain/driver/clang_runner.h"

namespace Carbon {

auto LinkOptions::Build(CommandLine::CommandBuilder& b) -> void {
  b.AddStringPositionalArg(
      {
          .name = "OBJECT_FILE",
          .help = R"""(
The input object files.
)""",
      },
      [&](auto& arg_b) {
        arg_b.Required(true);
        arg_b.Append(&object_filenames);
      });

  b.AddStringOption(
      {
          .name = "output",
          .value_name = "FILE",
          .help = R"""(
The linked file name. The output is always a linked binary.
)""",
      },
      [&](auto& arg_b) {
        arg_b.Required(true);
        arg_b.Set(&output_filename);
      });

  codegen_options.Build(b);
}

static void AddOSFlags(llvm::StringRef target,
                       llvm::SmallVectorImpl<llvm::StringRef>& args) {
  llvm::Triple triple(target);
  switch (triple.getOS()) {
    case llvm::Triple::Darwin:
    case llvm::Triple::MacOSX:
      // On macOS we need to set the sysroot to a viable SDK. Currently, this
      // hard codes the path to be the unversioned symlink. The prefix is also
      // hard coded in Homebrew and so this seems likely to work reasonably
      // well. Homebrew and I suspect the Xcode Clang both have this hard coded
      // at build time, so this seems reasonably safe but we can revisit if/when
      // needed.
      args.push_back(
          "--sysroot=/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk");
      // We also need to insist on a modern linker, otherwise the driver tries
      // too old and deprecated flags. The specific number here comes from an
      // inspection of the Clang driver source code to understand where features
      // were enabled, and this appears to be the latest version to control
      // driver behavior.
      //
      // TODO: We should replace this with use of `lld` eventually.
      args.push_back("-mlinker-version=705");
      break;

    default:
      // By default, just let the Clang driver handle everything.
      break;
  }
}

static constexpr CommandLine::CommandInfo SubcommandInfo = {
    .name = "link",
    .help = R"""(
Link Carbon executables.

This subcommand links Carbon executables by combining object files.

TODO: Support linking binary libraries, both archives and shared libraries.
TODO: Support linking against binary libraries.
)""",
};

LinkSubcommand::LinkSubcommand() : DriverSubcommand(SubcommandInfo) {}

auto LinkSubcommand::Run(DriverEnv& driver_env) -> DriverResult {
  // TODO: Currently we use the Clang driver to link. This works well on Unix
  // OSes but we likely need to directly build logic to invoke `link.exe` on
  // Windows where `cl.exe` doesn't typically cover that logic.

  // Use a reasonably large small vector here to minimize allocations. We expect
  // to link reasonably large numbers of object files.
  llvm::SmallVector<llvm::StringRef, 128> clang_args;

  // We link using a C++ mode of the driver.
  clang_args.push_back("--driver-mode=g++");

  // Pass the target down to Clang to pick up the correct defaults.
  std::string target_arg =
      llvm::formatv("--target={0}", options_.codegen_options.target).str();
  clang_args.push_back(target_arg);

  // Use LLD, which we provide in our install directory, for linking.
  clang_args.push_back("-fuse-ld=lld");

  // Disable linking the C++ standard library until can build and ship it as
  // part of the Carbon toolchain. This clearly won't work once we get into
  // interop, but for now it avoids spurious failures and distraction. The plan
  // is to build and bundle libc++ at which point we can replace this with
  // pointing at our bundled library.
  // TODO: Replace this when ready.
  clang_args.push_back("-nostdlib++");

  // Add OS-specific flags based on the target.
  AddOSFlags(options_.codegen_options.target, clang_args);

  clang_args.push_back("-o");
  clang_args.push_back(options_.output_filename);
  clang_args.append(options_.object_filenames.begin(),
                    options_.object_filenames.end());

  ClangRunner runner(driver_env.installation, driver_env.fs,
                     driver_env.vlog_stream);
  ErrorOr<bool> run_result = runner.Run(clang_args);
  if (!run_result.ok()) {
    // This is not a Clang failure, but a failure to even run Clang, so we need
    // to diagnose it here.
    CARBON_DIAGNOSTIC(FailureRunningClangToLink, Error,
                      "failure running `clang` to perform linking: {0}",
                      std::string);
    driver_env.emitter.Emit(FailureRunningClangToLink,
                            run_result.error().message());
    return {.success = false};
  }
  // Successfully ran Clang to perform the link, return its result.
  return {.success = *run_result};
}

}  // namespace Carbon
