clang_configuration.bzl 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. # Part of the Carbon Language project, under the Apache License v2.0 with LLVM
  2. # Exceptions. See /LICENSE for license information.
  3. # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
  4. """Starlark repository rules to configure Clang (and LLVM) toolchain for Bazel.
  5. These rules should be run from the `WORKSPACE` file to substitute appropriate
  6. configured values into a `clang_detected_variables.bzl` file that can be used
  7. by the actual toolchain configuration.
  8. """
  9. load(":clang_configs.bzl", "clang_configs")
  10. def _run(repository_ctx, cmd):
  11. """Runs the provided `cmd`, checks for failure, and returns the result."""
  12. exec_result = repository_ctx.execute(cmd)
  13. if exec_result.return_code != 0:
  14. fail("Unable to run command successfully: %s" % str(cmd))
  15. return exec_result
  16. def _clang_version(version_output):
  17. """Returns version information, or a (None, "unknown") tuple if not found.
  18. Returns both the major version number (14) and the full version number for
  19. caching.
  20. """
  21. clang_version = None
  22. clang_version_for_cache = "unknown"
  23. version_prefix = "clang version "
  24. version_start = version_output.find(version_prefix)
  25. if version_start == -1:
  26. # No version
  27. return (clang_version, clang_version_for_cache)
  28. version_start += len(version_prefix)
  29. # Find the newline.
  30. version_newline = version_output.find("\n", version_start)
  31. if version_newline == -1:
  32. return (clang_version, clang_version_for_cache)
  33. clang_version_for_cache = version_output[version_start:version_newline]
  34. # Find a dot to indicate something like 'clang version 14.0.6', and grab the
  35. # major version.
  36. version_dot = version_output.find(".", version_start)
  37. if version_dot != -1 and version_dot < version_newline:
  38. clang_version = int(version_output[version_start:version_dot])
  39. return (clang_version, clang_version_for_cache)
  40. def _detect_system_clang(repository_ctx):
  41. """Detects whether the system-provided clang can be used.
  42. Returns a tuple of (is_clang, environment).
  43. """
  44. # If the user provides an explicit `CC` environment variable, use that as
  45. # the compiler. This should point at the `clang` executable to use.
  46. cc = repository_ctx.os.environ.get("CC")
  47. cc_path = None
  48. if cc:
  49. cc_path = repository_ctx.path(cc)
  50. if not cc_path.exists:
  51. cc_path = repository_ctx.which(cc)
  52. if not cc_path:
  53. cc_path = repository_ctx.which("clang")
  54. if not cc_path:
  55. fail("Cannot find clang or CC (%s); either correct your path or set the CC environment variable" % cc)
  56. version_output = _run(repository_ctx, [cc_path, "--version"]).stdout
  57. if "clang" not in version_output:
  58. fail("Searching for clang or CC (%s), and found (%s), which is not a Clang compiler" % (cc, cc_path))
  59. clang_version, clang_version_for_cache = _clang_version(version_output)
  60. return (cc_path.realpath, clang_version, clang_version_for_cache)
  61. def _compute_clang_resource_dir(repository_ctx, clang):
  62. """Runs the `clang` binary to get its resource dir."""
  63. output = _run(
  64. repository_ctx,
  65. [clang, "-no-canonical-prefixes", "--print-resource-dir"],
  66. ).stdout
  67. # The only line printed is this path.
  68. return output.splitlines()[0]
  69. def _compute_mac_os_sysroot(repository_ctx):
  70. """Runs `xcrun` to extract the correct sysroot."""
  71. xcrun = repository_ctx.which("xcrun")
  72. if not xcrun:
  73. fail("`xcrun` not found: is Xcode installed?")
  74. output = _run(repository_ctx, [xcrun, "--show-sdk-path"]).stdout
  75. return output.splitlines()[0]
  76. def _compute_bsd_sysroot(repository_ctx):
  77. """Look around for sysroot. Return root (/) if nothing found."""
  78. # Try it-just-works for CMake users.
  79. default = "/"
  80. sysroot = repository_ctx.os.environ.get("CMAKE_SYSROOT", default)
  81. sysroot_path = repository_ctx.path(sysroot)
  82. if sysroot_path.exists:
  83. return sysroot_path.realpath
  84. return default
  85. def _compute_clang_cpp_include_search_paths(repository_ctx, clang, sysroot):
  86. """Runs the `clang` binary and extracts the include search paths.
  87. Returns the resulting paths as a list of strings.
  88. """
  89. # Create an empty temp file for Clang to use
  90. if repository_ctx.os.name.lower().startswith("windows"):
  91. repository_ctx.file("_temp", "")
  92. # Read in an empty input file. If we are building from
  93. # Windows, then we create an empty temp file. Clang
  94. # on Windows does not like it when you pass a non-existent file.
  95. if repository_ctx.os.name.lower().startswith("windows"):
  96. repository_ctx.file("_temp", "")
  97. input_file = repository_ctx.path("_temp")
  98. else:
  99. input_file = "/dev/null"
  100. # The only way to get this out of Clang currently is to parse the verbose
  101. # output of the compiler when it is compiling C++ code.
  102. cmd = [
  103. clang,
  104. # Avoid canonicalizing away symlinks.
  105. "-no-canonical-prefixes",
  106. # Extract verbose output.
  107. "-v",
  108. # Just parse the input, don't generate outputs.
  109. "-fsyntax-only",
  110. # Force the language to be C++.
  111. "-x",
  112. "c++",
  113. # Read in an empty input file.
  114. input_file,
  115. # Always use libc++.
  116. "-stdlib=libc++",
  117. ]
  118. # We need to use a sysroot to correctly represent a run on macOS.
  119. if repository_ctx.os.name.lower().startswith("mac os"):
  120. if not sysroot:
  121. fail("Must provide a sysroot on macOS!")
  122. cmd.append("--sysroot=" + sysroot)
  123. # Note that verbose output is on stderr, not stdout!
  124. output = _run(repository_ctx, cmd).stderr.splitlines()
  125. # Return the list of directories printed for system headers. These are the
  126. # only ones that Bazel needs us to manually provide. We find these by
  127. # searching for a begin and end marker. We also have to strip off a leading
  128. # space from each path.
  129. include_begin = output.index("#include <...> search starts here:") + 1
  130. include_end = output.index("End of search list.", include_begin)
  131. # Suffix present on framework paths.
  132. framework_suffix = " (framework directory)"
  133. return [
  134. repository_ctx.path(s.lstrip(" ").removesuffix(framework_suffix))
  135. for s in output[include_begin:include_end]
  136. ]
  137. def _configure_clang_toolchain_impl(repository_ctx):
  138. # First just symlink in the untemplated parts of the toolchain repo.
  139. repository_ctx.symlink(repository_ctx.attr._clang_toolchain_build, "BUILD")
  140. repository_ctx.symlink(
  141. repository_ctx.attr._clang_cc_toolchain_config,
  142. "cc_toolchain_config.bzl",
  143. )
  144. repository_ctx.symlink(
  145. repository_ctx.attr._clang_configs,
  146. "clang_configs.bzl",
  147. )
  148. # Find a Clang C++ compiler, and where it lives. We need to walk symlinks
  149. # here as the other LLVM tools may not be symlinked into the PATH even if
  150. # `clang` is. We also insist on finding the basename of `clang++` as that is
  151. # important for C vs. C++ compiles.
  152. (clang, clang_version, clang_version_for_cache) = _detect_system_clang(
  153. repository_ctx,
  154. )
  155. clang_cpp = clang.dirname.get_child("clang++")
  156. # Compute the various directories used by Clang.
  157. resource_dir = _compute_clang_resource_dir(repository_ctx, clang_cpp)
  158. sysroot_dir = None
  159. if repository_ctx.os.name.lower().startswith("mac os"):
  160. sysroot_dir = _compute_mac_os_sysroot(repository_ctx)
  161. if repository_ctx.os.name == "freebsd":
  162. sysroot_dir = _compute_bsd_sysroot(repository_ctx)
  163. include_dirs = _compute_clang_cpp_include_search_paths(
  164. repository_ctx,
  165. clang_cpp,
  166. sysroot_dir,
  167. )
  168. # We expect that the LLVM binutils live adjacent to llvm-ar.
  169. # First look for llvm-ar adjacent to clang, so that if found,
  170. # it is most likely to match the same version as clang.
  171. # Otherwise, try PATH.
  172. ar_path = clang.dirname.get_child("llvm-ar")
  173. if not ar_path.exists:
  174. ar_path = repository_ctx.which("llvm-ar")
  175. if not ar_path:
  176. fail("`llvm-ar` not found in PATH or adjacent to clang")
  177. # By default Windows uses '\' in its paths. These will be
  178. # interpreted as escape characters and fail the build, thus
  179. # we must manually replace the backslashes with '/'
  180. if repository_ctx.os.name.lower().startswith("windows"):
  181. resource_dir = resource_dir.replace("\\", "/")
  182. include_dirs = [str(s).replace("\\", "/") for s in include_dirs]
  183. repository_ctx.template(
  184. "clang_detected_variables.bzl",
  185. repository_ctx.attr._clang_detected_variables_template,
  186. substitutions = {
  187. "{CLANG_BINDIR}": str(clang.dirname),
  188. "{CLANG_INCLUDE_DIRS_LIST}": str(
  189. [str(path) for path in include_dirs],
  190. ),
  191. "{CLANG_RESOURCE_DIR}": resource_dir,
  192. "{CLANG_VERSION_FOR_CACHE}": clang_version_for_cache.replace('"', "_").replace("\\", "_"),
  193. "{CLANG_VERSION}": str(clang_version),
  194. "{LLVM_BINDIR}": str(ar_path.dirname),
  195. "{LLVM_SYMBOLIZER}": str(ar_path.dirname.get_child("llvm-symbolizer")),
  196. "{SYSROOT}": str(sysroot_dir),
  197. },
  198. executable = False,
  199. )
  200. configure_clang_toolchain = repository_rule(
  201. implementation = _configure_clang_toolchain_impl,
  202. configure = True,
  203. local = True,
  204. attrs = {
  205. "_clang_cc_toolchain_config": attr.label(
  206. default = Label(
  207. "//bazel/cc_toolchains:clang_cc_toolchain_config.bzl",
  208. ),
  209. allow_single_file = True,
  210. ),
  211. "_clang_configs": attr.label(
  212. default = Label("//bazel/cc_toolchains:clang_configs.bzl"),
  213. allow_single_file = True,
  214. ),
  215. "_clang_detected_variables_template": attr.label(
  216. default = Label(
  217. "//bazel/cc_toolchains:clang_detected_variables.tpl.bzl",
  218. ),
  219. allow_single_file = True,
  220. ),
  221. "_clang_toolchain_build": attr.label(
  222. default = Label("//bazel/cc_toolchains:clang_toolchain.BUILD"),
  223. allow_single_file = True,
  224. ),
  225. },
  226. environ = ["CC"],
  227. )
  228. def clang_register_toolchains(name):
  229. configure_clang_toolchain(name = name)
  230. for os, cpu in clang_configs:
  231. native.register_toolchains(
  232. "@{0}//:bazel_cc_toolchain_{1}_{2}".format(name, os, cpu),
  233. )