clang_configuration.bzl 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  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. def _run(repository_ctx, cmd):
  10. """Runs the provided `cmd`, checks for failure, and returns the result."""
  11. exec_result = repository_ctx.execute(cmd)
  12. if exec_result.return_code != 0:
  13. fail("Unable to run command successfully: %s" % str(cmd))
  14. return exec_result
  15. def _detect_system_clang(repository_ctx):
  16. """Detects whether the system-provided clang can be used.
  17. Returns a tuple of (is_clang, environment).
  18. """
  19. # If the user provides an explicit `CC` environment variable, use that as
  20. # the compiler. This should point at the `clang` executable to use.
  21. cc = repository_ctx.os.environ.get("CC")
  22. cc_path = None
  23. if cc:
  24. cc_path = repository_ctx.path(cc)
  25. if not cc_path.exists:
  26. cc_path = repository_ctx.which(cc)
  27. if not cc_path:
  28. cc_path = repository_ctx.which("clang")
  29. if not cc_path:
  30. fail("Cannot find clang or CC (%s); either correct your path or set the CC environment variable" % cc)
  31. version_output = _run(repository_ctx, [cc_path, "--version"]).stdout
  32. if "clang" not in version_output:
  33. fail("Searching for clang or CC (%s), and found (%s), which is not a Clang compiler" % (cc, cc_path))
  34. return cc_path
  35. def _compute_clang_resource_dir(repository_ctx, clang):
  36. """Runs the `clang` binary to get its resource dir."""
  37. output = _run(
  38. repository_ctx,
  39. [clang, "-no-canonical-prefixes", "--print-resource-dir"],
  40. ).stdout
  41. # The only line printed is this path.
  42. return output.splitlines()[0]
  43. def _compute_mac_os_sysroot(repository_ctx):
  44. """Runs `xcrun` to extract the correct sysroot."""
  45. xcrun = repository_ctx.which("xcrun")
  46. if not xcrun:
  47. fail("`xcrun` not found: is Xcode installed?")
  48. output = _run(repository_ctx, [xcrun, "--show-sdk-path"]).stdout
  49. return output.splitlines()[0]
  50. def _compute_clang_cpp_include_search_paths(repository_ctx, clang, sysroot):
  51. """Runs the `clang` binary and extracts the include search paths.
  52. Returns the resulting paths as a list of strings.
  53. """
  54. # The only way to get this out of Clang currently is to parse the verbose
  55. # output of the compiler when it is compiling C++ code.
  56. cmd = [
  57. clang,
  58. # Avoid canonicalizing away symlinks.
  59. "-no-canonical-prefixes",
  60. # Extract verbose output.
  61. "-v",
  62. # Just parse the input, don't generate outputs.
  63. "-fsyntax-only",
  64. # Force the language to be C++.
  65. "-x",
  66. "c++",
  67. # Read in an empty input file.
  68. "/dev/null",
  69. # Always use libc++.
  70. "-stdlib=libc++",
  71. ]
  72. # We need to use a sysroot to correctly represent a run on macOS.
  73. if repository_ctx.os.name.lower().startswith("mac os"):
  74. if not sysroot:
  75. fail("Must provide a sysroot on macOS!")
  76. cmd.append("--sysroot=" + sysroot)
  77. # Note that verbose output is on stderr, not stdout!
  78. output = _run(repository_ctx, cmd).stderr.splitlines()
  79. # Return the list of directories printed for system headers. These are the
  80. # only ones that Bazel needs us to manually provide. We find these by
  81. # searching for a begin and end marker. We also have to strip off a leading
  82. # space from each path.
  83. include_begin = output.index("#include <...> search starts here:") + 1
  84. include_end = output.index("End of search list.", include_begin)
  85. return [
  86. repository_ctx.path(s.lstrip(" "))
  87. for s in output[include_begin:include_end]
  88. ]
  89. def _configure_clang_toolchain_impl(repository_ctx):
  90. # First just symlink in the untemplated parts of the toolchain repo.
  91. repository_ctx.symlink(repository_ctx.attr._clang_toolchain_build, "BUILD")
  92. repository_ctx.symlink(
  93. repository_ctx.attr._clang_cc_toolchain_config,
  94. "cc_toolchain_config.bzl",
  95. )
  96. # Find a Clang C++ compiler, and where it lives. We need to walk symlinks
  97. # here as the other LLVM tools may not be symlinked into the PATH even if
  98. # `clang` is. We also insist on finding the basename of `clang++` as that is
  99. # important for C vs. C++ compiles.
  100. clang = _detect_system_clang(repository_ctx)
  101. clang = clang.realpath.dirname.get_child("clang++")
  102. # Compute the various directories used by Clang.
  103. resource_dir = _compute_clang_resource_dir(repository_ctx, clang)
  104. sysroot_dir = None
  105. if repository_ctx.os.name.lower().startswith("mac os"):
  106. sysroot_dir = _compute_mac_os_sysroot(repository_ctx)
  107. include_dirs = _compute_clang_cpp_include_search_paths(
  108. repository_ctx,
  109. clang,
  110. sysroot_dir,
  111. )
  112. # We expect that the LLVM binutils live adjacent to llvm-ar.
  113. # First look for llvm-ar adjacent to clang, so that if found,
  114. # it is most likely to match the same version as clang.
  115. # Otherwise, try PATH.
  116. arpath = clang.dirname.get_child("llvm-ar")
  117. if not arpath.exists:
  118. arpath = repository_ctx.which("llvm-ar")
  119. if not arpath:
  120. fail("`llvm-ar` not found in PATH or adjacent to clang")
  121. repository_ctx.template(
  122. "clang_detected_variables.bzl",
  123. repository_ctx.attr._clang_detected_variables_template,
  124. substitutions = {
  125. "{LLVM_BINDIR}": str(arpath.dirname),
  126. "{CLANG_BINDIR}": str(clang.dirname),
  127. "{CLANG_RESOURCE_DIR}": resource_dir,
  128. "{CLANG_INCLUDE_DIRS_LIST}": str(
  129. [str(path) for path in include_dirs],
  130. ),
  131. "{SYSROOT}": str(sysroot_dir),
  132. },
  133. executable = False,
  134. )
  135. configure_clang_toolchain = repository_rule(
  136. implementation = _configure_clang_toolchain_impl,
  137. configure = True,
  138. local = True,
  139. attrs = {
  140. "_clang_toolchain_build": attr.label(
  141. default = Label("//bazel/cc_toolchains:clang_toolchain.BUILD"),
  142. allow_single_file = True,
  143. ),
  144. "_clang_cc_toolchain_config": attr.label(
  145. default = Label(
  146. "//bazel/cc_toolchains:clang_cc_toolchain_config.bzl",
  147. ),
  148. allow_single_file = True,
  149. ),
  150. "_clang_detected_variables_template": attr.label(
  151. default = Label(
  152. "//bazel/cc_toolchains:clang_detected_variables.tpl.bzl",
  153. ),
  154. allow_single_file = True,
  155. ),
  156. },
  157. environ = ["CC"],
  158. )