Przeglądaj źródła

Introduce basic linking with the Clang driver. (#3922)

This adds the most rudimentary support for linking in the Carbon driver
by calling out to the Clang driver and letting it do the linking. This
is a super minimal version to start with, just enough to be somewhat
useful and to exercise calling into Clang for this.

Also adds a Clang runner to help run the Clang driver. This is a bit
more complete, although it sill needs some significant work. For
example, it will need some significant enhancements to be able to
compile C++ code, etc.

One thing that will likely change here is to better manage streams and
work better as a library. For now, this is a bit limited because Clang
and LLVM don't provide the necessary support for this.
Chandler Carruth 2 lat temu
rodzic
commit
36d8d2960a

+ 36 - 0
toolchain/driver/BUILD

@@ -13,6 +13,41 @@ filegroup(
     data = glob(["testdata/**/*.carbon"]),
 )
 
+cc_library(
+    name = "clang_runner",
+    srcs = ["clang_runner.cpp"],
+    hdrs = ["clang_runner.h"],
+    deps = [
+        "//common:command_line",
+        "//common:ostream",
+        "//common:vlog",
+        "@llvm-project//clang:basic",
+        "@llvm-project//clang:driver",
+        "@llvm-project//clang:frontend",
+        "@llvm-project//llvm:Core",
+        "@llvm-project//llvm:Support",
+        "@llvm-project//llvm:TargetParser",
+    ],
+)
+
+cc_test(
+    name = "clang_runner_test",
+    size = "small",
+    srcs = ["clang_runner_test.cpp"],
+    deps = [
+        ":clang_runner",
+        "//common:all_llvm_targets",
+        "//common:check",
+        "//common:ostream",
+        "//testing/base:gtest_main",
+        "//testing/base:test_raw_ostream",
+        "@googletest//:gtest",
+        "@llvm-project//llvm:Object",
+        "@llvm-project//llvm:Support",
+        "@llvm-project//llvm:TargetParser",
+    ],
+)
+
 cc_library(
     name = "driver",
     srcs = ["driver.cpp"],
@@ -20,6 +55,7 @@ cc_library(
     data = ["//core:prelude"],
     textual_hdrs = ["flags.def"],
     deps = [
+        ":clang_runner",
         "//common:command_line",
         "//common:vlog",
         "//toolchain/base:value_store",

+ 152 - 0
toolchain/driver/clang_runner.cpp

@@ -0,0 +1,152 @@
+// 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/clang_runner.h"
+
+#include <algorithm>
+#include <memory>
+#include <numeric>
+#include <optional>
+
+#include "clang/Basic/Diagnostic.h"
+#include "clang/Basic/DiagnosticOptions.h"
+#include "clang/Driver/Compilation.h"
+#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"
+#include "llvm/ADT/StringExtras.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/IR/LLVMContext.h"
+#include "llvm/Support/FileSystem.h"
+#include "llvm/Support/Path.h"
+#include "llvm/Support/Program.h"
+#include "llvm/Support/VirtualFileSystem.h"
+#include "llvm/TargetParser/Host.h"
+
+namespace Carbon {
+
+static auto GetExecutablePath(llvm::StringRef exe_name) -> std::string {
+  // If the `exe_name` isn't already a valid path, look it up.
+  if (!llvm::sys::fs::exists(exe_name)) {
+    if (llvm::ErrorOr<std::string> path_result =
+            llvm::sys::findProgramByName(exe_name)) {
+      return *path_result;
+    }
+  }
+
+  return exe_name.str();
+}
+
+ClangRunner::ClangRunner(llvm::StringRef exe_name, llvm::StringRef target,
+                         llvm::raw_ostream* vlog_stream)
+    : exe_name_(exe_name),
+      exe_path_(GetExecutablePath(exe_name)),
+      target_(target),
+      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_CHECK(!args.empty());
+  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.
+  std::array<llvm::StringRef, 1> exe_arg = {exe_name_};
+  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).drop_front()) {
+    CARBON_VLOG() << "    '" << cstr_arg << "'\n";
+  }
+
+  CARBON_VLOG() << "Preparing Clang driver...\n";
+
+  // Create the diagnostic options and parse arguments controlling them out of
+  // our arguments.
+  llvm::IntrusiveRefCntPtr<clang::DiagnosticOptions> diagnostic_options =
+      clang::CreateAndPopulateDiagOpts(cstr_args);
+
+  // TODO: We don't yet support serializing diagnostics the way the actual
+  // `clang` command line does. Unclear if we need to or not, but it would need
+  // a bit more logic here to set up chained consumers.
+  clang::TextDiagnosticPrinter diagnostic_client(llvm::errs(),
+                                                 diagnostic_options.get());
+
+  clang::DiagnosticsEngine diagnostics(
+      diagnostic_ids_, diagnostic_options.get(), &diagnostic_client,
+      /*ShouldOwnClient=*/false);
+  clang::ProcessWarningOptions(diagnostics, *diagnostic_options);
+
+  clang::driver::Driver driver(exe_path_, target_, diagnostics);
+
+  // TODO: Directly run in-process rather than using a subprocess. This is both
+  // more efficient and makes debugging (much) easier. Needs code like:
+  // driver.CC1Main = [](llvm::SmallVectorImpl<const char*>& argv) {};
+  std::unique_ptr<clang::driver::Compilation> compilation(
+      driver.BuildCompilation(cstr_args));
+  CARBON_CHECK(compilation) << "Should always successfully allocate!";
+  if (compilation->containsError()) {
+    // These should have been diagnosed by the driver.
+    return false;
+  }
+
+  CARBON_VLOG() << "Running Clang driver...\n";
+
+  llvm::SmallVector<std::pair<int, const clang::driver::Command*>>
+      failing_commands;
+  int result = driver.ExecuteCompilation(*compilation, failing_commands);
+
+  // Finish diagnosing any failures before we verbosely log the source of those
+  // failures.
+  diagnostic_client.finish();
+
+  CARBON_VLOG() << "Execution result code: " << result << "\n";
+  for (const auto& [command_result, failing_command] : failing_commands) {
+    CARBON_VLOG() << "Failing command '" << failing_command->getExecutable()
+                  << "' with code '" << command_result << "' was:\n";
+    if (vlog_stream_) {
+      failing_command->Print(*vlog_stream_, "\n\n", /*Quote=*/true);
+    }
+  }
+
+  // Return whether the command was executed successfully.
+  return result == 0 && failing_commands.empty();
+}
+
+}  // namespace Carbon

+ 60 - 0
toolchain/driver/clang_runner.h

@@ -0,0 +1,60 @@
+// 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_CLANG_RUNNER_H_
+#define CARBON_TOOLCHAIN_DRIVER_CLANG_RUNNER_H_
+
+#include "clang/Basic/DiagnosticIDs.h"
+#include "common/ostream.h"
+#include "llvm/ADT/ArrayRef.h"
+#include "llvm/ADT/StringRef.h"
+
+namespace Carbon {
+
+// Runs Clang in a similar fashion to invoking it with the provided arguments on
+// the command line. We use a textual command line interface to allow easily
+// incorporating custom command line flags from user invocations that we don't
+// parse, but will pass transparently along to Clang itself.
+//
+// This doesn't literally use a subprocess to invoke Clang; it instead tries to
+// directly use the Clang command line driver library. We also work to simplify
+// how that driver operates and invoke it in an opinionated way to get the best
+// behavior for our expected use cases in the Carbon driver:
+//
+// - Minimize canonicalization of file names to try to preserve the paths as
+//   users type them.
+// - Minimize the use of subprocess invocations which are expensive on some
+//   operating systems. To the extent possible, we try to directly invoke the
+//   Clang logic within this process.
+// - Provide programmatic API to control defaults of Clang. For example, causing
+//   verbose output.
+//
+// Note that this makes the current process behave like running Clang -- it uses
+// 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 {
+ public:
+  // Build a Clang runner that uses the provided `exe_name` and `err_stream`.
+  //
+  // If `verbose` is passed as true, will enable verbose logging to the
+  // `err_stream` both from the runner and Clang itself.
+  ClangRunner(llvm::StringRef exe_name, llvm::StringRef target,
+              llvm::raw_ostream* vlog_stream = nullptr);
+
+  // Run Clang with the provided arguments.
+  auto Run(llvm::ArrayRef<llvm::StringRef> args) -> bool;
+
+ private:
+  llvm::StringRef exe_name_;
+  std::string exe_path_;
+  llvm::StringRef target_;
+  llvm::raw_ostream* vlog_stream_;
+
+  llvm::IntrusiveRefCntPtr<clang::DiagnosticIDs> diagnostic_ids_;
+};
+
+}  // namespace Carbon
+
+#endif  // CARBON_TOOLCHAIN_DRIVER_CLANG_RUNNER_H_

