Reworking LLVM AOT linking to pick a linker tool based on target env.
This adds the WindowsLinkerTool that uses lld-link or link to generate
the DLLs/PDBs. The existing behavior is modeled in UnixLinkerTool that
is meant for use with ld/ld.lld.

TODOs are in place for mac mach-o and wasm linking, and the UnixLinkerTool
could use a lot of work (left for someone who knows more about ld).
diff --git a/iree/compiler/Dialect/HAL/Target/LLVM/AOT/BUILD b/iree/compiler/Dialect/HAL/Target/LLVM/AOT/BUILD
index 594a0be..0c81851 100644
--- a/iree/compiler/Dialect/HAL/Target/LLVM/AOT/BUILD
+++ b/iree/compiler/Dialect/HAL/Target/LLVM/AOT/BUILD
@@ -38,7 +38,7 @@
         "LLVMAOTTarget.h",
     ],
     deps = [
-        ":LLVMAOTTargetLinker",
+        ":LinkerTool",
         "//iree/compiler/Dialect/HAL/Target",
         "//iree/compiler/Dialect/HAL/Target/LLVM:LLVMBaseTarget",
         "//iree/compiler/Dialect/HAL/Target/LLVM:LLVMIRPasses",
@@ -57,16 +57,19 @@
 )
 
 cc_library(
-    name = "LLVMAOTTargetLinker",
-    hdrs = ["LLVMAOTTargetLinker.h"],
-    deps = platform_trampoline_deps("LLVMAOTTargetLinker", "compiler/Dialect/HAL/Target/LLVM/AOT"),
+    name = "LinkerTool",
+    srcs = ["LinkerTool.cpp"],
+    hdrs = ["LinkerTool.h"],
+    deps = platform_trampoline_deps("LinkerTools", "compiler/Dialect/HAL/Target/LLVM/AOT"),
 )
 
 cc_library(
-    name = "LLVMAOTTargetLinker_hdrs",
-    hdrs = ["LLVMAOTTargetLinker.h"],
+    name = "LinkerTool_hdrs",
+    hdrs = ["LinkerTool.h"],
     deps = [
-        "//iree/base:status",
         "//iree/compiler/Dialect/HAL/Target/LLVM:LLVMTargetOptions",
+        "@llvm-project//llvm:Core",
+        "@llvm-project//llvm:Support",
+        "@llvm-project//mlir:Support",
     ],
 )
diff --git a/iree/compiler/Dialect/HAL/Target/LLVM/AOT/CMakeLists.txt b/iree/compiler/Dialect/HAL/Target/LLVM/AOT/CMakeLists.txt
index ff46a62..7f69d4d 100644
--- a/iree/compiler/Dialect/HAL/Target/LLVM/AOT/CMakeLists.txt
+++ b/iree/compiler/Dialect/HAL/Target/LLVM/AOT/CMakeLists.txt
@@ -26,7 +26,7 @@
   SRCS
     "LLVMAOTTarget.cpp"
   DEPS
-    ::LLVMAOTTargetLinker
+    ::LinkerTool
     LLVMAArch64AsmParser
     LLVMAArch64CodeGen
     LLVMARMAsmParser
@@ -46,21 +46,25 @@
 
 iree_cc_library(
   NAME
-    LLVMAOTTargetLinker
+    LinkerTool
   HDRS
-    "LLVMAOTTargetLinker.h"
+    "LinkerTool.h"
+  SRCS
+    "LinkerTool.cpp"
   DEPS
-    iree::compiler::Dialect::HAL::Target::LLVM::AOT::internal::LLVMAOTTargetLinker_internal
+    iree::compiler::Dialect::HAL::Target::LLVM::AOT::internal::LinkerTools_internal
   PUBLIC
 )
 
 iree_cc_library(
   NAME
-    LLVMAOTTargetLinker_hdrs
+    LinkerTool_hdrs
   HDRS
-    "LLVMAOTTargetLinker.h"
+    "LinkerTool.h"
   DEPS
-    iree::base::status
+    LLVMCore
+    LLVMSupport
+    MLIRSupport
     iree::compiler::Dialect::HAL::Target::LLVM::LLVMTargetOptions
   PUBLIC
 )
diff --git a/iree/compiler/Dialect/HAL/Target/LLVM/AOT/LLVMAOTTarget.cpp b/iree/compiler/Dialect/HAL/Target/LLVM/AOT/LLVMAOTTarget.cpp
index 6060600..d86c12c 100644
--- a/iree/compiler/Dialect/HAL/Target/LLVM/AOT/LLVMAOTTarget.cpp
+++ b/iree/compiler/Dialect/HAL/Target/LLVM/AOT/LLVMAOTTarget.cpp
@@ -16,7 +16,7 @@
 
 #include <cstdlib>
 
-#include "iree/compiler/Dialect/HAL/Target/LLVM/AOT/LLVMAOTTargetLinker.h"
+#include "iree/compiler/Dialect/HAL/Target/LLVM/AOT/LinkerTool.h"
 #include "iree/compiler/Dialect/HAL/Target/LLVM/LLVMBaseTarget.h"
 #include "iree/compiler/Dialect/HAL/Target/LLVM/LLVMIRPasses.h"
 #include "iree/compiler/Dialect/HAL/Target/TargetRegistry.h"
@@ -46,16 +46,20 @@
     // multi-threading issues.
     llvm::LLVMContext context;
 
