clang_detection.bzl 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  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 detect and configure Clang (and LLVM) toolchain.
  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. # Tools that we verify are present as part of the detected Clang & LLVM toolchain.
  10. _CLANG_LLVM_TOOLS = [
  11. "llvm-ar",
  12. "ld.lld",
  13. "clang-cpp",
  14. "clang",
  15. "clang++",
  16. "llvm-dwp",
  17. "llvm-cov",
  18. "llvm-nm",
  19. "llvm-objcopy",
  20. "llvm-strip",
  21. ]
  22. def _run(repository_ctx, cmd):
  23. """Runs the provided `cmd`, checks for failure, and returns the result."""
  24. exec_result = repository_ctx.execute(cmd, timeout = 10)
  25. if exec_result.return_code != 0:
  26. fail("Unable to run command successfully: %s" % str(cmd))
  27. return exec_result
  28. def _find_clang(repository_ctx):
  29. """Returns the path to a Clang executable if it can find one.
  30. This assumes the `CC` environment variable points to a Clang binary or
  31. looks for one on the path.
  32. """
  33. cc_env = repository_ctx.os.environ.get("CC")
  34. if not cc_env:
  35. # Without a specified `CC` name, simply look for `clang`.
  36. clang = repository_ctx.which("clang")
  37. elif "/" not in cc_env:
  38. # Lookup relative `CC` names according to the system `PATH`.
  39. clang = repository_ctx.which(cc_env)
  40. else:
  41. # An absolute `CC` path is simply be used directly.
  42. clang = repository_ctx.path(cc_env)
  43. if not clang.exists:
  44. fail(("The `CC` environment variable is set to a path (`%s`) " +
  45. "that doesn't exist.") % cc_env)
  46. # Check if either of the `which` invocations fail.
  47. if not clang:
  48. missing = "`clang`"
  49. if cc_env:
  50. missing = "`%s` (from the `CC` environment variable)" % cc_env
  51. fail("Unable to find the %s executable on the PATH." % missing)
  52. version_output = _run(repository_ctx, [clang, "--version"]).stdout
  53. if "clang" not in version_output:
  54. fail(("Selected Clang executable (`%s`) does not appear to actually " +
  55. "be Clang.") % clang)
  56. # Make sure this is part of a complete Clang and LLVM toolchain.
  57. for tool in _CLANG_LLVM_TOOLS:
  58. if not clang.dirname.get_child(tool).exists:
  59. fail(("Couldn't find executable `%s` that is expected to be part " +
  60. "of the Clang and LLVM toolchain detected with `%s`.") %
  61. (tool, clang))
  62. return clang
  63. def _compute_clang_resource_dir(repository_ctx, clang):
  64. """Runs the `clang` binary to get its resource dir."""
  65. output = _run(
  66. repository_ctx,
  67. [clang, "-no-canonical-prefixes", "--print-resource-dir"],
  68. ).stdout
  69. # The only line printed is this path.
  70. return output.splitlines()[0]
  71. def _compute_clang_cpp_include_search_paths(repository_ctx, clang):
  72. """Runs the `clang` binary and extracts the include search paths.
  73. Returns the resulting paths as a list of strings.
  74. """
  75. # The only way to get this out of Clang currently is to parse the verbose
  76. # output of the compiler when it is compiling C++ code.
  77. cmd = [
  78. clang,
  79. # Avoid canonicalizing away symlinks.
  80. "-no-canonical-prefixes",
  81. # Extract verbose output.
  82. "-v",
  83. # Just parse the input, don't generate outputs.
  84. "-fsyntax-only",
  85. # Use libc++ rather than any other standard library.
  86. "-stdlib=libc++",
  87. # Force the language to be C++.
  88. "-x",
  89. "c++",
  90. # Read in an empty input file.
  91. "/dev/null",
  92. ]
  93. # Note that verbose output is on stderr, not stdout!
  94. output = _run(repository_ctx, cmd).stderr.splitlines()
  95. # Return the list of directories printed for system headers. These are the
  96. # only ones that Bazel needs us to manually provide. We find these by
  97. # searching for a begin and end marker. We also have to strip off a leading
  98. # space from each path.
  99. include_begin = output.index("#include <...> search starts here:") + 1
  100. include_end = output.index("End of search list.", include_begin)
  101. return [
  102. repository_ctx.path(s.lstrip(" "))
  103. for s in output[include_begin:include_end]
  104. ]
  105. def _detect_clang_toolchain_impl(repository_ctx):
  106. # First just symlink in the untemplated parts of the toolchain repo.
  107. repository_ctx.symlink(repository_ctx.attr._clang_toolchain_build, "BUILD")
  108. repository_ctx.symlink(
  109. repository_ctx.attr._clang_cc_toolchain_config,
  110. "cc_toolchain_config.bzl",
  111. )
  112. clang = _find_clang(repository_ctx)
  113. resource_dir = _compute_clang_resource_dir(repository_ctx, clang)
  114. include_dirs = _compute_clang_cpp_include_search_paths(
  115. repository_ctx,
  116. clang,
  117. )
  118. repository_ctx.template(
  119. "clang_detected_variables.bzl",
  120. repository_ctx.attr._clang_detected_variables_template,
  121. substitutions = {
  122. "{LLVM_BINDIR}": str(clang.dirname),
  123. "{CLANG_RESOURCE_DIR}": resource_dir,
  124. "{CLANG_INCLUDE_DIRS_LIST}": str([str(path) for path in include_dirs]),
  125. },
  126. executable = False,
  127. )
  128. detect_clang_toolchain = repository_rule(
  129. implementation = _detect_clang_toolchain_impl,
  130. configure = True,
  131. local = True,
  132. attrs = {
  133. "_clang_toolchain_build": attr.label(
  134. default = Label("//bazel/cc_toolchains:clang_toolchain.BUILD"),
  135. allow_single_file = True,
  136. ),
  137. "_clang_cc_toolchain_config": attr.label(
  138. default = Label("//bazel/cc_toolchains:clang_cc_toolchain_config.bzl"),
  139. allow_single_file = True,
  140. ),
  141. "_clang_detected_variables_template": attr.label(
  142. default = Label("//bazel/cc_toolchains:clang_detected_variables.tpl.bzl"),
  143. allow_single_file = True,
  144. ),
  145. },
  146. environ = ["CC"],
  147. )