Create option to build against a CMake build of LLVM

Set `PREBUILT_LLVM_PATH` to the root of an LLVM tree built with CMake
where the build output directory is named `build`.

The path can be provided relative to the root of the Crubit tree.

PiperOrigin-RevId: 450711879
diff --git a/.bazelrc b/.bazelrc
index c1881c2..301c00f 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -5,5 +5,12 @@
 build:generic_clang --cxxopt=-Wno-range-loop-analysis --host_cxxopt=-Wno-range-loop-analysis
 build:generic_clang --copt=-Wno-deprecated --host_copt=-Wno-deprecated
 
+# enable linking against prebuilt LLVM with no RTTI
+build:generic_clang --copt=-fno-rtti --host_copt=-fno-rtti
+
+# new warning in LLVM 15, fires when building LLVM 14.0.0
+# ref: https://github.com/llvm/llvm-project/commit/48285c20
+build:generic_clang --copt=-Wno-unused-but-set-variable --host_copt=-Wno-unused-but-set-variable
+
 build --config=generic_clang
 build --sandbox_base=/dev/shm
diff --git a/README.md b/README.md
index 55b3068..671119b 100644
--- a/README.md
+++ b/README.md
@@ -13,4 +13,16 @@
 $ git clone git@github.com:google/crubit.git
 $ cd crubit
 $ bazel build --linkopt=-fuse-ld=/usr/bin/ld.lld //rs_bindings_from_cc:rs_bindings_from_cc_impl
