Просмотр исходного кода

Add LLD subcommand and busybox support (#4973)

This removes the separately built and installed LLD binary. The symlinks
used by Clang when directly invoking LLD now point back to the main
`carbon-busybox` binary and dispatch through the newly added subcommand.

With this change we're down to shipping a single busybox binary in the
toolchain, removing duplicate installed copies of LLD and all its LLVM
dependencies. =]

The LLD subcommand works a bit differently from the `clang` subcommand
because the CLI for LLD is specific to which platform flavor of linker
is being invoked.

As part of this, I've extracted some of the common functionality in the
Clang runner into a base class that can be re-used. I expect to use this
again in a follow-up change to add subcommands to run other LLVM tools.

---------

Co-authored-by: Jon Ross-Perkins <jperkins@google.com>
Chandler Carruth 1 год назад
Родитель
Сommit
8d1d491ad0

+ 1 - 0
toolchain/diagnostics/diagnostic_kind.def

@@ -29,6 +29,7 @@ CARBON_DIAGNOSTIC_KIND(CompilePreludeManifestError)
 CARBON_DIAGNOSTIC_KIND(CompileInputNotRegularFile)
 CARBON_DIAGNOSTIC_KIND(CompileOutputFileOpenError)
 CARBON_DIAGNOSTIC_KIND(FormatMultipleFilesToOneOutput)
+CARBON_DIAGNOSTIC_KIND(LLDFuzzingDisallowed)
 
 // ============================================================================
 // SourceBuffer diagnostics

+ 58 - 4
toolchain/driver/BUILD