+ 150 - 0
toolchain/driver/clang_runner_test.cpp

@@ -0,0 +1,150 @@
+// 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/clang_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 "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/test_raw_ostream.h"
+
+namespace Carbon {
+namespace {
+
+using ::Carbon::Testing::TestRawOstream;
+using ::testing::HasSubstr;
+using ::testing::StrEq;
+
+// While these are marked as "internal" APIs, they seem to work and be pretty
+// widely used for their exact documented behavior.
+using ::testing::internal::CaptureStderr;
+using ::testing::internal::CaptureStdout;
+using ::testing::internal::GetCapturedStderr;
+using ::testing::internal::GetCapturedStdout;
+
+// Calls the provided lambda with `stderr` and `stdout` captured and saved into
+// the provided output parameters. The lambda's result is returned. It is
+// important to not put anything inside the lambda whose output would be useful
+// in interpreting test errors such as Google Test assertions as their output
+// will end up captured as well.
+template <typename CallableT>
+static auto RunWithCapturedOutput(std::string& out, std::string& err,
+                                  CallableT callable) {
+  CaptureStderr();
+  CaptureStdout();
+  auto result = callable();
+  // No need to flush stderr.
+  err = GetCapturedStderr();
+  llvm::outs().flush();
+  out = GetCapturedStdout();
+  return result;
+}
+
+TEST(ClangRunnerTest, Version) {
+  TestRawOstream test_os;
+  std::string target = llvm::sys::getDefaultTargetTriple();
+  ClangRunner runner("./toolchain/driver/run_clang_test", target, &test_os);
+
+  std::string out;
+  std::string err;
+  EXPECT_TRUE(RunWithCapturedOutput(out, err,
+                                    [&] { return runner.Run({"--version"}); }));
+  // The arguments to Clang should be part of the verbose log.
+  EXPECT_THAT(test_os.TakeStr(), HasSubstr("--version"));
+
+  // No need to flush stderr, just check its contents.
+  EXPECT_THAT(err, StrEq(""));
+
+  // Flush and get the captured stdout to test that this command worked.
+  // We don't care about any particular version, just that it is printed.
+  EXPECT_THAT(out, HasSubstr("clang version"));
+  // The target should match what we provided.
+  EXPECT_THAT(out, HasSubstr((llvm::Twine("Target: ") + target).str()));
+  // The installation should come from the above path of the test binary.
+  EXPECT_THAT(out, HasSubstr("InstalledDir: ./toolchain/driver"));
+}
+
+// Utility to write a test file. We don't need the full power provided here yet,
+// but we anticipate adding more tests such as compiling basic C++ code in the
+// future and this provides a basis for building those tests.
+static auto WriteTestFile(llvm::StringRef name_suffix, llvm::Twine contents)
+    -> std::filesystem::path {
+  std::filesystem::path test_tmpdir;
+  if (char* tmpdir_env = getenv("TEST_TMPDIR"); tmpdir_env != nullptr) {
+    test_tmpdir = std::string(tmpdir_env);
+  } else {
+    test_tmpdir = std::filesystem::temp_directory_path();
+  }
+
+  const auto* unit_test = ::testing::UnitTest::GetInstance();
+  const auto* test_info = unit_test->current_test_info();
+  std::filesystem::path test_file =
+      test_tmpdir / llvm::formatv("{0}_{1}_{2}", test_info->test_suite_name(),
+                                  test_info->name(), name_suffix)
+                        .str();
+  // Make debugging a bit easier by cleaning up any files from previous runs.
+  // This is only necessary when not run in Bazel's test environment.
+  std::filesystem::remove(test_file);
+  CARBON_CHECK(!std::filesystem::exists(test_file));
+
+  {
+    std::error_code ec;
+    llvm::raw_fd_ostream test_file_stream(test_file.string(), ec);
+    CARBON_CHECK(!ec) << "Test file error: " << ec.message();
+    test_file_stream << contents;
+  }
+  return test_file;
+}
+
+// It's hard to write a portable and reliable unittest for all the layers of the
+// Clang driver because they work hard to interact with the underlying
+// filesystem and operating system. For now, we just check that a link command
+// is echoed back with plausible contents.
+//
+// TODO: We should eventually strive to have a more complete setup that lets us
+// test more complete Clang functionality here.
+TEST(ClangRunnerTest, LinkCommandEcho) {
+  // Just create some empty files to use in a synthetic link command below.
+  std::filesystem::path foo_file = WriteTestFile("foo.o", "");
+  std::filesystem::path bar_file = WriteTestFile("bar.o", "");
+
+  std::string verbose_out;
+  llvm::raw_string_ostream verbose_os(verbose_out);
+  std::string target = llvm::sys::getDefaultTargetTriple();
+  ClangRunner runner("./toolchain/driver/run_clang_test", target, &verbose_os);
+  std::string out;
+  std::string err;
+  EXPECT_TRUE(RunWithCapturedOutput(out, err,
+                                    [&] {
+                                      return runner.Run({"-###", "-o", "binary",
+                                                         foo_file.string(),
+                                                         bar_file.string()});
+                                    }))
+      << "Verbose output from runner:\n"
+      << verbose_out << "\n";
+
+  // Because we use `-###' above, we should just see the command that the Clang
+  // driver would have run in a subprocess. This will be very architecture
+  // dependent and have lots of variety, but we expect to see both file strings
+  // in it the command at least.
+  EXPECT_THAT(err, HasSubstr(foo_file.string())) << err;
+  EXPECT_THAT(err, HasSubstr(bar_file.string())) << err;
+
+  // And no non-stderr output should be produced.
+  EXPECT_THAT(out, StrEq(""));
+}
+
+}  // namespace
+}  // namespace Carbon