-    // Remove all private functions, e.g tile size calcuations.
-    SmallVector<FuncOp, 4> nonPublicFn;
-    for (auto func : targetOp.getInnerModule().getOps<FuncOp>()) {
-      if (SymbolTable::getSymbolVisibility(func) !=
-          SymbolTable::Visibility::Public) {
-        nonPublicFn.push_back(func);
-      }
-    }
-    for (auto func : nonPublicFn) {
-      func.erase();
+    // We name our files after the executable name so that they are easy to
+    // track both during compilation (logs/artifacts/etc), as outputs (final
+    // intermediate code/binary files), and at runtime (loaded
+    // libraries/symbols/etc).
+    auto libraryName =
+        targetOp.getParentOfType<IREE::HAL::ExecutableOp>().getName().str();
+
+    // TODO(#3737): don't add functions we don't want to serialize to the
+    // module. Right now workgroup count calculation functions end up in here
+    // as std.func ops and not just the llvm.func ops we expect.
+    auto illegalFuncOps =
+        llvm::to_vector<4>(targetOp.getInnerModule().getOps<FuncOp>());
+    for (auto funcOp : illegalFuncOps) {
+      funcOp.erase();
     }
 
     // At this moment we are leaving MLIR LLVM dialect land translating module
@@ -63,63 +67,107 @@
     auto llvmModule =
         mlir::translateModuleToLLVMIR(targetOp.getInnerModule(), context);
     if (!llvmModule) {
-      return failure();
+      return targetOp.emitError() << "failed to translate the MLIR LLVM "
+                                     "dialect to the native llvm::Module";
     }
 
+    // Export all entry points such that they are accessible on the dynamic
+    // libraries we generate.
     iree::DyLibExecutableDefT dyLibExecutableDef;
-    // Create invocation function an populate entry_points.
-    auto entryPointOps = targetOp.getBlock().getOps<ExecutableEntryPointOp>();
-
-    for (auto entryPointOp : entryPointOps) {
+    SmallVector<StringRef, 8> entryPointNames;
+    for (auto entryPointOp :
+         targetOp.getBlock().getOps<ExecutableEntryPointOp>()) {
       dyLibExecutableDef.entry_points.push_back(
           std::string(entryPointOp.sym_name()));
+      entryPointNames.push_back(entryPointOp.sym_name());
     }
 
-    // LLVMIR opt passes.
+    // Try to grab a linker tool based on the options (and target environment).
+    llvm::Triple targetTriple(options_.targetTriple);
+    auto linkerTool = LinkerTool::getForTarget(targetTriple, options_);
+    if (!linkerTool) {
+      return mlir::emitError(targetOp.getLoc())
+             << "failed to find a target linker for the given target triple '"
+             << options_.targetTriple << "'";
+    }
+
+    // Configure the module with any code generation options required later by
+    // linking (such as initializer functions).
+    if (failed(
+            linkerTool->configureModule(llvmModule.get(), entryPointNames))) {
+      return targetOp.emitError()
+             << "failed to configure LLVM module for target linker";
+    }
+
+    // LLVM opt passes that perform code generation optimizations/transformation
+    // similar to what a frontend would do before passing to linking.
     auto targetMachine = createTargetMachine(options_);
     if (!targetMachine) {
-      targetOp.emitError("Can't create target machine for target triple: " +
-                         options_.targetTriple);
-      return failure();
+      return mlir::emitError(targetOp.getLoc())
+             << "failed to create target machine for target triple '"
+             << options_.targetTriple << "'";
     }
-
     llvmModule->setDataLayout(targetMachine->createDataLayout());
     llvmModule->setTargetTriple(targetMachine->getTargetTriple().str());
-
     if (failed(
             runLLVMIRPasses(options_, targetMachine.get(), llvmModule.get()))) {
-      return targetOp.emitError(
-          "Can't build LLVMIR opt passes for ExecutableOp module");
+      return targetOp.emitError()
+             << "failed to run LLVM-IR opt passes for IREE::HAL::ExecutableOp "
+                "targeting '"
+             << options_.targetTriple << "'";
     }
 
-    std::string objData;
-    if (failed(runEmitObjFilePasses(targetMachine.get(), llvmModule.get(),
-                                    &objData))) {
-      return targetOp.emitError("Can't compile LLVMIR module to an obj");
+    // Emit object files.
+    SmallVector<Artifact, 4> objectFiles;
+    {
+      // NOTE: today we just use a single object file, however if we wanted to
+      // scale code generation and linking we'd want to generate one per
+      // function (or something like that).
+      std::string objectData;
+      if (failed(runEmitObjFilePasses(targetMachine.get(), llvmModule.get(),
+                                      &objectData))) {
+        return targetOp.emitError()
+               << "failed to compile LLVM-IR module to an object file";
+      }
+      auto objectFile = Artifact::createTemporary(libraryName, "obj");
+      auto &os = objectFile.outputFile->os();
+      os << objectData;
+      os.flush();
+      os.close();
+      objectFiles.push_back(std::move(objectFile));
     }
 
-    std::string sharedLibData;
-    const char *linkerToolPath = std::getenv("IREE_LLVMAOT_LINKER_PATH");
-    if (linkerToolPath != nullptr) {
-      auto sharedLibDataStatus = linkLLVMAOTObjects(linkerToolPath, objData);
-      if (!sharedLibDataStatus.ok()) {
-        return targetOp.emitError(
-            "Can't link executable and generate target dylib, using linker "
-            "toolchain: '" +
-            std::string(linkerToolPath) + "'");
-      }
-      sharedLibData = sharedLibDataStatus.value();
-    } else {
-      auto sharedLibDataStatus = linkLLVMAOTObjectsWithLLDElf(objData);
-      if (!sharedLibDataStatus.ok()) {
-        return targetOp.emitError(
-            "Can't link executable and generate target dylib using "
-            "lld::elf::link");
-      }
-      sharedLibData = sharedLibDataStatus.value();
+    // Link the generated object files into a dylib.
+    auto linkArtifactsOr =
+        linkerTool->linkDynamicLibrary(libraryName, objectFiles);
+    if (!linkArtifactsOr.hasValue()) {
+      return mlir::emitError(targetOp.getLoc())
+             << "failed to link executable and generate target dylib using "
+                "linker toolchain "
+             << linkerTool->getToolPath();
     }
-    dyLibExecutableDef.library_embedded = {sharedLibData.begin(),
-                                           sharedLibData.end()};
+    auto &linkArtifacts = linkArtifactsOr.getValue();
+    dyLibExecutableDef.library_embedded =
+        linkArtifacts.libraryFile.read().getValueOr(std::vector<int8_t>());
+    if (dyLibExecutableDef.library_embedded.empty()) {
+      return targetOp.emitError() << "failed to read back dylib temp file at "
+                                  << linkArtifacts.libraryFile.path;
+    }
+
+    if (options_.debugSymbols && linkArtifacts.debugFile.outputFile) {
+      dyLibExecutableDef.debug_database_embedded =
+          linkArtifacts.debugFile.read().getValue();
+      assert(!dyLibExecutableDef.debug_database_embedded.empty());
+      dyLibExecutableDef.debug_database_filename =
+          llvm::sys::path::filename(linkArtifacts.debugFile.path).str();
+    }
+
+    if (options_.keepLinkerArtifacts) {
+      return mlir::emitRemark(targetOp.getLoc())
+             << "Linker artifacts for " << targetOp.getName() << " preserved:\n"
+             << "    " << linkArtifacts.libraryFile.path;
+      linkArtifacts.keepAllFiles();
+    }
 
     ::flatbuffers::FlatBufferBuilder fbb;
     auto executableOffset =
diff --git a/iree/compiler/Dialect/HAL/Target/LLVM/AOT/LLVMAOTTargetLinker.h b/iree/compiler/Dialect/HAL/Target/LLVM/AOT/LLVMAOTTargetLinker.h
deleted file mode 100644
index d6d5220..0000000
--- a/iree/compiler/Dialect/HAL/Target/LLVM/AOT/LLVMAOTTargetLinker.h
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright 2020 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      https://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-
-#ifndef IREE_COMPILER_DIALECT_HAL_TARGET_LLVM_AOT_LLVMAOTTARGETLINKER_H_
-#define IREE_COMPILER_DIALECT_HAL_TARGET_LLVM_AOT_LLVMAOTTARGETLINKER_H_
-
-#include <string>
-
-#include "iree/base/status.h"
-#include "iree/compiler/Dialect/HAL/Target/LLVM/LLVMTargetOptions.h"
-
-namespace mlir {
-namespace iree_compiler {
-namespace IREE {
-namespace HAL {
-
-// Calls linker tool to link objData and returns shared library blob.
-iree::StatusOr<std::string> linkLLVMAOTObjects(
-    const std::string& linkerToolPath, const std::string& objData);
-// Use lld::elf::link for linking objData and returns shared library blob.
-iree::StatusOr<std::string> linkLLVMAOTObjectsWithLLDElf(
-    const std::string& objData);
-
-}  // namespace HAL
-}  // namespace IREE
-}  // namespace iree_compiler
-}  // namespace mlir
-
-#endif  // IREE_COMPILER_DIALECT_HAL_TARGET_LLVM_AOT_LLVMAOTTARGETLINKER_H_
diff --git a/iree/compiler/Dialect/HAL/Target/LLVM/AOT/LinkerTool.cpp b/iree/compiler/Dialect/HAL/Target/LLVM/AOT/LinkerTool.cpp
new file mode 100644
index 0000000..033fe0e
--- /dev/null
+++ b/iree/compiler/Dialect/HAL/Target/LLVM/AOT/LinkerTool.cpp
@@ -0,0 +1,105 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "iree/compiler/Dialect/HAL/Target/LLVM/AOT/LinkerTool.h"
+
+#define DEBUG_TYPE "llvmaot-linker"
+
+namespace mlir {
+namespace iree_compiler {
+namespace IREE {
+namespace HAL {
+
+// static
+Artifact Artifact::createTemporary(StringRef prefix, StringRef suffix) {
+  llvm::SmallString<32> filePath;
+  if (std::error_code error =
+          llvm::sys::fs::createTemporaryFile(prefix, suffix, filePath)) {
+    llvm::errs() << "failed to generate temporary file: " << error.message();
+    return {};
+  }
+  std::error_code error;
+  auto file = std::make_unique<llvm::ToolOutputFile>(filePath, error,
+                                                     llvm::sys::fs::OF_None);
+  if (error) {
+    llvm::errs() << "failed to open temporary file '" << filePath
+                 << "': " << error.message();
+    return {};
+  }
+  return {filePath.str().str(), std::move(file)};
+}
+
+// static
+Artifact Artifact::createVariant(StringRef basePath, StringRef suffix) {
+  SmallString<32> filePath(basePath);
+  llvm::sys::path::replace_extension(filePath, suffix);
+  std::error_code error;
+  auto file = std::make_unique<llvm::ToolOutputFile>(filePath, error,
+                                                     llvm::sys::fs::OF_Append);
+  if (error) {
+    llvm::errs() << "failed to open temporary file '" << filePath
+                 << "': " << error.message();
+    return {};
+  }
+  return {filePath.str().str(), std::move(file)};
+}
+
+Optional<std::vector<int8_t>> Artifact::read() const {
+  auto fileData = llvm::MemoryBuffer::getFile(path);
+  if (!fileData) {
+    llvm::errs() << "failed to load library output file '" << path << "'";
+    return llvm::None;
+  }
+  auto sourceBuffer = fileData.get()->getBuffer();
+  std::vector<int8_t> resultBuffer(sourceBuffer.size());
+  std::memcpy(resultBuffer.data(), sourceBuffer.data(), sourceBuffer.size());
+  return resultBuffer;
+}
+
+void Artifact::close() { outputFile->os().close(); }
+
+void Artifacts::keepAllFiles() {
+  if (libraryFile.outputFile) libraryFile.outputFile->keep();
+  if (debugFile.outputFile) debugFile.outputFile->keep();
+  for (auto &file : otherFiles) {
+    file.outputFile->keep();
+  }
+}
+
+std::string LinkerTool::getToolPath() const {
+  return std::string(std::getenv("IREE_LLVMAOT_LINKER_PATH"));
+}
+
+LogicalResult LinkerTool::runLinkCommand(const std::string &commandLine) {
+  LLVM_DEBUG(llvm::dbgs() << "Running linker command:\n" << commandLine);
+#if defined(_MSC_VER)
+  // It's easy to run afoul of quoting rules on Windows (such as when using
+  // spaces in the linker environment variable). See:
+  // https://stackoverflow.com/a/9965141
+  auto quotedCommandLine = "\"" + commandLine + "\"";
+  int exitCode = system(quotedCommandLine.c_str());
+#else
+  int exitCode = system(commandLine.c_str());
+#endif  // _MSC_VER
+  if (exitCode == 0) return success();
+  llvm::errs() << "Linking failed; command line returned exit code " << exitCode
+               << ":\n\n"
+               << commandLine << "\n\n";
+  return failure();
+}
+
+}  // namespace HAL
+}  // namespace IREE
+}  // namespace iree_compiler
+}  // namespace mlir
diff --git a/iree/compiler/Dialect/HAL/Target/LLVM/AOT/LinkerTool.h b/iree/compiler/Dialect/HAL/Target/LLVM/AOT/LinkerTool.h
new file mode 100644
index 0000000..6f3c39a
--- /dev/null
+++ b/iree/compiler/Dialect/HAL/Target/LLVM/AOT/LinkerTool.h
@@ -0,0 +1,117 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#ifndef IREE_COMPILER_DIALECT_HAL_TARGET_LLVM_AOT_LINKERTOOL_H_
+#define IREE_COMPILER_DIALECT_HAL_TARGET_LLVM_AOT_LINKERTOOL_H_
+
+#include <string>
+
+#include "iree/compiler/Dialect/HAL/Target/LLVM/LLVMTargetOptions.h"
+#include "llvm/ADT/Triple.h"
+#include "llvm/IR/Module.h"
+#include "llvm/Support/ToolOutputFile.h"
+#include "mlir/Support/LogicalResult.h"
+
+namespace mlir {
+namespace iree_compiler {
+namespace IREE {
+namespace HAL {
+
+struct Artifact {
+  // Creates an output file path/container pair.
+  // By default the file will be deleted when the link completes; callers must
+  // use llvm::ToolOutputFile::keep() to prevent deletion upon success (or if
+  // leaving artifacts for debugging).
+  static Artifact createTemporary(StringRef prefix, StringRef suffix);
+
+  // Creates an output file derived from the given file's path with a new
+  // suffix.
+  static Artifact createVariant(StringRef basePath, StringRef suffix);
+
+  Artifact() = default;
+  Artifact(std::string path, std::unique_ptr<llvm::ToolOutputFile> outputFile)
+      : path(std::move(path)), outputFile(std::move(outputFile)) {}
+
+  std::string path;
+  std::unique_ptr<llvm::ToolOutputFile> outputFile;
+
+  // Reads the artifact file contents as bytes.
+  Optional<std::vector<int8_t>> read() const;
+
+  // Closes the ostream of the file while preserving the temporary entry on
+  // disk. Use this if files need to be modified by external tools that may
+  // require exclusive access.
+  void close();
+};
+
+struct Artifacts {
+  // File containing the linked library (DLL, ELF, etc).
+  Artifact libraryFile;
+
+  // Optional file containing associated debug information (if stored
+  // separately, such as PDB files).
+  Artifact debugFile;
+
+  // Other files associated with linking.
+  SmallVector<Artifact, 4> otherFiles;
+
+  // Keeps all of the artifacts around after linking completes. Useful for
+  // debugging.
+  void keepAllFiles();
+};
+
+// Base type for linker tools that can turn object files into shared objects.
+class LinkerTool {
+ public:
+  // Gets an instance of a linker tool for the given target options. This may
+  // be a completely different toolchain than that of the host.
+  static std::unique_ptr<LinkerTool> getForTarget(
+      llvm::Triple& targetTriple, LLVMTargetOptions& targetOptions);
+
+  explicit LinkerTool(llvm::Triple targetTriple,
+                      LLVMTargetOptions targetOptions)
+      : targetTriple(std::move(targetTriple)),
+        targetOptions(std::move(targetOptions)) {}
+
+  virtual ~LinkerTool() = default;
+
+  // Returns the path to the linker tool binary.
+  virtual std::string getToolPath() const;
+
+  // Configures a module prior to compilation with any additional
+  // functions/exports it may need, such as shared object initializer functions.
+  virtual LogicalResult configureModule(
+      llvm::Module* llvmModule, ArrayRef<StringRef> entryPointNames) = 0;
+
+  // Links the given object files into a dynamically loadable library.
+  // The resulting library (and other associated artifacts) will be returned on
+  // success.
+  virtual Optional<Artifacts> linkDynamicLibrary(
+      StringRef libraryName, ArrayRef<Artifact> objectFiles) = 0;
+
+ protected:
+  // Runs the given command line on the shell, logging failures.
+  LogicalResult runLinkCommand(const std::string& commandLine);
+
+  llvm::Triple targetTriple;
+  LLVMTargetOptions targetOptions;
+};
+
+}  // namespace HAL
+}  // namespace IREE
+}  // namespace iree_compiler
+}  // namespace mlir
+
+#endif  // IREE_COMPILER_DIALECT_HAL_TARGET_LLVM_AOT_LINKERTOOL_H_
diff --git a/iree/compiler/Dialect/HAL/Target/LLVM/AOT/internal/BUILD b/iree/compiler/Dialect/HAL/Target/LLVM/AOT/internal/BUILD
index 6dadc26..bc8a6e0 100644
--- a/iree/compiler/Dialect/HAL/Target/LLVM/AOT/internal/BUILD
+++ b/iree/compiler/Dialect/HAL/Target/LLVM/AOT/internal/BUILD
@@ -19,11 +19,16 @@
 )
 
 cc_library(
-    name = "LLVMAOTTargetLinker_internal",
-    srcs = ["LLVMAOTTargetLinker.cpp"],
+    name = "LinkerTools_internal",
+    srcs = [
+        "LinkerTools.cpp",
+        "UnixLinkerTool.cpp",
+        "WindowsLinkerTool.cpp",
+    ],
     deps = [
-        "//iree/base:status",
-        "//iree/compiler/Dialect/HAL/Target/LLVM/AOT:LLVMAOTTargetLinker_hdrs",
+        "//iree/compiler/Dialect/HAL/Target/LLVM/AOT:LinkerTool_hdrs",
+        "@llvm-project//llvm:Core",
         "@llvm-project//llvm:Support",
+        "@llvm-project//mlir:Support",
     ],
 )
diff --git a/iree/compiler/Dialect/HAL/Target/LLVM/AOT/internal/CMakeLists.txt b/iree/compiler/Dialect/HAL/Target/LLVM/AOT/internal/CMakeLists.txt
index 01cb459..aafc4a5 100644
--- a/iree/compiler/Dialect/HAL/Target/LLVM/AOT/internal/CMakeLists.txt
+++ b/iree/compiler/Dialect/HAL/Target/LLVM/AOT/internal/CMakeLists.txt
@@ -16,12 +16,15 @@
 
 iree_cc_library(
   NAME
-    LLVMAOTTargetLinker_internal
+    LinkerTools_internal
   SRCS
-    "LLVMAOTTargetLinker.cpp"
+    "LinkerTools.cpp"
+    "UnixLinkerTool.cpp"
+    "WindowsLinkerTool.cpp"
   DEPS
+    LLVMCore
     LLVMSupport
-    iree::base::status
-    iree::compiler::Dialect::HAL::Target::LLVM::AOT::LLVMAOTTargetLinker_hdrs
+    MLIRSupport
+    iree::compiler::Dialect::HAL::Target::LLVM::AOT::LinkerTool_hdrs
   PUBLIC
 )
diff --git a/iree/compiler/Dialect/HAL/Target/LLVM/AOT/internal/LLVMAOTTargetLinker.cpp b/iree/compiler/Dialect/HAL/Target/LLVM/AOT/internal/LLVMAOTTargetLinker.cpp
deleted file mode 100644
index 8acd2d6..0000000
--- a/iree/compiler/Dialect/HAL/Target/LLVM/AOT/internal/LLVMAOTTargetLinker.cpp
+++ /dev/null
@@ -1,79 +0,0 @@
-// Copyright 2020 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      https://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include "iree/compiler/Dialect/HAL/Target/LLVM/AOT/LLVMAOTTargetLinker.h"
-
-#include "iree/base/status.h"
-#include "llvm/Support/ToolOutputFile.h"
-
-namespace mlir {
-namespace iree_compiler {
-namespace IREE {
-namespace HAL {
-
-iree::StatusOr<std::string> linkLLVMAOTObjects(
-    const std::string& linkerToolPath, const std::string& objData) {
-  llvm::SmallString<32> objFilePath, dylibFilePath;
-  if (std::error_code error = llvm::sys::fs::createTemporaryFile(
-          "llvmaot_dylibs", "objfile", objFilePath)) {
-    return iree::InternalErrorBuilder(IREE_LOC)
-           << "Failed to generate temporary file for objfile : '"
-           << error.message() << "'";
-  }
-  if (std::error_code error = llvm::sys::fs::createTemporaryFile(
-          "llvmaot_dylibs", "dylibfile", dylibFilePath)) {
-    return iree::InternalErrorBuilder(IREE_LOC)
-           << "Failed to generate temporary file for dylib : '"
-           << error.message() << "'";
-  }
-  std::error_code error;
-  auto outputFile = std::make_unique<llvm::ToolOutputFile>(
-      objFilePath, error, llvm::sys::fs::F_None);
-  if (error) {
-    return iree::InternalErrorBuilder(IREE_LOC)
-           << "Failed to open temporary objfile '" << objFilePath.c_str()
-           << "' for dylib : '" << error.message() << "'";
-  }
-
-  outputFile->os() << objData;
-  outputFile->os().flush();
-
-  auto linkingCmd =
-      (linkerToolPath + " -shared " + objFilePath + " -o " + dylibFilePath)
-          .str();
-  int systemRet = system(linkingCmd.c_str());
-  if (systemRet != 0) {
-    return iree::InternalErrorBuilder(IREE_LOC)
-           << linkingCmd << " failed with exit code " << systemRet;
-  }
-
-  auto dylibData = llvm::MemoryBuffer::getFile(dylibFilePath);
-  if (!dylibData) {
-    return iree::InternalErrorBuilder(IREE_LOC)
-           << "Failed to read temporary dylib file '" << dylibFilePath.c_str()
-           << "'";
-  }
-  return dylibData.get()->getBuffer().str();
-}
-
-iree::StatusOr<std::string> linkLLVMAOTObjectsWithLLDElf(
-    const std::string& objData) {
-  return iree::UnimplementedErrorBuilder(IREE_LOC)
-         << "linkLLVMAOTObjectsWithLLD not implemented yet!";
-}
-
-}  // namespace HAL
-}  // namespace IREE
-}  // namespace iree_compiler
-}  // namespace mlir
diff --git a/iree/compiler/Dialect/HAL/Target/LLVM/AOT/internal/LinkerTools.cpp b/iree/compiler/Dialect/HAL/Target/LLVM/AOT/internal/LinkerTools.cpp
new file mode 100644
index 0000000..6fc4b93
--- /dev/null
+++ b/iree/compiler/Dialect/HAL/Target/LLVM/AOT/internal/LinkerTools.cpp
@@ -0,0 +1,43 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "iree/compiler/Dialect/HAL/Target/LLVM/AOT/LinkerTool.h"
+
+namespace mlir {
+namespace iree_compiler {
+namespace IREE {
+namespace HAL {
+
+// TODO(benvanik): add other platforms:
+// createMacLinkerTool using ld64.lld
+// createWasmLinkerTool wasm-ld
+
+std::unique_ptr<LinkerTool> createUnixLinkerTool(
+    llvm::Triple &targetTriple, LLVMTargetOptions &targetOptions);
+std::unique_ptr<LinkerTool> createWindowsLinkerTool(
+    llvm::Triple &targetTriple, LLVMTargetOptions &targetOptions);
+
+// static
+std::unique_ptr<LinkerTool> LinkerTool::getForTarget(
+    llvm::Triple &targetTriple, LLVMTargetOptions &targetOptions) {
+  if (targetTriple.isOSWindows() || targetTriple.isWindowsMSVCEnvironment()) {
+    return createWindowsLinkerTool(targetTriple, targetOptions);
+  }
+  return createUnixLinkerTool(targetTriple, targetOptions);
+}
+
+}  // namespace HAL
+}  // namespace IREE
+}  // namespace iree_compiler
+}  // namespace mlir
diff --git a/iree/compiler/Dialect/HAL/Target/LLVM/AOT/internal/UnixLinkerTool.cpp b/iree/compiler/Dialect/HAL/Target/LLVM/AOT/internal/UnixLinkerTool.cpp
new file mode 100644
index 0000000..fcd1986
--- /dev/null
+++ b/iree/compiler/Dialect/HAL/Target/LLVM/AOT/internal/UnixLinkerTool.cpp
@@ -0,0 +1,86 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "iree/compiler/Dialect/HAL/Target/LLVM/AOT/LinkerTool.h"
+#include "llvm/IR/Function.h"
+#include "llvm/IR/IRBuilder.h"
+#include "llvm/Support/FormatVariadic.h"
+
+#define DEBUG_TYPE "llvmaot-linker"
+
+namespace mlir {
+namespace iree_compiler {
+namespace IREE {
+namespace HAL {
+
+// Unix linker (ld-like); for ELF files.
+class UnixLinkerTool : public LinkerTool {
+ public:
+  using LinkerTool::LinkerTool;
+
+  std::string getToolPath() const override {
+    auto toolPath = LinkerTool::getToolPath();
+    return toolPath.empty() ? "ld.lld" : toolPath;
+  }
+
+  LogicalResult configureModule(llvm::Module *llvmModule,
+                                ArrayRef<StringRef> entryPointNames) override {
+    // Possibly a no-op in ELF files; needs to be verified.
+    return success();
+  }
+
+  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, 8> flags = {
+        getToolPath(),
+        "-shared",
+        "-o " + artifacts.libraryFile.path,
+    };
+
+    // TODO(ataei): add flags based on targetTriple.isAndroid(), like
+    //   -static-libstdc++ (if this is needed, which it shouldn't be).
+
+    // 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);
+    }
+
+    auto commandLine = llvm::join(flags, " ");
+    if (failed(runLinkCommand(commandLine))) return llvm::None;
+    return artifacts;
+  }
+};
+
+std::unique_ptr<LinkerTool> createUnixLinkerTool(
+    llvm::Triple &targetTriple, LLVMTargetOptions &targetOptions) {
+  return std::make_unique<UnixLinkerTool>(targetTriple, targetOptions);
+}
+
+}  // namespace HAL
+}  // namespace IREE
+}  // namespace iree_compiler
+}  // namespace mlir
diff --git a/iree/compiler/Dialect/HAL/Target/LLVM/AOT/internal/WindowsLinkerTool.cpp b/iree/compiler/Dialect/HAL/Target/LLVM/AOT/internal/WindowsLinkerTool.cpp
new file mode 100644
index 0000000..40632e3
--- /dev/null
+++ b/iree/compiler/Dialect/HAL/Target/LLVM/AOT/internal/WindowsLinkerTool.cpp
@@ -0,0 +1,290 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "iree/compiler/Dialect/HAL/Target/LLVM/AOT/LinkerTool.h"
+#include "llvm/IR/Function.h"
+#include "llvm/IR/IRBuilder.h"
+#include "llvm/Support/FormatVariadic.h"
+
+#define DEBUG_TYPE "llvmaot-linker"
+
+namespace mlir {
+namespace iree_compiler {
+namespace IREE {
+namespace HAL {
+
+// Windows linker (MSVC link.exe-like); for DLL files.
+class WindowsLinkerTool : public LinkerTool {
+ public:
+  using LinkerTool::LinkerTool;
+
+  std::string getToolPath() const override {
+    auto toolPath = LinkerTool::getToolPath();
+    return toolPath.empty() ? "lld-link" : toolPath;
+  }
+
+  LogicalResult configureModule(llvm::Module *llvmModule,
+                                ArrayRef<StringRef> entryPointNames) override {
+    auto &ctx = llvmModule->getContext();
+
+    // Create a _DllMainCRTStartup replacement that does not initialize the CRT.
+    // This is required to prevent a bunch of CRT junk (locale, errno, TLS, etc)
+    // from getting emitted in such a way that it cannot be stripped by LTCG.
+    // Since we don't emit code using the CRT (beyond memset/memcpy) this is
+    // fine and can reduce binary sizes by 50-100KB.
+    //
+    // More info:
+    // https://docs.microsoft.com/en-us/cpp/build/run-time-library-behavior?view=vs-2019
+    {
+      auto dwordType = llvm::IntegerType::get(ctx, 32);
+      auto ptrType = llvm::PointerType::getUnqual(dwordType);
+      auto entry = cast<llvm::Function>(
+          llvmModule
+              ->getOrInsertFunction("iree_dll_main", dwordType, ptrType,
+                                    dwordType, ptrType)
+              .getCallee());
+      entry->setCallingConv(llvm::CallingConv::X86_StdCall);
+      entry->setDLLStorageClass(
+          llvm::GlobalValue::DLLStorageClassTypes::DLLExportStorageClass);
+      entry->setLinkage(llvm::GlobalValue::LinkageTypes::ExternalLinkage);
+      auto *block = llvm::BasicBlock::Create(ctx, "entry", entry);
+      llvm::IRBuilder<> builder(block);
+      auto one = llvm::ConstantInt::get(dwordType, 1, false);
+      builder.CreateRet(one);
+    }
+
+    // For now we ensure that our entry points are exported (via linker
+    // directives embedded in the object file) and in a compatible calling
+    // convention.
+    // TODO(benvanik): switch to executable libraries w/ internal functions.
+    for (auto entryPointName : entryPointNames) {
+      auto *entryPointFn = llvmModule->getFunction(entryPointName);
+      entryPointFn->setCallingConv(llvm::CallingConv::X86_StdCall);
+      entryPointFn->setDLLStorageClass(
+          llvm::GlobalValue::DLLStorageClassTypes::DLLExportStorageClass);
+      entryPointFn->setLinkage(
+          llvm::GlobalValue::LinkageTypes::ExternalLinkage);
+      entryPointFn->setVisibility(
+          llvm::GlobalValue::VisibilityTypes::DefaultVisibility);
+      entryPointFn->addFnAttr(llvm::Attribute::UWTable);
+    }
+
+    return success();
+  }
+
+  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, "dll");
+    } else {
+      artifacts.libraryFile = Artifact::createTemporary(libraryName, "dll");
+    }
+
+    // link.exe doesn't like the files being opened. We don't use them as
+    // streams so close them all now before running the linker.
+    artifacts.libraryFile.close();
+
+    // We need a full path for the PDB and I hate strings in LLVM grumble.
+    SmallString<32> pdbPath(artifacts.libraryFile.path);
+    llvm::sys::path::replace_extension(pdbPath, "pdb");
+
+    SmallVector<std::string, 8> flags = {
+        getToolPath(),
+
+        // Useful when debugging linking/loading issues:
+        // "/verbose",
+
+        // https://docs.microsoft.com/en-us/cpp/build/reference/dll-build-a-dll?view=vs-2019
+        // Builds a DLL and exports functions with the dllexport storage class.
+        "/dll",
+
+        // Forces a fixed timestamp to ensure files are reproducable across
+        // builds. Undocumented but accepted by both link and lld-link.
+        // https://blog.conan.io/2019/09/02/Deterministic-builds-with-C-C++.html
+        "/Brepro",
+
+        // https://docs.microsoft.com/en-us/cpp/build/reference/nodefaultlib-ignore-libraries?view=vs-2019
+        // Ignore any libraries that are specified by the platform as we
+        // directly provide the ones we want.
+        "/nodefaultlib",
+
+        // https://docs.microsoft.com/en-us/cpp/build/reference/incremental-link-incrementally?view=vs-2019
+        // Disable incremental linking as we are only ever linking in one-shot
+        // mode to temp files. This avoids additional file padding and ordering
+        // restrictions that enable incremental linking. Our other options will
+        // prevent incremental linking in most cases, but it doesn't hurt to be
+        // explicit.
+        "/incremental:no",
+
+        // https://docs.microsoft.com/en-us/cpp/build/reference/guard-enable-guard-checks?view=vs-2019
+        // No control flow guard lookup (indirect branch verification).
+        "/guard:no",
+
+        // https://docs.microsoft.com/en-us/cpp/build/reference/safeseh-image-has-safe-exception-handlers?view=vs-2019
+        // We don't want exception unwind tables in our output.
+        "/safeseh:no",
+
+        // https://docs.microsoft.com/en-us/cpp/build/reference/entry-entry-point-symbol?view=vs-2019
+        // Use our entry point instead of the standard CRT one; ensures that we
+        // pull in no global state from the CRT.
+        "/entry:iree_dll_main",
+
+        // https://docs.microsoft.com/en-us/cpp/build/reference/debug-generate-debug-info?view=vs-2019
+        // Copies all PDB information into the final PDB so that we can use the
+        // same PDB across multiple machines.
+        "/debug:full",
+
+        // https://docs.microsoft.com/en-us/cpp/build/reference/pdb-use-program-database
+        // Generates the PDB file containing the debug information.
+        ("/pdb:" + pdbPath).str(),
+
+        // https://docs.microsoft.com/en-us/cpp/build/reference/pdbaltpath-use-alternate-pdb-path?view=vs-2019
+        // Forces the PDB we generate to be referenced in the DLL as just a
+        // relative path to the DLL itself. This allows us to move the PDBs
+        // along with the build DLLs across machines.
+        "/pdbaltpath:%_PDB%",
+
+        // https://docs.microsoft.com/en-us/cpp/build/reference/out-output-file-name?view=vs-2019
+        // Target for linker output. The base name of this path will be used for
+        // additional output files (like the map and pdb).
+        "/out:" + artifacts.libraryFile.path,
+    };
+
+    if (targetOptions.optLevel.getSpeedupLevel() >= 2 ||
+        targetOptions.optLevel.getSizeLevel() >= 2) {
+      // https://docs.microsoft.com/en-us/cpp/build/reference/opt-optimizations?view=vs-2019
+      // Enable all the fancy optimizations.
+      flags.push_back("/opt:ref,icf,lbr");
+    }
+
+    // SDK and MSVC paths.
+    // These rely on the environment variables provided by the
+    // vcvarsall or VsDevCmd ("Developer Command Prompt") scripts. They can also
+    // be manually be specified.
+    //
+    // We could also check to see if vswhere is installed and query that in the
+    // event of missing environment variables; that would eliminate the need for
+    // specifying things from for example IDEs that may not bring in the vcvars.
+    //
+    /* Example values:
+      UCRTVersion=10.0.18362.0
+      UniversalCRTSdkDir=C:\Program Files (x86)\Windows Kits\10\
+      VCToolsInstallDir=C:\Program Files (x86)\Microsoft Visual
+          Studio\2019\Preview\VC\Tools\MSVC\14.28.29304\
+      */
+    if (!getenv("VCToolsInstallDir") || !getenv("UniversalCRTSdkDir")) {
+      llvm::errs() << "required environment for lld-link/link not specified; "
+                      "ensure you are building from a shell where "
+                      "vcvarsall/VsDevCmd.bat/etc has been used";
+      return llvm::None;
+    }
+    const char *arch;
+    if (targetTriple.isARM() && targetTriple.isArch32Bit()) {
+      arch = "arm";
+    } else if (targetTriple.isARM()) {
+      arch = "arm64";
+    } else if (targetTriple.isX86() && targetTriple.isArch32Bit()) {
+      arch = "x86";
+    } else if (targetTriple.isX86()) {
+      arch = "x64";
+    } else {
+      llvm::errs() << "unsupported Windows target triple (no arch libs): "
+                   << targetTriple.str();
+      return llvm::None;
+    }
+    flags.push_back(
+        llvm::formatv("/libpath:\"{0}\\lib\\{1}\"", "%VCToolsInstallDir%", arch)
+            .str());
+    flags.push_back(llvm::formatv("/libpath:\"{0}\\Lib\\{1}\\ucrt\\{2}\"",
+                                  "%UniversalCRTSdkDir%", "%UCRTVersion%", arch)
+                        .str());
+    flags.push_back(llvm::formatv("/libpath:\"{0}\\Lib\\{1}\\um\\{2}\"",
+                                  "%UniversalCRTSdkDir%", "%UCRTVersion%", arch)
+                        .str());
+
+    // We need to link against different libraries based on our configuration
+    // matrix (dynamic/static and debug/release).
+    int libIndex = 0;
+    if (targetOptions.optLevel.getSpeedupLevel() == 0) {
+      libIndex += 0;  // debug
+    } else {
+      libIndex += 2;  // release
+    }
+    libIndex += targetOptions.linkStatic ? 1 : 0;
+
+    // The required libraries for linking DLLs:
+    // https://docs.microsoft.com/en-us/cpp/c-runtime-library/crt-library-features?view=msvc-160
+    //
+    // NOTE: there are only static versions of msvcrt as it's the startup code.
+    static const char *kMSVCRTLibs[4] = {
+        /*   debug/dynamic */ "msvcrtd.lib",
+        /*   debug/static  */ "msvcrtd.lib",
+        /* release/dynamic */ "msvcrt.lib",
+        /* release/static  */ "msvcrt.lib",
+    };
+    static const char *kVCRuntimeLibs[4] = {
+        /*   debug/dynamic */ "vcruntimed.lib",
+        /*   debug/static  */ "libvcruntimed.lib",
+        /* release/dynamic */ "vcruntime.lib",
+        /* release/static  */ "libvcruntime.lib",
+    };
+    static const char *kUCRTLibs[4] = {
+        /*   debug/dynamic */ "ucrtd.lib",
+        /*   debug/static  */ "libucrtd.lib",
+        /* release/dynamic */ "ucrt.lib",
+        /* release/static  */ "libucrt.lib",
+    };
+    flags.push_back(kMSVCRTLibs[libIndex]);
+    flags.push_back(kVCRuntimeLibs[libIndex]);
+    flags.push_back(kUCRTLibs[libIndex]);
+    flags.push_back("kernel32.lib");
+
+    // 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);
+    }
+
+    auto commandLine = llvm::join(flags, " ");
+    if (failed(runLinkCommand(commandLine))) return llvm::None;
+
+    // PDB file gets generated wtih the same path + .pdb.
+    artifacts.debugFile =
+        Artifact::createVariant(artifacts.libraryFile.path, "pdb");
+
+    // We currently discard some of the other file outputs (like the .exp
+    // listing the exported symbols) as we don't need them.
+    artifacts.otherFiles.push_back(
+        Artifact::createVariant(artifacts.libraryFile.path, "exp"));
+    artifacts.otherFiles.push_back(
+        Artifact::createVariant(artifacts.libraryFile.path, "lib"));
+
+    return artifacts;
+  }
+};
+
+std::unique_ptr<LinkerTool> createWindowsLinkerTool(
+    llvm::Triple &targetTriple, LLVMTargetOptions &targetOptions) {
+  return std::make_unique<WindowsLinkerTool>(targetTriple, targetOptions);
+}
+
+}  // namespace HAL
+}  // namespace IREE
+}  // namespace iree_compiler
+}  // namespace mlir
diff --git a/iree/compiler/Dialect/HAL/Target/LLVM/BUILD b/iree/compiler/Dialect/HAL/Target/LLVM/BUILD
index 9c608c2..9040b96 100644
--- a/iree/compiler/Dialect/HAL/Target/LLVM/BUILD
+++ b/iree/compiler/Dialect/HAL/Target/LLVM/BUILD
@@ -80,6 +80,7 @@
         "LLVMTargetOptions.h",
     ],
     deps = [
+        "@llvm-project//llvm:MC",
         "@llvm-project//llvm:Passes",
         "@llvm-project//llvm:Support",
         "@llvm-project//llvm:Target",
diff --git a/iree/compiler/Dialect/HAL/Target/LLVM/CMakeLists.txt b/iree/compiler/Dialect/HAL/Target/LLVM/CMakeLists.txt
index 1f0f77f..416f32e 100644
--- a/iree/compiler/Dialect/HAL/Target/LLVM/CMakeLists.txt
+++ b/iree/compiler/Dialect/HAL/Target/LLVM/CMakeLists.txt
@@ -66,6 +66,7 @@
   SRCS
     "LLVMTargetOptions.cpp"
   DEPS
+    LLVMMC
     LLVMPasses
     LLVMSupport
     LLVMTarget
diff --git a/iree/compiler/Dialect/HAL/Target/LLVM/LLVMBaseTarget.cpp b/iree/compiler/Dialect/HAL/Target/LLVM/LLVMBaseTarget.cpp
index 8efcf6c..4861dd3 100644
--- a/iree/compiler/Dialect/HAL/Target/LLVM/LLVMBaseTarget.cpp
+++ b/iree/compiler/Dialect/HAL/Target/LLVM/LLVMBaseTarget.cpp
@@ -80,13 +80,37 @@
   buildLLVMTransformPassPipeline(passManager);
 }
 