@@ -20,11 +20,8 @@ cc_library(
     name = "clang_runner",
     srcs = ["clang_runner.cpp"],
     hdrs = ["clang_runner.h"],
-    data = [
-        "//toolchain/install:install_data.no_driver",
-    ],
     deps = [
-        "//common:command_line",
+        ":tool_runner_base",
         "//common:ostream",
         "//common:vlog",
         "//toolchain/install:install_paths",
@@ -104,6 +101,8 @@ cc_library(
         "language_server_subcommand.h",
         "link_subcommand.cpp",
         "link_subcommand.h",
+        "lld_subcommand.cpp",
+        "lld_subcommand.h",
     ],
     hdrs = [
         "driver.h",
@@ -115,6 +114,7 @@ cc_library(
     textual_hdrs = ["flags.def"],
     deps = [
         ":clang_runner",
+        ":lld_runner",
         "//common:command_line",
         "//common:error",
         "//common:ostream",
@@ -182,3 +182,57 @@ cc_fuzz_test(
         "@llvm-project//llvm:Support",
     ],
 )
+
+cc_library(
+    name = "lld_runner",
+    srcs = ["lld_runner.cpp"],
+    hdrs = ["lld_runner.h"],
+    deps = [
+        ":tool_runner_base",
+        "//common:ostream",
+        "//common:vlog",
+        "//toolchain/install:install_paths",
+        "@llvm-project//lld:Common",
+        "@llvm-project//lld:ELF",
+        "@llvm-project//lld:MachO",
+        "@llvm-project//llvm:Support",
+    ],
+)
+
+cc_test(
+    name = "lld_runner_test",
+    size = "small",
+    srcs = ["lld_runner_test.cpp"],
+    env = cc_env(),
+    deps = [
+        ":clang_runner",
+        ":lld_runner",
+        "//common:all_llvm_targets",
+        "//common:check",
+        "//common:ostream",
+        "//common:raw_string_ostream",
+        "//testing/base:capture_std_streams",
+        "//testing/base:file_helpers",
+        "//testing/base:global_exe_path",
+        "//testing/base:gtest_main",
+        "@googletest//:gtest",
+        "@llvm-project//llvm:Object",
+        "@llvm-project//llvm:Support",
+        "@llvm-project//llvm:TargetParser",
+    ],
+)
+
+cc_library(
+    name = "tool_runner_base",
+    srcs = ["tool_runner_base.cpp"],
+    hdrs = ["tool_runner_base.h"],
+    data = [
+        "//toolchain/install:install_data.no_driver",
+    ],
+    deps = [
+        "//common:ostream",
+        "//common:vlog",
+        "//toolchain/install:install_paths",
+        "@llvm-project//llvm:Support",
+    ],
+)

+ 5 - 45
toolchain/driver/clang_runner.cpp

@@ -15,7 +15,6 @@
 #include "clang/Driver/Driver.h"
 #include "clang/Frontend/CompilerInvocation.h"
 #include "clang/Frontend/TextDiagnosticPrinter.h"
-#include "common/command_line.h"
 #include "common/vlog.h"
 #include "llvm/ADT/ArrayRef.h"
 #include "llvm/ADT/ScopeExit.h"
@@ -44,59 +43,20 @@ ClangRunner::ClangRunner(const InstallPaths* install_paths,
                          llvm::StringRef target,
                          llvm::IntrusiveRefCntPtr<llvm::vfs::FileSystem> fs,
                          llvm::raw_ostream* vlog_stream)
-    : installation_(install_paths),
+    : ToolRunnerBase(install_paths, vlog_stream),
       target_(target),
       fs_(std::move(fs)),
-      vlog_stream_(vlog_stream),
       diagnostic_ids_(new clang::DiagnosticIDs()) {}
 
 auto ClangRunner::Run(llvm::ArrayRef<llvm::StringRef> args) -> bool {
   // TODO: Maybe handle response file expansion similar to the Clang CLI?
 
-  // If we have a verbose logging stream, and that stream is the same as
-  // `llvm::errs`, then add the `-v` flag so that the driver also prints verbose
-  // information.
-  bool inject_v_arg = vlog_stream_ == &llvm::errs();
-  std::array<llvm::StringRef, 1> v_arg_storage;
-  llvm::ArrayRef<llvm::StringRef> maybe_v_arg;
-  if (inject_v_arg) {
-    v_arg_storage[0] = "-v";
-    maybe_v_arg = v_arg_storage;
-  }
-
-  CARBON_VLOG("Running Clang driver with arguments: \n");
-
-  // Render the arguments into null-terminated C-strings for use by the Clang
-  // driver. Command lines can get quite long in build systems so this tries to
-  // minimize the memory allocation overhead.
-
-  // Provide the wrapped `clang` path in order to support subprocessing. We also
-  // set the install directory below.
   std::string clang_path = installation_->clang_path();
-  std::array<llvm::StringRef, 1> exe_arg = {clang_path};
-  auto args_range =
-      llvm::concat<const llvm::StringRef>(exe_arg, maybe_v_arg, args);
-  int total_size = 0;
-  for (llvm::StringRef arg : args_range) {
-    // Accumulate both the string size and a null terminator byte.
-    total_size += arg.size() + 1;
-  }
 
-  // Allocate one chunk of storage for the actual C-strings and a vector of
-  // pointers into the storage.
-  llvm::OwningArrayRef<char> cstr_arg_storage(total_size);
-  llvm::SmallVector<const char*, 64> cstr_args;
-  cstr_args.reserve(args.size() + inject_v_arg + 1);
-  for (ssize_t i = 0; llvm::StringRef arg : args_range) {
-    cstr_args.push_back(&cstr_arg_storage[i]);
-    memcpy(&cstr_arg_storage[i], arg.data(), arg.size());
-    i += arg.size();
-    cstr_arg_storage[i] = '\0';
-    ++i;
-  }
-  for (const char* cstr_arg : llvm::ArrayRef(cstr_args)) {
-    CARBON_VLOG("    '{0}'\n", cstr_arg);
-  }
+  // Rebuild the args as C-string args.
+  llvm::OwningArrayRef<char> cstr_arg_storage;
+  llvm::SmallVector<const char*, 64> cstr_args =
+      BuildCStrArgs("Clang", clang_path, "-v", args, cstr_arg_storage);
 
   if (!args.empty() && args[0].starts_with("-cc1")) {
     CARBON_VLOG("Calling clang_main for cc1...");

+ 2 - 4
toolchain/driver/clang_runner.h

@@ -10,6 +10,7 @@
 #include "llvm/ADT/ArrayRef.h"
 #include "llvm/ADT/StringRef.h"
 #include "llvm/Support/VirtualFileSystem.h"
+#include "toolchain/driver/tool_runner_base.h"
 #include "toolchain/install/install_paths.h"
 
 namespace Carbon {
@@ -36,7 +37,7 @@ namespace Carbon {
 // standard output and standard error, and otherwise can only read and write
 // files based on their names described in the arguments. It doesn't provide any
 // higher-level abstraction such as streams for inputs or outputs.
-class ClangRunner {
+class ClangRunner : ToolRunnerBase {
  public:
   // Build a Clang runner that uses the provided `exe_name` and `err_stream`.
   //
@@ -61,11 +62,8 @@ class ClangRunner {
   auto EnableLeakingMemory() -> void { enable_leaking_ = true; }
 
  private:
-  const InstallPaths* installation_;
-
   llvm::StringRef target_;
   llvm::IntrusiveRefCntPtr<llvm::vfs::FileSystem> fs_;
-  llvm::raw_ostream* vlog_stream_;
 
   llvm::IntrusiveRefCntPtr<clang::DiagnosticIDs> diagnostic_ids_;
 

+ 3 - 0
toolchain/driver/driver.cpp

@@ -15,6 +15,7 @@
 #include "toolchain/driver/format_subcommand.h"
 #include "toolchain/driver/language_server_subcommand.h"
 #include "toolchain/driver/link_subcommand.h"
+#include "toolchain/driver/lld_subcommand.h"
 
 namespace Carbon {
 
@@ -33,6 +34,7 @@ struct Options {
   FormatSubcommand format;
   LanguageServerSubcommand language_server;
   LinkSubcommand link;
+  LldSubcommand lld;
 
   // On success, this is set to the subcommand to run.
   DriverSubcommand* selected_subcommand = nullptr;
@@ -89,6 +91,7 @@ applies to each message that forms a diagnostic, not just the primary message.
   format.AddTo(b, &selected_subcommand);
   language_server.AddTo(b, &selected_subcommand);
   link.AddTo(b, &selected_subcommand);
+  lld.AddTo(b, &selected_subcommand);
 
   b.RequiresSubcommand();
 }

+ 59 - 0
toolchain/driver/lld_runner.cpp

@@ -0,0 +1,59 @@
+// 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/lld_runner.h"
+
+#include <algorithm>
+#include <memory>
+#include <numeric>
+#include <optional>
+
+#include "common/vlog.h"
+#include "llvm/ADT/ArrayRef.h"
+#include "llvm/ADT/StringRef.h"
+
+// Declare the supported driver flavor entry points.
+//
+// TODO: Currently, just ELF and MachO, but eventually we should support all of
+// the LLD platforms.
+//
+// NOLINTBEGIN(readability-identifier-naming): External library name.
+LLD_HAS_DRIVER(elf)
+LLD_HAS_DRIVER(macho)
+// NOLINTEND(readability-identifier-naming)
+
+namespace Carbon {
+
+auto LldRunner::LinkHelper(llvm::StringLiteral label,
+                           llvm::ArrayRef<llvm::StringRef> args,
+                           const std::string& path, lld::DriverDef driver_def)
+    -> bool {
+  // Allocate one chunk of storage for the actual C-strings and a vector of
+  // pointers into the storage.
+  llvm::OwningArrayRef<char> cstr_arg_storage;
+  llvm::SmallVector<const char*, 64> cstr_args =
+      BuildCStrArgs("LLD", path, "-v", args, cstr_arg_storage);
+
+  CARBON_VLOG("Running LLD {0}-platform link...\n", label);
+  lld::Result result =
+      lld::lldMain(cstr_args, llvm::outs(), llvm::errs(), {driver_def});
+
+  // Check for an unrecoverable error.
+  CARBON_CHECK(result.canRunAgain, "LLD encountered an unrecoverable error!");
+
+  // TODO: Should this be forwarding the full exit code?
+  return result.retCode == 0;
+}
+
+auto LldRunner::ElfLink(llvm::ArrayRef<llvm::StringRef> args) -> bool {
+  return LinkHelper("GNU", args, installation_->ld_lld_path(),
+                    {.f = lld::Gnu, .d = &lld::elf::link});
+}
+
+auto LldRunner::MachOLink(llvm::ArrayRef<llvm::StringRef> args) -> bool {
+  return LinkHelper("Darwin", args, installation_->ld64_lld_path(),
+                    {.f = lld::Darwin, .d = &lld::macho::link});
+}
+
+}  // namespace Carbon

+ 36 - 0
toolchain/driver/lld_runner.h

@@ -0,0 +1,36 @@
+// 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
+
+#ifndef CARBON_TOOLCHAIN_DRIVER_LLD_RUNNER_H_
+#define CARBON_TOOLCHAIN_DRIVER_LLD_RUNNER_H_
+
+#include "common/ostream.h"
+#include "lld/Common/Driver.h"
+#include "llvm/ADT/ArrayRef.h"
+#include "llvm/ADT/StringRef.h"
+#include "toolchain/driver/tool_runner_base.h"
+#include "toolchain/install/install_paths.h"
+
+namespace Carbon {
+
+// Runs LLD in a manner similar to invoking it with the provided arguments.
+class LldRunner : ToolRunnerBase {
+ public:
+  using ToolRunnerBase::ToolRunnerBase;
+
+  // Run LLD as a GNU-style linker with the provided arguments.
+  auto ElfLink(llvm::ArrayRef<llvm::StringRef> args) -> bool;
+
+  // Run LLD as a Darwin-style linker with the provided arguments.
+  auto MachOLink(llvm::ArrayRef<llvm::StringRef> args) -> bool;
+
+ private:
+  auto LinkHelper(llvm::StringLiteral label,
+                  llvm::ArrayRef<llvm::StringRef> args, const std::string& path,
+                  lld::DriverDef driver_def) -> bool;
+};
+
+}  // namespace Carbon
+
+#endif  // CARBON_TOOLCHAIN_DRIVER_LLD_RUNNER_H_

+ 210 - 0
toolchain/driver/lld_runner_test.cpp

@@ -0,0 +1,210 @@
+// 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/lld_runner.h"
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include <filesystem>
+#include <fstream>
+#include <utility>
+
+#include "common/check.h"
+#include "common/ostream.h"
+#include "common/raw_string_ostream.h"
+#include "llvm/ADT/ScopeExit.h"
+#include "llvm/Object/Binary.h"
+#include "llvm/Support/FormatVariadic.h"
+#include "llvm/Support/Program.h"
+#include "llvm/TargetParser/Host.h"
+#include "testing/base/capture_std_streams.h"
+#include "testing/base/file_helpers.h"
+#include "testing/base/global_exe_path.h"
+#include "toolchain/driver/clang_runner.h"
+
+namespace Carbon {
+namespace {
+
+using ::testing::HasSubstr;
+using ::testing::Not;
+using ::testing::StrEq;
+
+TEST(LldRunnerTest, Version) {
+  RawStringOstream test_os;
+  const auto install_paths =
+      InstallPaths::MakeForBazelRunfiles(Testing::GetExePath());
+  LldRunner runner(&install_paths, &test_os);
+
+  std::string out;
+  std::string err;
+  EXPECT_TRUE(Testing::CallWithCapturedOutput(
+      out, err, [&] { return runner.ElfLink({"--version"}); }));
+
+  // The arguments to LLD should be part of the verbose log.
+  EXPECT_THAT(test_os.TakeStr(), HasSubstr("--version"));
+
+  // Nothing should print to stderr here.
+  EXPECT_THAT(err, StrEq(""));
+
+  // We don't care about any particular version, just that it is printed.
+  EXPECT_THAT(out, HasSubstr("LLD"));
+  // Check that it was in fact the GNU linker.
+  EXPECT_THAT(out, HasSubstr("compatible with GNU linkers"));
+
+  // Try the Darwin linker.
+  EXPECT_TRUE(Testing::CallWithCapturedOutput(
+      out, err, [&] { return runner.MachOLink({"--version"}); }));
+
+  // Again, the arguments to LLD should be part of the verbose log.
+  EXPECT_THAT(test_os.TakeStr(), HasSubstr("--version"));
+
+  // Nothing should print to stderr.
+  EXPECT_THAT(err, StrEq(""));
+
+  // We don't care about any particular version.
+  EXPECT_THAT(out, HasSubstr("LLD"));
+  // The Darwin link code path doesn't print anything distinct, so instead check
+  // that the GNU output isn't repeated.
+  EXPECT_THAT(out, Not(HasSubstr("GNU")));
+}
+
+static auto CompileTwoSources(const InstallPaths& install_paths,
+                              llvm::StringRef target)
+    -> std::pair<std::filesystem::path, std::filesystem::path> {
+  std::filesystem::path test_a_file =
+      *Testing::WriteTestFile("test_a.cpp", "int test_a() { return 0; }");
+  std::filesystem::path test_b_file = *Testing::WriteTestFile(
+      "test_b.cpp", "int test_a();\nint main() { return test_a(); }");
+  std::filesystem::path test_a_output = *Testing::WriteTestFile("test_a.o", "");
+  std::filesystem::path test_b_output = *Testing::WriteTestFile("test_b.o", "");
+
+  // First compile the two source files to `.o` files with Clang.
+  RawStringOstream verbose_out;
+  auto vfs = llvm::vfs::getRealFileSystem();
+  ClangRunner clang(&install_paths, target, vfs, &verbose_out);
+  std::string target_arg = llvm::formatv("--target={0}", target).str();
+  std::string out;
+  std::string err;
+  CARBON_CHECK(
+      Testing::CallWithCapturedOutput(
+          out, err,
+          [&] {
+            return clang.Run({target_arg, "-fPIE", "-c", test_a_file.string(),
+                              "-o", test_a_output.string()});
+          }),
+      "Verbose output from runner:\n{0}\nStderr:\n{1}\n", verbose_out.TakeStr(),
+      err);
+  verbose_out.clear();
+
+  CARBON_CHECK(
+      Testing::CallWithCapturedOutput(
+          out, err,
+          [&] {
+            return clang.Run({target_arg, "-fPIE", "-c", test_b_file.string(),
+                              "-o", test_b_output.string()});
+          }),
+      "Verbose output from runner:\n{0}\nStderr:\n{1}\n", verbose_out.TakeStr(),
+      err);
+  verbose_out.clear();
+
+  return {test_a_output, test_b_output};
+}
+
+TEST(LldRunnerTest, ElfLinkTest) {
+  const auto install_paths =
+      InstallPaths::MakeForBazelRunfiles(Testing::GetExePath());
+
+  std::filesystem::path test_a_output;
+  std::filesystem::path test_b_output;
+  std::tie(test_a_output, test_b_output) =
+      CompileTwoSources(install_paths, "aarch64-unknown-linux");
+
+  std::filesystem::path test_output = *Testing::WriteTestFile("test.o", "");
+
+  RawStringOstream verbose_out;
+  std::string out;
+  std::string err;
+
+  LldRunner lld(&install_paths, &verbose_out);
+
+  // Link the two object files together.
+  //
+  // TODO: Currently, this uses a relocatable link, but it would be better to do
+  // a full link to an executable. For that to work, we need at least the
+  // C-runtime built artifacts available in the toolchain. We should revisit
+  // this once we have those in place. This also prevents us from testing a
+  // failed link easily.
+  EXPECT_TRUE(Testing::CallWithCapturedOutput(
+      out, err,
+      [&] {
+        return lld.ElfLink({"-m", "aarch64linux", "--relocatable", "-o",
+                            test_output.string(), test_a_output.string(),
+                            test_b_output.string()});
+      }))
+      << "Verbose output from runner:\n"
+      << verbose_out.TakeStr() << "\n";
+  verbose_out.clear();
+
+  // No output should be produced.
+  EXPECT_THAT(out, StrEq(""));
+  EXPECT_THAT(err, StrEq(""));
+}
+
+TEST(LldRunnerTest, MachOLinkTest) {
+  const auto install_paths =
+      InstallPaths::MakeForBazelRunfiles(Testing::GetExePath());
+
+  std::filesystem::path test_a_output;
+  std::filesystem::path test_b_output;
+  std::tie(test_a_output, test_b_output) =
+      CompileTwoSources(install_paths, "arm64-unknown-macosx10.4.0");
+
+  std::filesystem::path test_output = *Testing::WriteTestFile("test.o", "");
+
+  RawStringOstream verbose_out;
+  std::string out;
+  std::string err;
+
+  // Link the two object files together.
+  //
+  // This is a somewhat arbitrary command line, and is missing the C-runtimes,
+  // but seems to succeed currently. The goal isn't to test any *particular*
+  // link, but just than an actual link occurs successfully.
+  LldRunner lld(&install_paths, &verbose_out);
+  EXPECT_TRUE(Testing::CallWithCapturedOutput(
+      out, err,
+      [&] {
+        return lld.MachOLink({"-arch", "arm64", "-platform_version", "macos",
+                              "10.4.0", "10.4.0", "-o", test_output.string(),
+                              test_a_output.string(), test_b_output.string()});
+      }))
+      << "Verbose output from runner:\n"
+      << verbose_out.TakeStr() << "\n";
+  verbose_out.clear();
+
+  // No output should be produced.
+  EXPECT_THAT(out, StrEq(""));
+  EXPECT_THAT(err, StrEq(""));
+
+  // Re-do the link, but with only one of the inputs. This should fail due to an
+  // unresolved symbol.
+  EXPECT_FALSE(Testing::CallWithCapturedOutput(
+      out, err,
+      [&] {
+        return lld.MachOLink({"-arch", "arm64", "-platform_version", "macos",
+                              "10.4.0", "10.4.0", "-o", test_output.string(),
+                              test_b_output.string()});
+      }))
+      << "Verbose output from runner:\n"
+      << verbose_out.TakeStr() << "\n";
+  verbose_out.clear();
+
+  // The missing symbol should be diagnosed on `stderr`.
+  EXPECT_THAT(out, StrEq(""));
+  EXPECT_THAT(err, HasSubstr("undefined symbol: __Z6test_av"));
+}
+
+}  // namespace
+}  // namespace Carbon

+ 113 - 0
toolchain/driver/lld_subcommand.cpp

@@ -0,0 +1,113 @@
+// 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/lld_subcommand.h"
+
+#include "llvm/TargetParser/Host.h"
+#include "llvm/TargetParser/Triple.h"
+#include "toolchain/driver/lld_runner.h"
+
+namespace Carbon {
+
+auto LldOptions::Build(CommandLine::CommandBuilder& b) -> void {
+  // We want to select a default platform based on the default target. Since
+  // that requires some dynamic inspection of the target, do that here.
+  std::string default_target = llvm::sys::getDefaultTargetTriple();
+  llvm::Triple default_triple(default_target);
+  switch (default_triple.getObjectFormat()) {
+    case llvm::Triple::MachO:
+      platform = Platform::MachO;
+      break;
+
+      // We default to the GNU or Unix platform as ELF is a plausible default
+      // and LLD doesn't support any generic invocations.
+    default:
+    case llvm::Triple::ELF:
+      platform = Platform::Elf;
+      break;
+  }
+
+  b.AddOneOfOption(
+      {
+          .name = "platform",
+          .help = R"""(
+Platform linking style to use. The default is selected to match the default
+target's platform.
+)""",
+      },
+      [&](auto& arg_b) {
+        arg_b.SetOneOf(
+            {
+                arg_b.OneOfValue("elf", Platform::Elf),
+                // Some of LLD documentation uses "Unix" or "GNU", so
+                // include an alias here.
+                arg_b.OneOfValue("gnu", Platform::Elf),
+                arg_b.OneOfValue("unix", Platform::Elf),
+
+                arg_b.OneOfValue("macho", Platform::MachO),
+                // Darwin is also sometimes used, include it as an alias here.
+                arg_b.OneOfValue("darwin", Platform::MachO),
+            },
+            &platform);
+      });
+  b.AddStringPositionalArg(
+      {
+          .name = "ARG",
+          .help = R"""(
+Arguments passed to LLD.
+)""",
+      },
+      [&](auto& arg_b) { arg_b.Append(&args); });
+}
+
+static constexpr CommandLine::CommandInfo SubcommandInfo = {
+    .name = "lld",
+    .help = R"""(
+Runs LLD with the provided arguments.
+
+Note that a specific LLD platform must be selected, and it is actually that
+particular platform's LLD-driver that is run with the arguments. There is no
+generic LLD command line.
+
+For a given platform, this is equivalent to running that platform's LLD alias
+directly, and provides the full command line interface.
+
+Use `carbon lld --platform=elf -- ARGS` to separate the `ARGS` forwarded to LLD
+from the flags passed to the Carbon subcommand.
+
+Note that typically it is better to use a higher level command to link code,
+such as invoking `carbon link` with the relevant flags. However, this subcommand
+supports when you already have a specific invocation using existing command line
+syntaxes, as well as testing and debugging of the underlying tool.
+)""",
+};
+
+LldSubcommand::LldSubcommand() : DriverSubcommand(SubcommandInfo) {}
+
+// TODO: This lacks a lot of features from the main driver code. We may need to
+// add more.
+// https://github.com/llvm/llvm-project/blob/main/clang/tools/driver/driver.cpp
+auto LldSubcommand::Run(DriverEnv& driver_env) -> DriverResult {
+  LldRunner runner(driver_env.installation, driver_env.vlog_stream);
+
+  // Don't run LLD when fuzzing, as we're not currently in a good position to
+  // debug and fix fuzzer-found bugs within LLD.
+  if (driver_env.fuzzing) {
+    CARBON_DIAGNOSTIC(
+        LLDFuzzingDisallowed, Error,
+        "preventing fuzzing of `lld` subcommand due to external library");
+    driver_env.emitter.Emit(LLDFuzzingDisallowed);
+    return {.success = false};
+  }
+
+  switch (options_.platform) {
+    case LldOptions::Platform::Elf:
+      return {.success = runner.ElfLink(options_.args)};
+    case LldOptions::Platform::MachO:
+      return {.success = runner.MachOLink(options_.args)};
+  }
+  CARBON_FATAL("Failed to find and run a valid LLD platform link!");
+}
+
+}  // namespace Carbon

+ 54 - 0
toolchain/driver/lld_subcommand.h

@@ -0,0 +1,54 @@
+// 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
+
+#ifndef CARBON_TOOLCHAIN_DRIVER_LLD_SUBCOMMAND_H_
+#define CARBON_TOOLCHAIN_DRIVER_LLD_SUBCOMMAND_H_
+
+#include "common/command_line.h"
+#include "llvm/ADT/SmallVector.h"
+#include "llvm/ADT/StringRef.h"
+#include "toolchain/driver/driver_env.h"
+#include "toolchain/driver/driver_subcommand.h"
+
+namespace Carbon {
+
+// Options for the LLD subcommand, which is just a thin wrapper.
+//
+// See the implementation of `Build` for documentation on members.
+struct LldOptions {
+  // Supported linking platforms.
+  //
+  // Note that these are similar to the object formats in an LLVM triple, but we
+  // use a distinct enum because we only include the platforms supported by our
+  // subcommand which is a subset of those recognized by the LLVM triple
+  // infrastructure.
+  enum class Platform {
+    Elf,
+    MachO,
+  };
+
+  auto Build(CommandLine::CommandBuilder& b) -> void;
+
+  Platform platform;
+  llvm::SmallVector<llvm::StringRef> args;
+};
+
+// Implements the LLD subcommand of the driver.
+class LldSubcommand : public DriverSubcommand {
+ public:
+  explicit LldSubcommand();
+
+  auto BuildOptions(CommandLine::CommandBuilder& b) -> void override {
+    options_.Build(b);
+  }
+
+  auto Run(DriverEnv& driver_env) -> DriverResult override;
+
+ private:
+  LldOptions options_;
+};
+
+}  // namespace Carbon
+
+#endif  // CARBON_TOOLCHAIN_DRIVER_LLD_SUBCOMMAND_H_

+ 15 - 0
toolchain/driver/testdata/fail_lld_fuzzing.carbon

@@ -0,0 +1,15 @@
+// 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
+//
+// ARGS: --include-diagnostic-kind --fuzzing lld -- -o foo foo.o bar.o
+//
+// SET-CAPTURE-CONSOLE-OUTPUT
+// clang-format off
+// AUTOUPDATE
+// TIP: To test this file alone, run:
+// TIP:   bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/driver/testdata/fail_lld_fuzzing.carbon
+// TIP: To dump output, run:
+// TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/driver/testdata/fail_lld_fuzzing.carbon
+// CHECK:STDERR: error: preventing fuzzing of `lld` subcommand due to external library [LLDFuzzingDisallowed]
+// CHECK:STDERR:

+ 73 - 0
toolchain/driver/tool_runner_base.cpp

@@ -0,0 +1,73 @@
+// 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/tool_runner_base.h"
+
+#include <memory>
+
+#include "common/vlog.h"
+#include "llvm/ADT/ArrayRef.h"
+#include "llvm/ADT/StringRef.h"
+
+namespace Carbon {
+
+ToolRunnerBase::ToolRunnerBase(const InstallPaths* install_paths,
+                               llvm::raw_ostream* vlog_stream)
+    : installation_(install_paths), vlog_stream_(vlog_stream) {}
+
+auto ToolRunnerBase::BuildCStrArgs(llvm::StringRef tool_name,
+                                   llvm::StringRef tool_path,
+                                   std::optional<llvm::StringRef> verbose_flag,
+                                   llvm::ArrayRef<llvm::StringRef> args,
+                                   llvm::OwningArrayRef<char>& cstr_arg_storage)
+    -> llvm::SmallVector<const char*, 64> {
+  // TODO: Maybe handle response file expansion similar to the Clang CLI?
+
+  // If we have a verbose logging stream, and that stream is the same as
+  // `llvm::errs`, then add the `-v` flag so that the driver also prints verbose
+  // information.
+  bool inject_v_arg = verbose_flag.has_value() && vlog_stream_ == &llvm::errs();
+  std::array<llvm::StringRef, 1> v_arg_storage;
+  llvm::ArrayRef<llvm::StringRef> maybe_v_arg;
+  if (inject_v_arg) {
+    v_arg_storage[0] = *verbose_flag;
+    maybe_v_arg = v_arg_storage;
+  }
+
+  CARBON_VLOG("Running {} driver with arguments:\n", tool_name);
+
+  // Render the arguments into null-terminated C-strings. Command lines can get
+  // quite long in build systems so this tries to minimize the memory allocation
+  // overhead.
+
+  // Provide the wrapped tool path as the synthetic `argv[0]`.
+  std::array<llvm::StringRef, 1> exe_arg = {tool_path};
+  auto args_range =
+      llvm::concat<const llvm::StringRef>(exe_arg, maybe_v_arg, args);
+  int total_size = 0;
+  for (llvm::StringRef arg : args_range) {
+    // Accumulate both the string size and a null terminator byte.
+    total_size += arg.size() + 1;
+  }
+
+  // Allocate one chunk of storage for the actual C-strings and a vector of
+  // pointers into the storage.
+  cstr_arg_storage = llvm::OwningArrayRef<char>(total_size);
+  llvm::SmallVector<const char*, 64> cstr_args;
+  cstr_args.reserve(args.size() + inject_v_arg + 1);
+  for (ssize_t i = 0; llvm::StringRef arg : args_range) {
+    cstr_args.push_back(&cstr_arg_storage[i]);
+    memcpy(&cstr_arg_storage[i], arg.data(), arg.size());
+    i += arg.size();
+    cstr_arg_storage[i] = '\0';
+    ++i;
+  }
+  for (const char* cstr_arg : llvm::ArrayRef(cstr_args)) {
+    CARBON_VLOG("    '{0}'\n", cstr_arg);
+  }
+
+  return cstr_args;
+}
+
+}  // namespace Carbon

+ 65 - 0
toolchain/driver/tool_runner_base.h

@@ -0,0 +1,65 @@
+// 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
+
+#ifndef CARBON_TOOLCHAIN_DRIVER_TOOL_RUNNER_BASE_H_
+#define CARBON_TOOLCHAIN_DRIVER_TOOL_RUNNER_BASE_H_
+
+#include <optional>
+
+#include "common/ostream.h"
+#include "llvm/ADT/ArrayRef.h"
+#include "llvm/ADT/StringRef.h"
+#include "toolchain/install/install_paths.h"
+
+namespace Carbon {
+
+// Base  that factors out common utilities needed when implementing a runner of
+// an external tool, especially tools part of the LLVM and Clang C++ toolchain
+// that Carbon will end up wrapping.
+//
+// Note that this struct just collects common data and helper methods, and does
+// not itself impose any invariants or form a meaningful API. It should be used
+// as an implementation detail only.
+class ToolRunnerBase {
+ public:
+  // Construct the tool runner bas.
+  //
+  // If `vlog_stream` is provided, it will be used for `CARBON_VLOG`s. If it is
+  // also equal to `&llvm::errs()`, and so tied to stderr, that will be used by
+  // verbose flag injection helpers in this class.
+  explicit ToolRunnerBase(const InstallPaths* install_paths,
+                          llvm::raw_ostream* vlog_stream = nullptr);
+
+ protected:
+  // Translates `args` into C-string arguments for tool APIs based on `main`.
+  //
+  // Accepts a `tool_name` for logging, and a `tool_path` that will be used as
+  // the first C-string argument to simulate and `argv[0]` entry.
+  //
+  // Accepts a `cstr_arg_storage` that will provide the underlying storage for
+  // the C-strings, and returns a small vector of the C-string pointers. The
+  // returned small vector uses a large small size to allow most common command
+  // lines to avoid extra allocations and growth passes.
+  //
+  // Lastly accepts an optional `verbose_flag`. If provided, and if
+  // `vlog_stream_` is bound to stderr for this instance, the verbose flag will
+  // be injected at the start of the argument list.
+  auto BuildCStrArgs(llvm::StringRef tool_name, llvm::StringRef tool_path,
+                     std::optional<llvm::StringRef> verbose_flag,
+                     llvm::ArrayRef<llvm::StringRef> args,
+                     llvm::OwningArrayRef<char>& cstr_arg_storage)
+      -> llvm::SmallVector<const char*, 64>;
+
+  // We use protected members as this base is just factoring out common
+  // implementation details of other runners.
+  //
+  // NOLINTBEGIN(misc-non-private-member-variables-in-classes)
+  const InstallPaths* installation_;
+  llvm::raw_ostream* vlog_stream_;
+  // NOLINTEND(misc-non-private-member-variables-in-classes)
+};
+
+}  // namespace Carbon
+
+#endif  // CARBON_TOOLCHAIN_DRIVER_TOOL_RUNNER_BASE_H_

+ 7 - 8
toolchain/install/BUILD

@@ -116,11 +116,11 @@ cc_binary(
     ],
 )
 
+# TODO: Add remaining aliases of LLD for Windows and WASM when we have support
+# for them wired up through the busybox.
 lld_aliases = [
     "ld.lld",
     "ld64.lld",
-    "lld-link",
-    "wasm-ld",
 ]
 
 filegroup(
@@ -156,17 +156,16 @@ install_dirs = {
         install_filegroup("core", "//core:prelude"),
     ],
     "lib/carbon/llvm/bin": [
-        install_target(
-            "lld",
-            "@llvm-project//lld:lld",
-            executable = True,
-        ),
         install_symlink(
             "clang",
             "../../carbon-busybox",
             is_driver = True,
         ),
-    ] + [install_symlink(name, "lld") for name in lld_aliases],
+    ] + [install_symlink(
+        name,
+        "../../carbon-busybox",
+        is_driver = True,
+    ) for name in lld_aliases],
     "lib/carbon/llvm/lib/clang/" + LLVM_VERSION_MAJOR: [
         install_filegroup("include", ":clang_headers", "staging/include/"),
     ],

+ 14 - 1
toolchain/install/busybox_main.cpp

@@ -42,7 +42,20 @@ static auto Main(int argc, char** argv) -> ErrorOr<int> {
   llvm::SmallVector<llvm::StringRef> args;
   args.reserve(argc + 1);
   if (busybox_info.mode) {
-    args.append({*busybox_info.mode, "--"});
+    // Map busybox modes to the relevant subcommands with any flags needed to
+    // emulate the requested command. Typically, our busyboxed binaries redirect
+    // to a specific subcommand with some flags set and then pass the remaining
+    // busybox arguments as positional arguments to that subcommand.
+    //
+    // TODO: Add relevant flags to the `clang` subcommand and add `clang`-based
+    // symlinks to this like `clang++`.
+    auto subcommand_args =
+        llvm::StringSwitch<llvm::SmallVector<llvm::StringRef>>(
+            *busybox_info.mode)
+            .Case("ld.lld", {"lld", "--platform=gnu", "--"})
+            .Case("ld64.lld", {"lld", "--platform=darwin", "--"})
+            .Default({*busybox_info.mode, "--"});
+    args.append(subcommand_args);
   }
   args.append(argv + 1, argv + argc);
 

+ 24 - 0
toolchain/install/install_paths.cpp

@@ -171,4 +171,28 @@ auto InstallPaths::clang_path() const -> std::string {
   return path.str().str();
 }
 
+auto InstallPaths::lld_path() const -> std::string {
+  llvm::SmallString<256> path(prefix_);
+  // TODO: Adjust this to work equally well on Windows.
+  llvm::sys::path::append(path, llvm::sys::path::Style::posix,
+                          "lib/carbon/llvm/bin/lld");
+  return path.str().str();
+}
+
+auto InstallPaths::ld_lld_path() const -> std::string {
+  llvm::SmallString<256> path(prefix_);
+  // TODO: Adjust this to work equally well on Windows.
+  llvm::sys::path::append(path, llvm::sys::path::Style::posix,
+                          "lib/carbon/llvm/bin/ld.lld");
+  return path.str().str();
+}
+
+auto InstallPaths::ld64_lld_path() const -> std::string {
+  llvm::SmallString<256> path(prefix_);
+  // TODO: Adjust this to work equally well on Windows.
+  llvm::sys::path::append(path, llvm::sys::path::Style::posix,
+                          "lib/carbon/llvm/bin/ld64.lld");
+  return path.str().str();
+}
+
 }  // namespace Carbon

+ 5 - 0
toolchain/install/install_paths.h

@@ -87,6 +87,11 @@ class InstallPaths {
   // The path to `clang`.
   auto clang_path() const -> std::string;
 
+  // The path to `lld' and various aliases of `lld`.
+  auto lld_path() const -> std::string;
+  auto ld_lld_path() const -> std::string;
+  auto ld64_lld_path() const -> std::string;
+
  private:
   friend class InstallPathsTestPeer;
 

+ 1 - 2
toolchain/install/install_paths_test.cpp

@@ -75,8 +75,7 @@ class InstallPathsTest : public ::testing::Test {
     EXPECT_TRUE(llvm::sys::fs::is_directory(llvm_bin_path))
         << "path: " << llvm_bin_path;
 
-    for (llvm::StringRef llvm_bin :
-         {"lld", "ld.lld", "ld64.lld", "lld-link", "wasm-ld"}) {
+    for (llvm::StringRef llvm_bin : {"ld.lld", "ld64.lld"}) {
       llvm::SmallString<128> bin_path;
       bin_path.assign(llvm_bin_path);
       llvm::sys::path::append(bin_path, llvm_bin);