rules_foreign_cc/foreign_cc/private/cmake_script.bzl

403 lines
16 KiB
Python

""" Contains all logic for calling CMake for building external libraries/binaries """
load(":cc_toolchain_util.bzl", "absolutize_path_in_str")
def _escape_dquote_bash(text):
""" Escape double quotes in flag lists for use in bash strings that set environment variables """
# We use a starlark raw string to prevent the need to escape backslashes for starlark as well.
return text.replace('"', r'\\\"')
def _escape_dquote_bash_crosstool(text):
""" Escape double quotes in flag lists for use in bash strings to be passed to a HEREDOC.
This requires an additional level of indirection as CMake requires the variables to be escaped inside the crosstool file
"""
# We use a starlark raw string to prevent the need to escape backslashes for starlark as well.
return text.replace('"', r'\\\\\\\"')
_TARGET_OS_PARAMS = {
"android": {
"ANDROID": "YES",
"CMAKE_SYSTEM_NAME": "Linux",
},
"linux": {
"CMAKE_SYSTEM_NAME": "Linux",
},
}
_TARGET_ARCH_PARAMS = {
"aarch64": {
"CMAKE_SYSTEM_PROCESSOR": "aarch64",
},
"x86_64": {
"CMAKE_SYSTEM_PROCESSOR": "x86_64",
},
}
def create_cmake_script(
workspace_name,
target_os,
target_arch,
host_os,
generator,
cmake_path,
tools,
flags,
install_prefix,
root,
no_toolchain_file,
user_cache,
user_env,
options,
cmake_commands,
include_dirs = [],
cmake_prefix = None,
is_debug_mode = True):
"""Constructs CMake script to be passed to cc_external_rule_impl.
Args:
workspace_name: current workspace name
target_os: The target OS for the build
target_arch: The target arch for the build
host_os: The execution OS for the build
generator: The generator target for cmake to use
cmake_path: The path to the cmake executable
tools: cc_toolchain tools (CxxToolsInfo)
flags: cc_toolchain flags (CxxFlagsInfo)
install_prefix: value ot pass to CMAKE_INSTALL_PREFIX
root: sources root relative to the $EXT_BUILD_ROOT
no_toolchain_file: if False, CMake toolchain file will be generated, otherwise not
user_cache: dictionary with user's values of cache initializers
user_env: dictionary with user's values for CMake environment variables
options: other CMake options specified by user
cmake_commands: A list of cmake commands for building and installing targets
include_dirs: Optional additional include directories. Defaults to [].
cmake_prefix: Optional prefix before the cmake command (without the trailing space).
is_debug_mode: If the compilation mode is `debug`. Defaults to True.
Returns:
list: Lines of bash which make up the build script
"""
merged_prefix_path = _merge_prefix_path(user_cache, include_dirs)
toolchain_dict = _fill_crossfile_from_toolchain(workspace_name, tools, flags)
params = None
keys_with_empty_values_in_user_cache = [key for key in user_cache if user_cache.get(key) == ""]
if no_toolchain_file:
params = _create_cache_entries_env_vars(toolchain_dict, user_cache, user_env)
else:
params = _create_crosstool_file_text(toolchain_dict, user_cache, user_env)
build_type = params.cache.get(
"CMAKE_BUILD_TYPE",
"Debug" if is_debug_mode else "Release",
)
params.cache.update({
"CMAKE_BUILD_TYPE": build_type,
"CMAKE_INSTALL_PREFIX": install_prefix,
"CMAKE_PREFIX_PATH": merged_prefix_path,
})
# Give user the ability to suppress some value, taken from Bazel's toolchain,
# or to suppress calculated CMAKE_BUILD_TYPE
# If the user passes "CMAKE_BUILD_TYPE": "" (empty string),
# CMAKE_BUILD_TYPE will not be passed to CMake
_wipe_empty_values(params.cache, keys_with_empty_values_in_user_cache)
# However, if no CMAKE_RANLIB was passed, pass the empty value for it explicitly,
# as it is legacy and autodetection of ranlib made by CMake automatically
# breaks some cross compilation builds,
# see https://github.com/envoyproxy/envoy/pull/6991
if not params.cache.get("CMAKE_RANLIB"):
params.cache.update({"CMAKE_RANLIB": ""})
# Avoid CMake passing the wrong linker flags when cross compiling
# by setting CMAKE_SYSTEM_NAME and CMAKE_SYSTEM_PROCESSOR,
# see https://github.com/bazelbuild/rules_foreign_cc/issues/289,
# and https://github.com/bazelbuild/rules_foreign_cc/pull/1062
if target_os != host_os and target_os != "unknown":
params.cache.update(_TARGET_OS_PARAMS.get(target_os, {}))
params.cache.update(_TARGET_ARCH_PARAMS.get(target_arch, {}))
set_env_vars = [
"export {}=\"{}\"".format(key, _escape_dquote_bash(params.env[key]))
for key in params.env
]
str_cmake_cache_entries = " ".join([
"-D{}=\"{}\"".format(key, _escape_dquote_bash_crosstool(params.cache[key]))
for key in params.cache
])
# Add definitions for all environment variables
script = set_env_vars
directory = "$$EXT_BUILD_ROOT$$/" + root
script.append("##enable_tracing##")
# Configure the CMake generate command
cmake_prefixes = [cmake_prefix] if cmake_prefix else []
script.append(" ".join(cmake_prefixes + [
cmake_path,
str_cmake_cache_entries,
" ".join(options),
# Generator is always set last and will override anything specified by the user
"-G '{}'".format(generator),
directory,
]))
script.extend(cmake_commands)
script.append("##disable_tracing##")
return params.commands + script
def _wipe_empty_values(cache, keys_with_empty_values_in_user_cache):
for key in keys_with_empty_values_in_user_cache:
if cache.get(key) != None:
cache.pop(key)
# From CMake documentation: ;-list of directories specifying installation prefixes to be searched...
def _merge_prefix_path(user_cache, include_dirs):
user_prefix = user_cache.get("CMAKE_PREFIX_PATH")
values = ["$$EXT_BUILD_DEPS$$"] + include_dirs
if user_prefix != None:
# remove it, it is gonna be merged specifically
user_cache.pop("CMAKE_PREFIX_PATH")
values.append(user_prefix.strip("\"'"))
return ";".join(values)
_CMAKE_ENV_VARS_FOR_CROSSTOOL = {
"ASMFLAGS": struct(value = "CMAKE_ASM_FLAGS_INIT", replace = False),
"CC": struct(value = "CMAKE_C_COMPILER", replace = True),
"CFLAGS": struct(value = "CMAKE_C_FLAGS_INIT", replace = False),
"CXX": struct(value = "CMAKE_CXX_COMPILER", replace = True),
"CXXFLAGS": struct(value = "CMAKE_CXX_FLAGS_INIT", replace = False),
}
_CMAKE_CACHE_ENTRIES_CROSSTOOL = {
"CMAKE_AR": struct(value = "CMAKE_AR", replace = True),
"CMAKE_ASM_FLAGS": struct(value = "CMAKE_ASM_FLAGS_INIT", replace = False),
"CMAKE_CXX_ARCHIVE_CREATE": struct(value = "CMAKE_CXX_ARCHIVE_CREATE", replace = False),
"CMAKE_CXX_FLAGS": struct(value = "CMAKE_CXX_FLAGS_INIT", replace = False),
"CMAKE_CXX_LINK_EXECUTABLE": struct(value = "CMAKE_CXX_LINK_EXECUTABLE", replace = True),
"CMAKE_C_ARCHIVE_CREATE": struct(value = "CMAKE_C_ARCHIVE_CREATE", replace = False),
"CMAKE_C_FLAGS": struct(value = "CMAKE_C_FLAGS_INIT", replace = False),
"CMAKE_EXE_LINKER_FLAGS": struct(value = "CMAKE_EXE_LINKER_FLAGS_INIT", replace = False),
"CMAKE_RANLIB": struct(value = "CMAKE_RANLIB", replace = True),
"CMAKE_SHARED_LINKER_FLAGS": struct(value = "CMAKE_SHARED_LINKER_FLAGS_INIT", replace = False),
"CMAKE_STATIC_LINKER_FLAGS": struct(value = "CMAKE_STATIC_LINKER_FLAGS_INIT", replace = False),
}
def _create_crosstool_file_text(toolchain_dict, user_cache, user_env):
cache_entries = _dict_copy(user_cache)
env_vars = _dict_copy(user_env)
_move_dict_values(toolchain_dict, env_vars, _CMAKE_ENV_VARS_FOR_CROSSTOOL)
_move_dict_values(toolchain_dict, cache_entries, _CMAKE_CACHE_ENTRIES_CROSSTOOL)
lines = []
crosstool_vars = []
# The __var_* bash variables that are set here are a method to avoid
# having to quote the values when they are expanded in the HEREDOC.
# We could disable shell expansion by single quoting EOF in the HEREDOC
# but then we loose the ability to expand other variables such as
# $EXT_BUILD_DEPS and so we use this trick to leave expansion turned on in
# the HEREDOC for the crosstool
for key in toolchain_dict:
crosstool_vars.append("__var_{}=\"{}\"".format(key, _escape_dquote_bash_crosstool(toolchain_dict[key])))
if ("CMAKE_AR" == key):
lines.append('set({} "$$__var_{}$$" {})'.format(
key,
key,
'CACHE FILEPATH "Archiver"',
))
else:
lines.append('set({} "$$__var_{}$$")'.format(key, key))
cache_entries.update({
"CMAKE_TOOLCHAIN_FILE": "$$BUILD_TMPDIR$$/crosstool_bazel.cmake",
})
return struct(
commands = sorted(crosstool_vars) + ["cat > crosstool_bazel.cmake << EOF"] + sorted(lines) + ["EOF", ""],
env = env_vars,
cache = cache_entries,
)
def _dict_copy(d):
out = {}
if d:
out.update(d)
return out
def _create_cache_entries_env_vars(toolchain_dict, user_cache, user_env):
_move_dict_values(toolchain_dict, user_env, _CMAKE_ENV_VARS_FOR_CROSSTOOL)
_move_dict_values(toolchain_dict, user_cache, _CMAKE_CACHE_ENTRIES_CROSSTOOL)
merged_env = _translate_from_toolchain_file(toolchain_dict, _CMAKE_ENV_VARS_FOR_CROSSTOOL)
merged_cache = _translate_from_toolchain_file(toolchain_dict, _CMAKE_CACHE_ENTRIES_CROSSTOOL)
# anything left in user's env_entries does not correspond to anything defined by toolchain
# => simple merge
merged_env.update(user_env)
merged_cache.update(user_cache)
return struct(
commands = [],
env = merged_env,
cache = merged_cache,
)
def _translate_from_toolchain_file(toolchain_dict, descriptor_map):
reverse = _reverse_descriptor_dict(descriptor_map)
cl_keyed_toolchain = dict()
keys = toolchain_dict.keys()
for key in keys:
env_var_key = reverse.get(key)
if env_var_key:
cl_keyed_toolchain[env_var_key.value] = toolchain_dict.pop(key)
return cl_keyed_toolchain
def _merge_toolchain_and_user_values(toolchain_dict, user_dict, descriptor_map):
_move_dict_values(toolchain_dict, user_dict, descriptor_map)
cl_keyed_toolchain = _translate_from_toolchain_file(toolchain_dict, descriptor_map)
# anything left in user's env_entries does not correspond to anything defined by toolchain
# => simple merge
cl_keyed_toolchain.update(user_dict)
return cl_keyed_toolchain
def _reverse_descriptor_dict(dict):
out_dict = {}
for key in dict:
value = dict[key]
out_dict[value.value] = struct(value = key, replace = value.replace)
return out_dict
def _move_dict_values(target, source, descriptor_map):
keys = source.keys()
for key in keys:
existing = descriptor_map.get(key)
if existing:
value = source.pop(key)
if existing.replace or target.get(existing.value) == None:
target[existing.value] = value
else:
target[existing.value] = target[existing.value] + " " + value
def _fill_crossfile_from_toolchain(workspace_name, tools, flags):
dict = {}
_sysroot = _find_in_cc_or_cxx(flags, "sysroot")
if _sysroot:
dict["CMAKE_SYSROOT"] = _absolutize(workspace_name, _sysroot)
_ext_toolchain_cc = _find_flag_value(flags.cc, "gcc_toolchain")
if _ext_toolchain_cc:
dict["CMAKE_C_COMPILER_EXTERNAL_TOOLCHAIN"] = _absolutize(workspace_name, _ext_toolchain_cc)
_ext_toolchain_cxx = _find_flag_value(flags.cxx, "gcc_toolchain")
if _ext_toolchain_cxx:
dict["CMAKE_CXX_COMPILER_EXTERNAL_TOOLCHAIN"] = _absolutize(workspace_name, _ext_toolchain_cxx)
# Force convert tools paths to absolute using $EXT_BUILD_ROOT
if tools.cc:
dict["CMAKE_C_COMPILER"] = _absolutize(workspace_name, tools.cc, True)
if tools.cxx:
dict["CMAKE_CXX_COMPILER"] = _absolutize(workspace_name, tools.cxx, True)
if tools.cxx_linker_static:
dict["CMAKE_AR"] = _absolutize(workspace_name, tools.cxx_linker_static, True)
if tools.cxx_linker_static.endswith("/libtool"):
dict["CMAKE_C_ARCHIVE_CREATE"] = "<CMAKE_AR> %s <OBJECTS>" % \
" ".join(flags.cxx_linker_static)
dict["CMAKE_CXX_ARCHIVE_CREATE"] = "<CMAKE_AR> %s <OBJECTS>" % \
" ".join(flags.cxx_linker_static)
if tools.cxx_linker_executable and tools.cxx_linker_executable != tools.cxx:
normalized_path = _absolutize(workspace_name, tools.cxx_linker_executable)
dict["CMAKE_CXX_LINK_EXECUTABLE"] = " ".join([
normalized_path,
"<FLAGS>",
"<CMAKE_CXX_LINK_FLAGS>",
"<LINK_FLAGS>",
"<OBJECTS>",
"-o <TARGET>",
"<LINK_LIBRARIES>",
])
if flags.cc:
dict["CMAKE_C_FLAGS_INIT"] = _join_flags_list(workspace_name, flags.cc)
if flags.cxx:
dict["CMAKE_CXX_FLAGS_INIT"] = _join_flags_list(workspace_name, flags.cxx)
if flags.assemble:
dict["CMAKE_ASM_FLAGS_INIT"] = _join_flags_list(workspace_name, flags.assemble)
# todo this options are needed, but cause a bug because the flags are put in wrong order => keep this line
# if flags.cxx_linker_static:
# lines += [_set_list(ctx, "CMAKE_STATIC_LINKER_FLAGS_INIT", flags.cxx_linker_static)]
if flags.cxx_linker_shared:
dict["CMAKE_SHARED_LINKER_FLAGS_INIT"] = _join_flags_list(workspace_name, flags.cxx_linker_shared)
if flags.cxx_linker_executable:
dict["CMAKE_EXE_LINKER_FLAGS_INIT"] = _join_flags_list(workspace_name, flags.cxx_linker_executable)
return dict
def _find_in_cc_or_cxx(flags, flag_name_no_dashes):
_value = _find_flag_value(flags.cxx, flag_name_no_dashes)
if _value:
return _value
return _find_flag_value(flags.cc, flag_name_no_dashes)
def _find_flag_value(list, flag_name_no_dashes):
one_dash = "-" + flag_name_no_dashes.lstrip(" ")
two_dash = "--" + flag_name_no_dashes.lstrip(" ")
check_for_value = False
for value in list:
value = value.lstrip(" ")
if check_for_value:
return value.lstrip(" =")
_tail = _tail_if_starts_with(value, one_dash)
_tail = _tail_if_starts_with(value, two_dash) if _tail == None else _tail
if _tail != None and len(_tail) > 0:
return _tail.lstrip(" =")
if _tail != None:
check_for_value = True
return None
def _tail_if_starts_with(str, start):
if (str.startswith(start)):
return str[len(start):]
return None
def _absolutize(workspace_name, text, force = False):
if text.strip(" ").startswith("C:") or text.strip(" ").startswith("c:"):
return text
# Use bash parameter substitution to replace backslashes with forward slashes as CMake fails if provided paths containing backslashes
return absolutize_path_in_str(workspace_name, "$${EXT_BUILD_ROOT//\\\\//}$$/", text, force)
def _join_flags_list(workspace_name, flags):
return " ".join([_absolutize(workspace_name, flag) for flag in flags])
export_for_test = struct(
absolutize = _absolutize,
tail_if_starts_with = _tail_if_starts_with,
find_flag_value = _find_flag_value,
fill_crossfile_from_toolchain = _fill_crossfile_from_toolchain,
move_dict_values = _move_dict_values,
reverse_descriptor_dict = _reverse_descriptor_dict,
merge_toolchain_and_user_values = _merge_toolchain_and_user_values,
CMAKE_ENV_VARS_FOR_CROSSTOOL = _CMAKE_ENV_VARS_FOR_CROSSTOOL,
CMAKE_CACHE_ENTRIES_CROSSTOOL = _CMAKE_CACHE_ENTRIES_CROSSTOOL,
)