614 lines (543 sloc) 23.8 KB
# Copyright 2016 The Bazel Authors. All rights reserved.
# 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
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# See the License for the specific language governing permissions and
# limitations under the License.
"""Rules for configuring the C++ toolchain (experimental)."""
def _get_value(it):
"""Convert `it` in serialized protobuf format."""
if type(it) == "int":
return str(it)
elif type(it) == "bool":
return "true" if it else "false"
return "\"%s\"" % it
def _build_crosstool(d, prefix=" "):
"""Convert `d` to a string version of a CROSSTOOL file content."""
lines = []
for k in d:
if type(d[k]) == "list":
for it in d[k]:
lines.append("%s%s: %s" % (prefix, k, _get_value(it)))
lines.append("%s%s: %s" % (prefix, k, _get_value(d[k])))
return "\n".join(lines)
def _build_tool_path(d):
"""Build the list of tool_path for the CROSSTOOL file."""
lines = []
for k in d:
lines.append(" tool_path {name: \"%s\" path: \"%s\" }" % (k, d[k]))
return "\n".join(lines)
def auto_configure_fail(msg):
"""Output failure message when auto configuration fails."""
red = "\033[0;31m"
no_color = "\033[0m"
fail("\n%sAuto-Configuration Error:%s %s\n" % (red, no_color, msg))
def auto_configure_warning(msg):
"""Output warning message during auto configuration."""
yellow = "\033[1;33m"
no_color = "\033[0m"
print("\n%sAuto-Configuration Warning:%s %s\n" % (yellow, no_color, msg))
def _get_env_var(repository_ctx, name, default = None):
"""Find an environment variable in system path."""
if name in repository_ctx.os.environ:
return repository_ctx.os.environ[name]
if default != None:
auto_configure_warning("'%s' environment variable is not set, using '%s' as default" % (name, default))
return default
auto_configure_fail("'%s' environment variable is not set" % name)
def _which(repository_ctx, cmd, default = None):
"""A wrapper around repository_ctx.which() to provide a fallback value."""
result = repository_ctx.which(cmd)
return default if result == None else str(result)
def _which_cmd(repository_ctx, cmd, default = None):
"""Find cmd in PATH using repository_ctx.which() and fail if cannot find it."""
result = repository_ctx.which(cmd)
if result != None:
return str(result)
path = _get_env_var(repository_ctx, "PATH")
if default != None:
auto_configure_warning("Cannot find %s in PATH, using '%s' as default.\nPATH=%s" % (cmd, default, path))
return default
auto_configure_fail("Cannot find %s in PATH, please make sure %s is installed and add its directory in PATH.\nPATH=%s" % (cmd, cmd, path))
return str(result)
def _execute(repository_ctx, command, environment = None):
"""Execute a command, return stdout if succeed and throw an error if it fails."""
if environment:
result = repository_ctx.execute(command, environment = environment)
result = repository_ctx.execute(command)
if result.stderr:
return result.stdout.strip()
def _get_tool_paths(repository_ctx, darwin, cc):
"""Compute the path to the various tools."""
return {k: _which(repository_ctx, k, "/usr/bin/" + k)
for k in [
]} + {
"gcc": cc,
"ar": "/usr/bin/libtool"
if darwin else _which(repository_ctx, "ar", "/usr/bin/ar")
def _cplus_include_paths(repository_ctx):
"""Use ${CPLUS_INCLUDE_PATH} to compute the list of flags for cxxflag."""
if "CPLUS_INCLUDE_PATH" in repository_ctx.os.environ:
result = []
for p in repository_ctx.os.environ["CPLUS_INCLUDE_PATH"].split(":"):
p = str(repository_ctx.path(p)) # Normalize the path
result.append("-I" + p)
return result
return []
def _get_cpu_value(repository_ctx):
"""Compute the cpu_value based on the OS name."""
os_name =
if os_name.startswith("mac os"):
return "darwin"
if os_name.find("freebsd") != -1:
return "freebsd"
if os_name.find("windows") != -1:
return "x64_windows"
# Use uname to figure out whether we are on x86_32 or x86_64
result = repository_ctx.execute(["uname", "-m"])
if result.stdout.strip() in ["power", "ppc64le", "ppc"]:
return "ppc"
return "k8" if result.stdout.strip() in ["amd64", "x86_64", "x64"] else "piii"
_INC_DIR_MARKER_BEGIN = "#include <...>"
# OSX add " (framework directory)" at the end of line, strip it.
_OSX_FRAMEWORK_SUFFIX = " (framework directory)"
def _cxx_inc_convert(path):
"""Convert path returned by cc -E xc++ in a complete path."""
path = path.strip()
if path.endswith(_OSX_FRAMEWORK_SUFFIX):
path = path[:-_OSX_FRAMEWORK_SUFFIX_LEN].strip()
return path
def _get_cxx_inc_directories(repository_ctx, cc):
"""Compute the list of default C++ include directories."""
result = repository_ctx.execute([cc, "-E", "-xc++", "-", "-v"])
index1 = result.stderr.find(_INC_DIR_MARKER_BEGIN)
if index1 == -1:
return []
index1 = result.stderr.find("\n", index1)
if index1 == -1:
return []
index2 = result.stderr.rfind("\n ")
if index2 == -1 or index2 < index1:
return []
index2 = result.stderr.find("\n", index2 + 1)
if index2 == -1:
inc_dirs = result.stderr[index1 + 1:]
inc_dirs = result.stderr[index1 + 1:index2].strip()
return [repository_ctx.path(_cxx_inc_convert(p))
for p in inc_dirs.split("\n")]
def _add_option_if_supported(repository_ctx, cc, option):
"""Checks that `option` is supported by the C compiler."""
result = repository_ctx.execute([
return [option] if result.stderr.find(option) == -1 else []
def _is_gold_supported(repository_ctx, cc):
"""Checks that `gold` is supported by the C compiler."""
result = repository_ctx.execute([
return result.return_code == 0
def _crosstool_content(repository_ctx, cc, cpu_value, darwin):
"""Return the content for the CROSSTOOL file, in a dictionary."""
supports_gold_linker = _is_gold_supported(repository_ctx, cc)
return {
"abi_version": "local",
"abi_libc_version": "local",
"builtin_sysroot": "",
"compiler": "compiler",
"host_system_name": "local",
"needsPic": True,
"supports_gold_linker": supports_gold_linker,
"supports_incremental_linker": False,
"supports_fission": False,
"supports_interface_shared_objects": False,
"supports_normalizing_ar": False,
"supports_start_end_lib": supports_gold_linker,
"target_libc": "macosx" if darwin else "local",
"target_cpu": cpu_value,
"target_system_name": "local",
"cxx_flag": [
] + _cplus_include_paths(repository_ctx),
"linker_flag": [
"-lm", # Some systems expect -lm in addition to -lstdc++
# Anticipated future default.
] + (
["-fuse-ld=gold"] if supports_gold_linker else []
) + _add_option_if_supported(
repository_ctx, cc, "-Wl,-no-as-needed"
) + _add_option_if_supported(
repository_ctx, cc, "-Wl,-z,relro,-z,now"
) + (
] if darwin else [
"-B" + str(repository_ctx.path(cc).dirname),
# Always have -B/usr/bin, see
# Stamp the binary with a unique identifier.
# Gold linker only? Can we enable this by default?
# "-Wl,--warn-execstack",
# "-Wl,--detect-odr-violations"
] + _add_option_if_supported(
# Have gcc return the exit code from ld.
repository_ctx, cc, "-pass-exit-codes"
"ar_flag": ["-static", "-s", "-o"] if darwin else [],
"cxx_builtin_include_directory": _get_cxx_inc_directories(repository_ctx, cc),
"objcopy_embed_flag": ["-I", "binary"],
# If the compiler sometimes rewrites paths in the .d files without symlinks
# (ie when they're shorter), it confuses Bazel's logic for verifying all
# #included header files are listed as inputs to the action.
_add_option_if_supported(repository_ctx, cc, "-fno-canonical-system-headers") + [
# Make C++ compilation deterministic. Use linkstamping instead of these
# compiler symbols.
"compiler_flag": [
# Security hardening requires optimization.
# We need to undef it as some distributions now have it enabled by default.
# All warnings are enabled. Maybe enable -Werror as well?
# Enable a few more warnings that aren't part of -Wall.
] + (["-Wthread-safety", "-Wself-assign"] if darwin else [
"-B" + str(repository_ctx.path(cc).dirname),
# Always have -B/usr/bin, see
]) + (
# Disable problematic warnings.
_add_option_if_supported(repository_ctx, cc, "-Wunused-but-set-parameter") +
# has false positives
_add_option_if_supported(repository_ctx, cc, "-Wno-free-nonheap-object") +
# Enable coloring even if there's no attached terminal. Bazel removes the
# escape sequences if --nocolor is specified.
_add_option_if_supported(repository_ctx, cc, "-fcolor-diagnostics")) + [
# Keep stack frames for debugging, even in opt mode.
# TODO(pcloudy): Remove this after MSVC CROSSTOOL becomes default on Windows
def _get_windows_crosstool_content(repository_ctx):
"""Return the content of msys crosstool which is still the default CROSSTOOL on Windows."""
msys_root = _execute(repository_ctx, ["cygpath", "-m", "/"])
return (
' abi_version: "local"\n' +
' abi_libc_version: "local"\n' +
' builtin_sysroot: ""\n' +
' compiler: "windows_msys64"\n' +
' host_system_name: "local"\n' +
" needsPic: false\n" +
' target_libc: "local"\n' +
' target_cpu: "x64_windows"\n' +
' target_system_name: "local"\n' +
' tool_path { name: "ar" path: "%susr/bin/ar" }\n' % msys_root +
' tool_path { name: "compat-ld" path: "%susr/bin/ld" }\n' % msys_root +
' tool_path { name: "cpp" path: "%susr/bin/cpp" }\n' % msys_root +
' tool_path { name: "dwp" path: "%susr/bin/dwp" }\n' % msys_root +
' tool_path { name: "gcc" path: "%susr/bin/gcc" }\n' % msys_root +
' cxx_flag: "-std=gnu++0x"\n' +
' linker_flag: "-lstdc++"\n' +
' cxx_builtin_include_directory: "%s"\n' % msys_root +
' cxx_builtin_include_directory: "/usr/"\n' +
' tool_path { name: "gcov" path: "%susr/bin/gcov" }\n' % msys_root +
' tool_path { name: "ld" path: "%susr/bin/ld" }\n' % msys_root +
' tool_path { name: "nm" path: "%susr/bin/nm" }\n' % msys_root +
' tool_path { name: "objcopy" path: "%susr/bin/objcopy" }\n' % msys_root +
' objcopy_embed_flag: "-I"\n' +
' objcopy_embed_flag: "binary"\n' +
' tool_path { name: "objdump" path: "%susr/bin/objdump" }\n' % msys_root +
' tool_path { name: "strip" path: "%susr/bin/strip" }'% msys_root )
def _opt_content(darwin):
"""Return the content of the opt specific section of the CROSSTOOL file."""
return {
"compiler_flag": [
# No debug symbols.
# Maybe we should enable for opt or
# even generally? However, that can't happen here, as it requires special
# handling in Bazel.
# Conservative choice for -O
# -O3 can increase binary size and even slow down the resulting binaries.
# Profile first and / or use FDO if you need better performance than this.
# Security hardening on by default.
# Conservative choice; -D_FORTIFY_SOURCE=2 may be unsafe in some cases.
# Disable assertions
# Removal of unused code and data at link time (can this increase binary size in some cases?).
"linker_flag": [] if darwin else ["-Wl,--gc-sections"]
def _dbg_content():
"""Return the content of the dbg specific section of the CROSSTOOL file."""
# Enable debug symbols
return {"compiler_flag": "-g"}
def _get_system_root(repository_ctx):
r"""Get System root path on Windows, default is C:\\Windows."""
if "SYSTEMROOT" in repository_ctx.os.environ:
return repository_ctx.os.environ["SYSTEMROOT"]
auto_configure_warning("SYSTEMROOT is not set, using default SYSTEMROOT=C:\\Windows")
return "C:\\Windows"
def _find_cc(repository_ctx):
"""Find the C++ compiler."""
cc_name = "gcc"
if "CC" in repository_ctx.os.environ:
cc_name = repository_ctx.os.environ["CC"].strip()
if not cc_name:
cc_name = "gcc"
if cc_name.startswith("/"):
# Absolute path, maybe we should make this suported by our which function.
return cc_name
cc = repository_ctx.which(cc_name)
if cc == None:
"Cannot find gcc, either correct your path or set the CC" +
" environment variable")
return cc
def _find_cuda(repository_ctx):
"""Find out if and where cuda is installed."""
if "CUDA_PATH" in repository_ctx.os.environ:
return repository_ctx.os.environ["CUDA_PATH"]
nvcc = _which(repository_ctx, "nvcc.exe")
if nvcc:
return nvcc[:-len("/bin/nvcc.exe")]
return None
def _find_python(repository_ctx):
"""Find where is python on Windows."""
if "BAZEL_PYTHON" in repository_ctx.os.environ:
python_binary = repository_ctx.os.environ["BAZEL_PYTHON"]
if not python_binary.endswith(".exe"):
python_binary = python_binary + ".exe"
return python_binary
auto_configure_warning("'BAZEL_PYTHON' is not set, start looking for python in PATH.")
python_binary = _which_cmd(repository_ctx, "python.exe")
auto_configure_warning("Python found at %s" % python_binary)
return python_binary
def _find_vs_path(repository_ctx):
"""Find Visual Studio install path."""
if "BAZEL_VS" in repository_ctx.os.environ:
return repository_ctx.os.environ["BAZEL_VS"]
auto_configure_warning("'BAZEL_VS' is not set, start looking for the latest Visual Studio installed.")
# Query the installed visual stduios on the machine, should return something like:
# HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\VisualStudio\14.0
# HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\VisualStudio\8.0
# ...
reg_binary = _get_system_root(repository_ctx) + "\\system32\\reg.exe"
result = _execute(repository_ctx, [reg_binary, "query", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Wow6432Node\\Microsoft\\VisualStudio"])
vs_versions = result.split("\n")
vs_dir = None
vs_version_number = -1
for entry in vs_versions:
entry = entry.strip()
# Query InstallDir to find out where a specific version of VS is installed.
# The output looks like if succeeded:
# HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\VisualStudio\14.0
# InstallDir REG_SZ C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\IDE\
result = repository_ctx.execute([reg_binary, "query", entry, "-v", "InstallDir"])
if not result.stderr:
for line in result.stdout.split("\n"):
line = line.strip()
if line.startswith("InstallDir"):
path_segments = [ seg for seg in line[len("InstallDir REG_SZ "):].split("\\") if seg ]
# Extract version number X from "Microsoft Visual Studio X.0"
version_number = int(path_segments[-3].split(" ")[-1][:-len(".0")])
if version_number > vs_version_number:
vs_version_number = version_number
vs_dir = "\\".join(path_segments[:-2])
if not vs_dir:
auto_configure_fail("Visual Studio not found on your machine.")
auto_configure_warning("Visual Studio found at %s" % vs_dir)
return vs_dir
def _find_env_vars(repository_ctx, vs_path):
"""Get environment variables set by VCVARSALL.BAT."""
vsvars = vs_path + "/VC/VCVARSALL.BAT"
"@echo off\n" +
"call \"" + vsvars + "\" amd64 \n" +
env = repository_ctx.os.environ
if "PATH" not in env:
# Running VCVARSALL.BAT needs %SYSTEMROOT%\\system32 to be in PATH
env["PATH"] = env["PATH"] + ";" + _get_system_root(repository_ctx) + "\\system32"
envs = _execute(repository_ctx, ["wrapper/get_env.bat"], environment=env).split(",")
env_map = {}
for env in envs:
key, value = env.split("=")
env_map[key] = value.replace("\\", "\\\\")
return env_map
def _is_support_whole_archive(repository_ctx, vs_dir):
"""Run MSVC linker alone to see if it supports /WHOLEARCHIVE."""
env = repository_ctx.os.environ
return False
result = _execute(repository_ctx, [vs_dir + "/VC/BIN/amd64/link"])
return result.find("/WHOLEARCHIVE") != -1
def _cuda_compute_capabilities(repository_ctx):
"""Returns a list of strings representing cuda compute capabilities."""
if "CUDA_COMPUTE_CAPABILITIES" not in repository_ctx.os.environ:
return ["3.5", "5.2"]
capabilities_str = repository_ctx.os.environ["CUDA_COMPUTE_CAPABILITIES"]
capabilities = capabilities_str.split(",")
for capability in capabilities:
# Workaround for Skylark's lack of support for regex. This check should
# be equivalent to checking:
# if re.match("[0-9]+.[0-9]+", capability) == None:
parts = capability.split(".")
if len(parts) != 2 or not parts[0].isdigit() or not parts[1].isdigit():
auto_configure_fail("Invalid compute capability: %s" % capability)
return capabilities
def _tpl(repository_ctx, tpl, substitutions={}, out=None):
if not out:
out = tpl
Label("@bazel_tools//tools/cpp:%s.tpl" % tpl),
def _get_env(repository_ctx):
"""Convert the environment in a list of export if in Homebrew."""
env = repository_ctx.os.environ
return "\n".join([
"export %s='%s'" % (k, env[k].replace("'", "'\\''"))
for k in env
if k != "_" and k.find(".") == -1
return ""
def _impl(repository_ctx):
repository_ctx.file("tools/cpp/", "int main() {}")
cpu_value = _get_cpu_value(repository_ctx)
if cpu_value == "freebsd":
# This is defaulting to the static crosstool, we should eventually do those platform too.
# Theorically, FreeBSD should be straightforward to add but we cannot run it in a docker
# container so escaping until we have proper tests for FreeBSD.
repository_ctx.symlink(Label("@bazel_tools//tools/cpp:CROSSTOOL"), "CROSSTOOL")
repository_ctx.symlink(Label("@bazel_tools//tools/cpp:BUILD.static"), "BUILD")
elif cpu_value == "x64_windows":
repository_ctx.symlink(Label("@bazel_tools//tools/cpp:BUILD.static"), "BUILD")
msvc_wrapper = repository_ctx.path(Label("@bazel_tools//tools/cpp:CROSSTOOL")).dirname.get_child("wrapper").get_child("bin")
for f in ["msvc_cl.bat", "msvc_link.bat", "msvc_nop.bat"]:
repository_ctx.symlink(msvc_wrapper.get_child(f), "wrapper/bin/" + f)
msvc_wrapper = msvc_wrapper.get_child("pydir")
for f in ["", ""]:
repository_ctx.symlink(msvc_wrapper.get_child(f), "wrapper/bin/pydir/" + f)
python_binary = _find_python(repository_ctx)
_tpl(repository_ctx, "wrapper/bin/call_python.bat", {"%{python_binary}": python_binary})
vs_path = _find_vs_path(repository_ctx)
env = _find_env_vars(repository_ctx, vs_path)
python_dir = python_binary[0:-10].replace("\\", "\\\\")
include_paths = env["INCLUDE"] + (python_dir + "include")
lib_paths = env["LIB"] + (python_dir + "libs")
lib_tool = vs_path.replace("\\", "\\\\") + "/VC/bin/amd64/lib.exe"
if _is_support_whole_archive(repository_ctx, vs_path):
support_whole_archive = "True"
support_whole_archive = "False"
tmp_dir = _get_env_var(repository_ctx, "TMP", "C:\\Windows\\Temp").replace("\\", "\\\\")
# Make sure nvcc.exe is in PATH
paths = env["PATH"]
cuda_path = _find_cuda(repository_ctx)
if cuda_path:
paths = (cuda_path.replace("\\", "\\\\") + "/bin;") + paths
compute_capabilities = _cuda_compute_capabilities(repository_ctx)
_tpl(repository_ctx, "wrapper/bin/pydir/", {
"%{tmp}": tmp_dir,
"%{path}": paths,
"%{include}": include_paths,
"%{lib}": lib_paths,
"%{lib_tool}": lib_tool,
"%{support_whole_archive}": support_whole_archive,
"%{cuda_compute_capabilities}": ", ".join(
["\"%s\"" % c for c in compute_capabilities]),
# nvcc will generate some source files under tmp_dir
cxx_include_directories = [ "cxx_builtin_include_directory: \"%s\"" % tmp_dir ]
for path in include_paths.split(";"):
if path:
cxx_include_directories.append(("cxx_builtin_include_directory: \"%s\"" % path))
_tpl(repository_ctx, "CROSSTOOL", {
"%{cpu}": cpu_value,
"%{content}": _get_windows_crosstool_content(repository_ctx),
"%{opt_content}": "",
"%{dbg_content}": "",
"%{cxx_builtin_include_directory}": "\n".join(cxx_include_directories),
darwin = cpu_value == "darwin"
cc = _find_cc(repository_ctx)
tool_paths = _get_tool_paths(repository_ctx, darwin,
"" if darwin else str(cc))
crosstool_content = _crosstool_content(repository_ctx, cc, cpu_value, darwin)
opt_content = _opt_content(darwin)
dbg_content = _dbg_content()
_tpl(repository_ctx, "BUILD", {
"%{name}": cpu_value,
"%{supports_param_files}": "0" if darwin else "1",
"%{cc_compiler_deps}": ":cc_wrapper" if darwin else ":empty",
"" if darwin else "",
{"%{cc}": str(cc), "%{env}": _get_env(repository_ctx)},
_tpl(repository_ctx, "CROSSTOOL", {
"%{cpu}": cpu_value,
"%{content}": _build_crosstool(crosstool_content) + "\n" +
"%{opt_content}": _build_crosstool(opt_content, " "),
"%{dbg_content}": _build_crosstool(dbg_content, " "),
"%{cxx_builtin_include_directory}": "",
cc_autoconf = repository_rule(implementation=_impl, local=True)
def cc_configure():
"""A C++ configuration rules that generate the crosstool file."""
native.bind(name="cc_toolchain", actual="@local_config_cc//:toolchain")