+ 153 - 27
toolchain/driver/driver.cpp

@@ -17,10 +17,12 @@
 #include "llvm/IR/LLVMContext.h"
 #include "llvm/Support/Path.h"
 #include "llvm/TargetParser/Host.h"
+#include "llvm/TargetParser/Triple.h"
 #include "toolchain/base/value_store.h"
 #include "toolchain/check/check.h"
 #include "toolchain/codegen/codegen.h"
 #include "toolchain/diagnostics/sorting_diagnostic_consumer.h"
+#include "toolchain/driver/clang_runner.h"
 #include "toolchain/lex/lex.h"
 #include "toolchain/lower/lower.h"
 #include "toolchain/parse/parse.h"
@@ -68,6 +70,30 @@ auto Driver::FindPreludeFiles(llvm::StringRef data_dir,
   return result;
 }
 
+struct Driver::CodegenOptions {
+  void Build(CommandLine::CommandBuilder& b) {
+    b.AddStringOption(
+        {
+            .name = "target",
+            .help = R"""(
+Select a target platform. Uses the LLVM target syntax. Also known as a "triple"
+for historical reasons.
+
+This corresponds to the `target` flag to Clang and accepts the same strings
+documented there:
+https://clang.llvm.org/docs/CrossCompilation.html#target-triple
+)""",
+        },
+        [&](auto& arg_b) {
+          arg_b.Default(host);
+          arg_b.Set(&target);
+        });
+  }
+
+  std::string host = llvm::sys::getDefaultTargetTriple();
+  llvm::StringRef target;
+};
+
 struct Driver::CompileOptions {
   static constexpr CommandLine::CommandInfo Info = {
       .name = "compile",
@@ -114,7 +140,7 @@ can be written to standard output as these phases progress.
     return out;
   }
 
-  void Build(CommandLine::CommandBuilder& b) {
+  void Build(CommandLine::CommandBuilder& b, CodegenOptions& codegen_options) {
     b.AddStringPositionalArg(
         {
             .name = "FILE",
@@ -168,22 +194,10 @@ object output can be forced by enabling `--force-obj-output`.
         },
         [&](auto& arg_b) { arg_b.Set(&output_filename); });
 
-    b.AddStringOption(
-        {
-            .name = "target",
-            .help = R"""(
-Select a target platform. Uses the LLVM target syntax. Also known as a "triple"
-for historical reasons.
-
-This corresponds to the `target` flag to Clang and accepts the same strings
-documented there:
-https://clang.llvm.org/docs/CrossCompilation.html#target-triple
-)""",
-        },
-        [&](auto& arg_b) {
-          arg_b.Default(host);
-          arg_b.Set(&target);
-        });
+    // Include the common code generation options at this point to render it
+    // after the more common options above, but before the more unusual options
+    // below.
+    codegen_options.Build(b);
 
     b.AddFlag(
         {
@@ -317,9 +331,6 @@ Excludes files with the given prefix from dumps.
 
   Phase phase;
 
-  std::string host = llvm::sys::getDefaultTargetTriple();
-  llvm::StringRef target;
-
   llvm::StringRef output_filename;
   llvm::SmallVector<llvm::StringRef> input_filenames;
 
@@ -340,6 +351,52 @@ Excludes files with the given prefix from dumps.
   llvm::StringRef exclude_dump_file_prefix;
 };
 
+struct Driver::LinkOptions {
+  static constexpr CommandLine::CommandInfo Info = {
+      .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.
+)""",
+  };
+
+  void Build(CommandLine::CommandBuilder& b, CodegenOptions& codegen_options) {
+    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);
+  }
+
+  llvm::StringRef output_filename;
+  llvm::SmallVector<llvm::StringRef> object_filenames;
+};
+
 struct Driver::Options {
   static constexpr CommandLine::CommandInfo Info = {
       .name = "carbon",
@@ -362,6 +419,7 @@ For questions, issues, or bug reports, please use our GitHub project:
 
   enum class Subcommand : int8_t {
     Compile,
+    Link,
   };
 
   void Build(CommandLine::CommandBuilder& b) {
@@ -375,17 +433,24 @@ For questions, issues, or bug reports, please use our GitHub project:
 
     b.AddSubcommand(CompileOptions::Info,
                     [&](CommandLine::CommandBuilder& sub_b) {
-                      compile_options.Build(sub_b);
+                      compile_options.Build(sub_b, codegen_options);
                       sub_b.Do([&] { subcommand = Subcommand::Compile; });
                     });
 
+    b.AddSubcommand(LinkOptions::Info, [&](CommandLine::CommandBuilder& sub_b) {
+      link_options.Build(sub_b, codegen_options);
+      sub_b.Do([&] { subcommand = Subcommand::Link; });
+    });
+
     b.RequiresSubcommand();
   }
 
   bool verbose;
   Subcommand subcommand;
 
+  CodegenOptions codegen_options;
   CompileOptions compile_options;
+  LinkOptions link_options;
 };
 
 auto Driver::ParseArgs(llvm::ArrayRef<llvm::StringRef> args, Options& options)
@@ -411,7 +476,9 @@ auto Driver::RunCommand(llvm::ArrayRef<llvm::StringRef> args) -> RunResult {
 
   switch (options.subcommand) {
     case Options::Subcommand::Compile:
-      return Compile(options.compile_options);
+      return Compile(options.compile_options, options.codegen_options);
+    case Options::Subcommand::Link:
+      return Link(options.link_options, options.codegen_options);
   }
   llvm_unreachable("All subcommands handled!");
 }
@@ -456,10 +523,12 @@ auto Driver::ValidateCompileOptions(const CompileOptions& options) const
 class Driver::CompilationUnit {
  public:
   explicit CompilationUnit(Driver* driver, const CompileOptions& options,
+                           const CodegenOptions& codegen_options,
                            DiagnosticConsumer* consumer,
                            llvm::StringRef input_filename)
       : driver_(driver),
         options_(options),
+        codegen_options_(codegen_options),
         input_filename_(input_filename),
         vlog_stream_(driver_->vlog_stream_) {
     if (vlog_stream_ != nullptr || options_.stream_errors) {
@@ -605,8 +674,8 @@ class Driver::CompilationUnit {
  private:
   // Do codegen. Returns true on success.
   auto RunCodeGenHelper() -> bool {
-    std::optional<CodeGen> codegen =
-        CodeGen::Make(*module_, options_.target, driver_->error_stream_);
+    std::optional<CodeGen> codegen = CodeGen::Make(
+        *module_, codegen_options_.target, driver_->error_stream_);
     if (!codegen) {
       return false;
     }
@@ -689,6 +758,7 @@ class Driver::CompilationUnit {
   Driver* driver_;
   SharedValueStores value_stores_;
   const CompileOptions& options_;
+  const CodegenOptions& codegen_options_;
   std::string input_filename_;
 
   // Copied from driver_ for CARBON_VLOG.
@@ -709,7 +779,8 @@ class Driver::CompilationUnit {
   std::unique_ptr<llvm::Module> module_;
 };
 
-auto Driver::Compile(const CompileOptions& options) -> RunResult {
+auto Driver::Compile(const CompileOptions& options,
+                     const CodegenOptions& codegen_options) -> RunResult {
   if (!ValidateCompileOptions(options)) {
     return {.success = false};
   }
@@ -733,13 +804,13 @@ auto Driver::Compile(const CompileOptions& options) -> RunResult {
   // Add the prelude files.
   for (const auto& input_filename : prelude) {
     units.push_back(std::make_unique<CompilationUnit>(
-        this, options, &stream_consumer, input_filename));
+        this, options, codegen_options, &stream_consumer, input_filename));
   }
 
   // Add the input source files.
   for (const auto& input_filename : options.input_filenames) {
     units.push_back(std::make_unique<CompilationUnit>(
-        this, options, &stream_consumer, input_filename));
+        this, options, codegen_options, &stream_consumer, input_filename));
   }
 
   auto on_exit = llvm::make_scope_exit([&]() {
@@ -829,4 +900,59 @@ auto Driver::Compile(const CompileOptions& options) -> RunResult {
   return make_result();
 }
 
+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;
+  }
+}
+
+auto Driver::Link(const LinkOptions& options,
+                  const CodegenOptions& codegen_options) -> RunResult {
+  // 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++");
+
+  // Add OS-specific flags based on the target.
+  AddOSFlags(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("FIXME", codegen_options.target, vlog_stream_);
+  return {.success = runner.Run(clang_args)};
+}
+
 }  // namespace Carbon

+ 8 - 1
toolchain/driver/driver.h

@@ -57,7 +57,9 @@ class Driver {
 
  private:
   struct Options;
+  struct CodegenOptions;
   struct CompileOptions;
+  struct LinkOptions;
   class CompilationUnit;
 
   // Delegates to the command line library to parse the arguments and store the
@@ -70,7 +72,12 @@ class Driver {
   auto ValidateCompileOptions(const CompileOptions& options) const -> bool;
 
   // Implements the compile subcommand of the driver.
-  auto Compile(const CompileOptions& options) -> RunResult;
+  auto Compile(const CompileOptions& options,
+               const CodegenOptions& codegen_options) -> RunResult;
+
+  // Implements the link subcommand of the driver.
+  auto Link(const LinkOptions& options, const CodegenOptions& codegen_options)
+      -> RunResult;
 
   // The filesystem for source code.
   llvm::vfs::FileSystem& fs_;