+static FileLineColLoc findFirstFileLoc(Location baseLoc) {
+  if (auto loc = baseLoc.dyn_cast<FusedLoc>()) {
+    for (auto &childLoc : loc.getLocations()) {
+      auto childResult = findFirstFileLoc(childLoc);
+      if (childResult) return childResult;
+    }
+  } else if (auto loc = baseLoc.dyn_cast<FileLineColLoc>()) {
+    return loc;
+  }
+  return FileLineColLoc{};
+}
+
+static std::string guessModuleName(mlir::ModuleOp moduleOp) {
+  std::string moduleName =
+      moduleOp.getName().hasValue() ? moduleOp.getName().getValue().str() : "";
+  if (!moduleName.empty()) return moduleName;
+  FileLineColLoc loc = findFirstFileLoc(moduleOp.getLoc());
+  return llvm::sys::path::stem(loc.getFilename()).str();
+}
+
 LogicalResult LLVMBaseTargetBackend::linkExecutables(mlir::ModuleOp moduleOp) {
   OpBuilder builder = OpBuilder::atBlockBegin(moduleOp.getBody());
   auto executableOps =
       llvm::to_vector<8>(moduleOp.getOps<IREE::HAL::ExecutableOp>());
 
+  // Guess a module name, if needed, to make the output files readable.
+  auto moduleName = guessModuleName(moduleOp);
+
   // Create our new "linked" hal.executable.
-  std::string linkedExecutableName = llvm::formatv("linked_{0}", name());
+  std::string linkedExecutableName =
+      llvm::formatv("{0}_linked_{1}", moduleName, name());
   auto linkedExecutableOp = builder.create<IREE::HAL::ExecutableOp>(
       moduleOp.getLoc(), linkedExecutableName);
   linkedExecutableOp.setPrivate();
diff --git a/iree/compiler/Dialect/HAL/Target/LLVM/LLVMIRPasses.cpp b/iree/compiler/Dialect/HAL/Target/LLVM/LLVMIRPasses.cpp
index de66446..65cd442 100644
--- a/iree/compiler/Dialect/HAL/Target/LLVM/LLVMIRPasses.cpp
+++ b/iree/compiler/Dialect/HAL/Target/LLVM/LLVMIRPasses.cpp
@@ -31,16 +31,33 @@
 namespace IREE {
 namespace HAL {
 
+static llvm::CodeGenOpt::Level passBuilderOptLevelToCodeGenOptLevel(
+    const llvm::PassBuilder::OptimizationLevel &level) {
+  switch (level.getSpeedupLevel()) {
+    case 0:
+      return llvm::CodeGenOpt::None;
+    case 1:
+      return llvm::CodeGenOpt::Less;
+    case 2:
+    default:
+      return llvm::CodeGenOpt::Default;
+    case 3:
+      return llvm::CodeGenOpt::Aggressive;
+  }
+}
+
 std::unique_ptr<llvm::TargetMachine> createTargetMachine(
     const LLVMTargetOptions &targetOptions) {
   std::string errorMessage;
   auto target = llvm::TargetRegistry::lookupTarget(targetOptions.targetTriple,
                                                    errorMessage);
   if (!target) return nullptr;
-  // TODO(ataei): Once we have an AOT backend pass cpu and cpu-features
   std::unique_ptr<llvm::TargetMachine> machine(target->createTargetMachine(
-      targetOptions.targetTriple, "generic" /* cpu e.g k8*/,
-      "" /* cpu features e.g avx512fma*/, targetOptions.options, {}));
+      targetOptions.targetTriple, targetOptions.targetCPU /* cpu e.g k8*/,
+      targetOptions.targetCPUFeatures /* cpu features e.g avx512fma*/,
+      targetOptions.options, {}, {},
+      passBuilderOptLevelToCodeGenOptLevel(targetOptions.optLevel),
+      /*JIT=*/false));
   return machine;
 }
 
@@ -68,10 +85,12 @@
   passBuilder.registerLoopAnalyses(loopAnalysisManager);
   passBuilder.crossRegisterProxies(loopAnalysisManager, functionAnalysisManager,
                                    cGSCCAnalysisManager, moduleAnalysisManager);
-  llvm::ModulePassManager modulePassManager;
-  modulePassManager =
-      passBuilder.buildPerModuleDefaultPipeline(options.optLevel);
-  modulePassManager.run(*module, moduleAnalysisManager);
+  if (options.optLevel != llvm::PassBuilder::OptimizationLevel::O0) {
+    llvm::ModulePassManager modulePassManager;
+    modulePassManager =
+        passBuilder.buildPerModuleDefaultPipeline(options.optLevel);
+    modulePassManager.run(*module, moduleAnalysisManager);
+  }
 
   if (llvm::verifyModule(*module)) return failure();
 
diff --git a/iree/compiler/Dialect/HAL/Target/LLVM/LLVMTargetOptions.cpp b/iree/compiler/Dialect/HAL/Target/LLVM/LLVMTargetOptions.cpp
index e71beac..649dbf0 100644
--- a/iree/compiler/Dialect/HAL/Target/LLVM/LLVMTargetOptions.cpp
+++ b/iree/compiler/Dialect/HAL/Target/LLVM/LLVMTargetOptions.cpp
@@ -15,6 +15,7 @@
 #include "iree/compiler/Dialect/HAL/Target/LLVM/LLVMTargetOptions.h"
 
 #include "llvm/ADT/APFloat.h"
+#include "llvm/MC/SubtargetFeature.h"
 #include "llvm/Support/CommandLine.h"
 #include "llvm/Support/Host.h"
 #include "llvm/Target/TargetOptions.h"
@@ -26,17 +27,34 @@
 
 LLVMTargetOptions getDefaultLLVMTargetOptions() {
   LLVMTargetOptions targetOptions;
+
   // Host target triple.
   targetOptions.targetTriple = llvm::sys::getDefaultTargetTriple();
+  targetOptions.targetCPU = llvm::sys::getHostCPUName().str();
+  {
+    llvm::SubtargetFeatures features;
+    llvm::StringMap<bool> hostFeatures;
+    if (llvm::sys::getHostCPUFeatures(hostFeatures)) {
+      for (auto &feature : hostFeatures) {
+        features.AddFeature(feature.first(), feature.second);
+      }
+    }
+    targetOptions.targetCPUFeatures = features.getString();
+  }
+
   // LLVM loop optimization options.
   targetOptions.pipelineTuningOptions.LoopInterleaving = true;
   targetOptions.pipelineTuningOptions.LoopVectorization = true;
   targetOptions.pipelineTuningOptions.LoopUnrolling = true;
+
   // LLVM SLP Auto vectorizer.
   targetOptions.pipelineTuningOptions.SLPVectorization = true;
+
   // LLVM -O3.
+  // TODO(benvanik): add an option for this.
   targetOptions.optLevel = llvm::PassBuilder::OptimizationLevel::O3;
   targetOptions.options.FloatABIType = llvm::FloatABI::Hard;
+
   return targetOptions;
 }
 
@@ -46,16 +64,57 @@
   static llvm::cl::opt<std::string> clTargetTriple(
       "iree-llvm-target-triple", llvm::cl::desc("LLVM target machine triple"),
       llvm::cl::init(llvmTargetOptions.targetTriple));
-  static llvm::cl::opt<bool> clSoftFloat(
-      "iree-llvm-enable-msoft-float-abi",
+  static llvm::cl::opt<std::string> clTargetCPU(
+      "iree-llvm-target-cpu",
+      llvm::cl::desc(
+          "LLVM target machine CPU; use 'host' for your host native CPU"),
+      llvm::cl::init("generic"));
+  static llvm::cl::opt<std::string> clTargetCPUFeatures(
+      "iree-llvm-target-cpu-features",
+      llvm::cl::desc("LLVM target machine CPU features; use 'host' for your "
+                     "host native CPU"),
+      llvm::cl::init(""));
+  llvmTargetOptions.targetTriple = clTargetTriple;
+  if (clTargetCPU != "host") {
+    llvmTargetOptions.targetCPU = clTargetCPU;
+  }
+  if (clTargetCPUFeatures != "host") {
+    llvmTargetOptions.targetCPUFeatures = clTargetCPUFeatures;
+  }
+
+  static llvm::cl::opt<llvm::FloatABI::ABIType> clTargetFloatABI(
+      "iree-llvm-target-float-abi",
       llvm::cl::desc("LLVM target codegen enables soft float abi e.g "
                      "-mfloat-abi=softfp"),
-      llvm::cl::init(false));
+      llvm::cl::init(llvmTargetOptions.options.FloatABIType),
+      llvm::cl::values(
+          clEnumValN(llvm::FloatABI::Default, "default", "Default (softfp)"),
+          clEnumValN(llvm::FloatABI::Soft, "soft",
+                     "Software floating-point emulation"),
+          clEnumValN(llvm::FloatABI::Hard, "hard",
+                     "Hardware floating-point instructions")));
+  llvmTargetOptions.options.FloatABIType = clTargetFloatABI;
 
-  llvmTargetOptions.targetTriple = clTargetTriple;
-  if (clSoftFloat) {
-    llvmTargetOptions.options.FloatABIType = llvm::FloatABI::Soft;
-  }
+  static llvm::cl::opt<bool> clDebugSymbols(
+      "iree-llvm-debug-symbols",
+      llvm::cl::desc("Generate and embed debug information (DWARF, PDB, etc)"),
+      llvm::cl::init(llvmTargetOptions.debugSymbols));
+  llvmTargetOptions.debugSymbols = clDebugSymbols;
+
+  static llvm::cl::opt<bool> clLinkStatic(
+      "iree-llvm-link-static",
+      llvm::cl::desc(
+          "Links system libraries into binaries statically to isolate them "
+          "from platform dependencies needed at runtime"),
+      llvm::cl::init(llvmTargetOptions.linkStatic));
+  llvmTargetOptions.linkStatic = clLinkStatic;
+
+  static llvm::cl::opt<bool> clKeepLinkerArtifacts(
+      "iree-llvm-keep-linker-artifacts",
+      llvm::cl::desc("Keep LLVM linker target artifacts (.so/.dll/etc)"),
+      llvm::cl::init(llvmTargetOptions.keepLinkerArtifacts));
+  llvmTargetOptions.keepLinkerArtifacts = clKeepLinkerArtifacts;
+
   return llvmTargetOptions;
 }
 