+```
+
+### Using a prebuilt LLVM tree
+
+```
+$ git clone https://github.com/llvm/llvm-project
+$ cd llvm-project
+$ CC=clang CXX=clang++ cmake -S llvm -B build -DLLVM_ENABLE_PROJECTS='clang' -DCMAKE_BUILD_TYPE=Release
+$ cmake --build build -j
+$ # wait...
+$ cd ../crubit
+$ PREBUILT_LLVM_PATH=../llvm-project bazel build //rs_bindings_from_cc:rs_bindings_from_cc_impl
 ```
\ No newline at end of file
diff --git a/WORKSPACE b/WORKSPACE
index 060b2c5..e4e4928 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -102,21 +102,12 @@
     urls = ["https://github.com/abseil/abseil-cpp/archive/refs/tags/20211102.0.zip"],
 )
 
-# https://github.com/llvm/llvm-project/blob/main/utils/bazel/examples/http_archive/WORKSPACE
-# https://github.com/llvm/llvm-project/releases/tag/llvmorg-14.0.0
+# Create the "loader" repository, then use it to configure the desired LLVM
+# repository. For more details, see the comment in bazel/llvm.bzl.
 
-http_archive(
-    name = "llvm-raw",
-    build_file_content = "# empty",
-    sha256 = "eb7437b60a6f78e7910d08911975f100e99e9c714f069a5487119c7eadc79171",
-    strip_prefix = "llvm-project-llvmorg-14.0.0",
-    urls = ["https://github.com/llvm/llvm-project/archive/refs/tags/llvmorg-14.0.0.zip"],
-)
+load("//bazel:llvm.bzl", "llvm_loader_repository_dependencies", "llvm_loader_repository")
+llvm_loader_repository_dependencies()
+llvm_loader_repository(name = "llvm-loader")
 
-load("@llvm-raw//utils/bazel:configure.bzl", "llvm_configure", "llvm_disable_optional_support_deps")
-
-# this *must* be llvm-project, it's hardcoded in the Bazel build
-# e.g. https://github.com/llvm/llvm-project/blob/aaddfbf9/utils/bazel/llvm-project-overlay/clang/BUILD.bazel#L1473
-llvm_configure(name = "llvm-project")
-
-llvm_disable_optional_support_deps()
+load("@llvm-loader//:llvm.bzl", "llvm_repository")
+llvm_repository(name = "llvm-project")
diff --git a/bazel/BUILD b/bazel/BUILD
new file mode 100644
index 0000000..1bb8bf6
--- /dev/null
+++ b/bazel/BUILD
@@ -0,0 +1 @@
+# empty
diff --git a/bazel/llvm.bzl b/bazel/llvm.bzl
new file mode 100644
index 0000000..bcfd0e4
--- /dev/null
+++ b/bazel/llvm.bzl
@@ -0,0 +1,78 @@
+# Part of the Crubit project, under the Apache License v2.0 with LLVM
+# Exceptions. See /LICENSE for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
+
+# Create a loader/trampoline repository that we can call into to load LLVM.
+#
+# Our real goal is to choose between two different sources for LLVM binaries:
+#  - if `PREBUILT_LLVM_PATH` is in the environment, we treat it as the root of
+#    an LLVM tree that's been built with CMake and try to use headers and
+#    libraries from there
+#  - otherwise, we build LLVM from source
+#
+# We *could* implement this choice directly as an if/else between `http_archive`
+# or `new_local_repository`. However, all Bazel `load`s are unconditional, so we
+# would always end up cloning the (very large) LLVM project repository to load
+# its Bazel configuration even if we aren't going to use it.
+#
+# To avoid that, we add the extra indirection of the "loader" repository. We
+# populate the loader repository with one of two templated .bzl files depending
+# on whether we want "local" or "remote" LLVM. Then our caller activates that
+# .bzl file and gets the desired effect.
+def _llvm_loader_repository(repository_ctx):
+    # The final repository is required to have a `BUILD` file at the root.
+    repository_ctx.file("BUILD")
+
+    # Create `llvm.bzl` from one of `llvm_{remote|local}.bzl.tmpl`.
+    if "PREBUILT_LLVM_PATH" in repository_ctx.os.environ:
+        # Use prebuilt LLVM
+        path = repository_ctx.os.environ["PREBUILT_LLVM_PATH"]
+
+        # If needed, resolve relative to root of *calling* repository
+        if not path.startswith("/"):
+            root_path = repository_ctx.path(
+                repository_ctx.attr.file_at_root,
+            ).dirname
+            path = repository_ctx.path(str(root_path) + "/" + path)
+
+        repository_ctx.template(
+            "llvm.bzl",
+            Label("//bazel:llvm_local.bzl.tmpl"),
+            substitutions = {
+                "${PREBUILT_LLVM_PATH}": str(path),
+                "${CMAKE_BUILD_DIR}": "build",
+            },
+            executable = False,
+        )
+    else:
+        # Use downloaded LLVM built with Bazel
+        repository_ctx.template(
+            "llvm.bzl",
+            Label("//bazel:llvm_remote.bzl.tmpl"),
+            substitutions = {},
+            executable = False,
+        )
+
+def llvm_loader_repository_dependencies():
+    # This *declares* the dependency, but it won't actually be *downloaded*
+    # unless it's used.
+    http_archive(
+        name = "llvm-raw",
+        build_file_content = "# empty",
+        sha256 = "eb7437b60a6f78e7910d08911975f100e99e9c714f069a5487119c7eadc79171",
+        strip_prefix = "llvm-project-llvmorg-14.0.0",
+        urls = ["https://github.com/llvm/llvm-project/archive/refs/tags/llvmorg-14.0.0.zip"],
+    )
+
+llvm_loader_repository = repository_rule(
+    implementation = _llvm_loader_repository,
+    attrs = {
+        # We need a file from the root in order to get the workspace path
+        "file_at_root": attr.label(default = "//:BUILD"),
+    },
+    environ = [
+        "PREBUILT_LLVM_PATH",
+    ],
+)
diff --git a/bazel/llvm_local.bzl.tmpl b/bazel/llvm_local.bzl.tmpl
new file mode 100644
index 0000000..b78d7ad
--- /dev/null
+++ b/bazel/llvm_local.bzl.tmpl
@@ -0,0 +1,48 @@
+# Part of the Crubit project, under the Apache License v2.0 with LLVM
+# Exceptions. See /LICENSE for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+# Provide LLVM from a local tree built with CMake.
+
+prebuilt_llvm_tree_build_file_contents = """
+package(default_visibility = ["//visibility:public"])
+
+# An extremely coarse-grained target that provides *all* headers and libraries.
+cc_library(
+    name = "all",
+    srcs = glob([
+        "${CMAKE_BUILD_DIR}/lib/*.a",
+    ], exclude = [
+        "**/*.i386.a",
+    ]),
+    hdrs = glob([
+        "${CMAKE_BUILD_DIR}/include/**",
+        "${CMAKE_BUILD_DIR}/tools/clang/include/**",
+        "clang/include/**",
+        "llvm/include/**",
+    ]),
+    includes = [
+        "${CMAKE_BUILD_DIR}/include/",
+        "${CMAKE_BUILD_DIR}/tools/clang/include/",
+        "clang/include/",
+        "llvm/include/",
+    ],
+    linkopts = ["-lncurses", "-lz"],
+)
+"""
+
+def llvm_repository(name):
+    # First, create an intermediate repo that overlays the BUILD file onto the
+    # prebuilt LLVM tree, yielding the single ":all" target.
+    native.new_local_repository(
+        name = "prebuilt-llvm",
+        path = "${PREBUILT_LLVM_PATH}",  # template value
+        build_file_content = prebuilt_llvm_tree_build_file_contents,
+    )
+
+    # Next, create the final repo that emulates the layout of Bazel targets in
+    # llvm-project.
+    native.local_repository(
+        name = name,
+        path = "bazel/llvm_project",
+    )
diff --git a/bazel/llvm_project/README.md b/bazel/llvm_project/README.md
new file mode 100644
index 0000000..a189d4b
--- /dev/null
+++ b/bazel/llvm_project/README.md
@@ -0,0 +1,3 @@
+# `llvm-project` shim
+
+This directory exists to provide a set of Bazel targets that match [LLVM's Bazel build](https://github.com/llvm/llvm-project/tree/main/utils/bazel) but all forward to a single "all LLVM" target.
diff --git a/bazel/llvm_project/WORKSPACE b/bazel/llvm_project/WORKSPACE
new file mode 100644
index 0000000..90aec77
--- /dev/null
+++ b/bazel/llvm_project/WORKSPACE
@@ -0,0 +1,5 @@
+# Part of the Crubit project, under the Apache License v2.0 with LLVM
+# Exceptions. See /LICENSE for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+# empty, dependencies are handled by the repo that loads us
diff --git a/bazel/llvm_project/clang/BUILD b/bazel/llvm_project/clang/BUILD
new file mode 100644
index 0000000..159d84b
--- /dev/null
+++ b/bazel/llvm_project/clang/BUILD
@@ -0,0 +1,14 @@
+package(default_visibility = ["//visibility:public"])
+
+[cc_library(
+    name = n,
+    deps = ["@prebuilt-llvm//:all"],
+) for n in [
+    "ast",
+    "basic",
+    "format",
+    "frontend",
+    "lex",
+    "sema",
+    "tooling",
+]]
diff --git a/bazel/llvm_project/llvm/BUILD b/bazel/llvm_project/llvm/BUILD
new file mode 100644
index 0000000..04760b1
--- /dev/null
+++ b/bazel/llvm_project/llvm/BUILD
@@ -0,0 +1,6 @@
+package(default_visibility = ["//visibility:public"])
+
+cc_library(
+    name = "Support",
+    deps = ["@prebuilt-llvm//:all"],
+)
diff --git a/bazel/llvm_remote.bzl.tmpl b/bazel/llvm_remote.bzl.tmpl
new file mode 100644
index 0000000..061a0be
--- /dev/null
+++ b/bazel/llvm_remote.bzl.tmpl
@@ -0,0 +1,21 @@
+# Part of the Crubit project, under the Apache License v2.0 with LLVM
+# Exceptions. See /LICENSE for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+# Provide LLVM with upstream's Bazel build support.
+#
+# refs:
+#   https://github.com/llvm/llvm-project/blob/main/utils/bazel/examples/http_archive/WORKSPACE
+#   https://github.com/llvm/llvm-project/releases/tag/llvmorg-14.0.0
+
+load("@llvm-raw//utils/bazel:configure.bzl", "llvm_configure", "llvm_disable_optional_support_deps")
+
+# Pass through to LLVM's Bazel configuration to create the repository.
+def llvm_repository(name):
+    if name != "llvm-project":
+        # this *must* be llvm-project, it's hardcoded in the Bazel build
+        # e.g. https://github.com/llvm/llvm-project/blob/aaddfbf9/utils/bazel/llvm-project-overlay/clang/BUILD.bazel#L1473
+        fail("""name must be llvm-project""")
+
+    llvm_configure(name = "llvm-project")
+    llvm_disable_optional_support_deps()