diff --git a/iree/compiler/Dialect/HAL/Target/LLVM/LLVMTargetOptions.h b/iree/compiler/Dialect/HAL/Target/LLVM/LLVMTargetOptions.h
index 4893566..ba454b6 100644
--- a/iree/compiler/Dialect/HAL/Target/LLVM/LLVMTargetOptions.h
+++ b/iree/compiler/Dialect/HAL/Target/LLVM/LLVMTargetOptions.h
@@ -24,10 +24,28 @@
 namespace HAL {
 
 struct LLVMTargetOptions {
+  // Target machine configuration.
+  std::string targetTriple;
+  std::string targetCPU;
+  std::string targetCPUFeatures;
+
   llvm::PipelineTuningOptions pipelineTuningOptions;
   llvm::PassBuilder::OptimizationLevel optLevel;
   llvm::TargetOptions options;
-  std::string targetTriple;
+
+  // Include debug information in output files (PDB, DWARF, etc).
+  // Though this can be set independently from the optLevel (so -O3 with debug
+  // information is valid) it may significantly change the output program
+  // and benchmarking
+  bool debugSymbols = true;
+
+  // Link any required runtime libraries into the produced binaries statically.
+  // This increases resulting binary size but enables the binaries to be used on
+  // any machine without requiring matching system libraries to be installed.
+  bool linkStatic = false;
+
+  // True to keep linker artifacts for debugging.
+  bool keepLinkerArtifacts = false;
 };
 
 // Returns LLVMTargetOptions struct intialized with the
diff --git a/iree/compiler/Dialect/HAL/Target/LLVM/test/binaryop_test.mlir b/iree/compiler/Dialect/HAL/Target/LLVM/test/binary_op.mlir
similarity index 90%
rename from iree/compiler/Dialect/HAL/Target/LLVM/test/binaryop_test.mlir
rename to iree/compiler/Dialect/HAL/Target/LLVM/test/binary_op.mlir
index 0a66d94..41f1939 100644
--- a/iree/compiler/Dialect/HAL/Target/LLVM/test/binaryop_test.mlir
+++ b/iree/compiler/Dialect/HAL/Target/LLVM/test/binary_op.mlir
@@ -11,7 +11,7 @@
   }
 }
 
-// CHECK-LABEL: hal.executable @linked_llvm_ir
+// CHECK-LABEL: hal.executable @binary_op_linked_llvm_ir
 // CHECK-DAG:   hal.executable.binary attributes {
 // CHECK-SAME:     data = dense
 // CHECK-SAME:     format = 1280071245 : i32} {
diff --git a/iree/compiler/Dialect/HAL/Target/LLVM/test/matmul_op.mlir b/iree/compiler/Dialect/HAL/Target/LLVM/test/matmul_op.mlir
index 6744a70..1a48518 100644
--- a/iree/compiler/Dialect/HAL/Target/LLVM/test/matmul_op.mlir
+++ b/iree/compiler/Dialect/HAL/Target/LLVM/test/matmul_op.mlir
@@ -11,7 +11,7 @@
   }
 }
 
-// CHECK-LABEL: hal.executable @linked_llvm_ir
+// CHECK-LABEL: hal.executable @matmul_op_linked_llvm_ir
 // CHECK-DAG:   hal.executable.binary attributes {
 // CHECK-SAME:     data = dense
 // CHECK-SAME:     format = 1280071245 : i32} {