blob: e8dbfe0cb519ef1ef58a7baa95c72faa6c8e2f59 [file] [edit]
# Copyright 2026 The Abseil Authors.
#
# 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
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Reusable multi-stage CMake build and per-test execution rules for Bazel RBE."""
DEFAULT_CMAKE_MODES = [
("static", "OFF", "OFF"),
("shared", "ON", "OFF"),
("monolithic_shared", "ON", "ON"),
]
_CmakeHeadersInfo = provider(
doc = "Transitive header and include files required to compile CMake targets.",
fields = ["headers"],
)
def _cmake_headers_aspect_impl(target, ctx):
if target.label.workspace_name != "":
return [_CmakeHeadersInfo(headers = depset())]
direct = []
for attr_name in ("srcs", "hdrs", "textual_hdrs"):
if hasattr(ctx.rule.files, attr_name):
for f in getattr(ctx.rule.files, attr_name):
if f.owner.workspace_name == "" and not f.path.endswith((".cc", ".c")):
direct.append(f)
transitive = [
d[_CmakeHeadersInfo].headers
for d in getattr(ctx.rule.attr, "deps", [])
if _CmakeHeadersInfo in d
]
return [_CmakeHeadersInfo(headers = depset(direct, transitive = transitive))]
_cmake_headers_aspect = aspect(
implementation = _cmake_headers_aspect_impl,
attr_aspects = ["deps"],
)
def _cmake_sh_test_impl(ctx):
out = ctx.actions.declare_file(ctx.label.name + ".sh")
log_paths = " ".join(['"%s"' % f.short_path for f in ctx.files.data if f.path.endswith(".log")])
ctx.actions.write(
output = out,
content = ctx.attr.content.replace("__CMAKE_LOG_FILES__", log_paths),
is_executable = True,
)
runfiles = ctx.runfiles(files = ctx.files.data)
for d in ctx.attr.data:
runfiles = runfiles.merge(d[DefaultInfo].default_runfiles)
return [DefaultInfo(
files = depset([out]),
executable = out,
runfiles = runfiles,
)]
_cmake_sh_test = rule(
implementation = _cmake_sh_test_impl,
attrs = {
"content": attr.string(mandatory = True),
"data": attr.label_list(allow_files = True),
},
exec_groups = {
"test": exec_group(),
},
test = True,
)
def _cmake_configure_impl(ctx):
out_tar = ctx.actions.declare_file(ctx.label.name + ".tar")
gtest_file = ctx.file.googletest_archive
inputs = depset(ctx.files.srcs + [gtest_file])
cmd = """
set -euo pipefail
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${{PATH}}"
GTEST_ARCHIVE="$(realpath "{gtest_path}")"
OUT_TAR="$(realpath -m "{out_tar_path}")"
WORK_ROOT="/tmp/cmake-work"
exec 9>/tmp/cmake-work.lock
flock 9
rm -rf "${{WORK_ROOT}}"
mkdir -p "${{WORK_ROOT}}/src"
cp -RL {top_level_entries} "${{WORK_ROOT}}/src/"
chmod -R u+w "${{WORK_ROOT}}/src"
# Create 0-byte stubs for all source/header files referenced in CMakeLists.txt
# and CMake/*.cmake so CMake generates the full Ninja/CTest graph without
# depending on any project .cc or .h file contents.
python3 -c '
import os, re
src_root = "/tmp/cmake-work/src"
for root, dirs, files in os.walk(src_root):
for f in files:
os.utime(os.path.join(root, f), (946684800, 946684800))
if f == "CMakeLists.txt" or f.endswith(".cmake"):
with open(os.path.join(root, f)) as cf:
text = cf.read()
base_dir = os.path.join(src_root, "absl") if f == "AbseilDll.cmake" else root
for token in re.findall(r"[\\"\\s]([A-Za-z0-9_./-]+\\.(?:cc|c|h|inc))[\\"\\s)]", text):
candidate = os.path.join(base_dir, token)
if not os.path.exists(candidate):
os.makedirs(os.path.dirname(candidate), exist_ok=True)
open(candidate, "w").close()
os.utime(candidate, (946684800, 946684800))
'
export CC=/usr/local/bin/gcc
export CXX=/usr/local/bin/g++
export CFLAGS="-Werror"
export CXXFLAGS="-Werror"
export CMAKE_GENERATOR=Ninja
export CMAKE_BUILD_PARALLEL_LEVEL="$(nproc)"
cmake -S "${{WORK_ROOT}}/src" -B "${{WORK_ROOT}}/build" \\
-DCMAKE_SUPPRESS_REGENERATION=ON \\
-DABSL_BUILD_TESTING=ON \\
-DABSL_GOOGLETEST_DOWNLOAD_URL="file://${{GTEST_ARCHIVE}}" \\
-DCMAKE_BUILD_TYPE="{build_type}" \\
-DCMAKE_C_FLAGS_DEBUG="-Og" \\
-DCMAKE_CXX_FLAGS_DEBUG="-Og" \\
-DCMAKE_EXE_LINKER_FLAGS_DEBUG="" \\
-DCMAKE_SHARED_LINKER_FLAGS_DEBUG="" \\
-DCMAKE_CXX_STANDARD=17 \\
-DBUILD_SHARED_LIBS={shared} \\
-DABSL_BUILD_MONOLITHIC_SHARED_LIBS={monolithic} \\
-DCMAKE_JOB_POOLS="link=$(nproc)" \\
-DCMAKE_JOB_POOL_LINK=link
# Build GoogleTest/GMock once during configure and prepare rules.ninja/build.ninja
# for isolated per-cc_library object compilation on RBE.
python3 -c '
import os, re, subprocess
build_dir = "/tmp/cmake-work/build"
out = subprocess.check_output(["ninja", "-C", build_dir, "-t", "targets", "all"], text=True)
gtest_targets = [
line.split(":", 1)[0]
for line in out.splitlines()
if ("_STATIC_LIBRARY_LINKER__" in line or "_SHARED_LIBRARY_LINKER__" in line)
and line.startswith("lib/")
]
if gtest_targets:
subprocess.check_call(["ninja", "-C", build_dir] + gtest_targets)
import shutil
for d in ("googletest-build", "googletest-external"):
p = os.path.join(build_dir, d)
if os.path.exists(p):
shutil.rmtree(p)
rules_file = os.path.join(build_dir, "CMakeFiles", "rules.ninja")
if os.path.exists(rules_file):
with open(rules_file) as rf:
rtext = rf.read()
rtext = re.sub(r"^\\s*deps\\s*=\\s*gcc\\s*\\n", "", rtext, flags=re.M)
rtext = re.sub(r"^\\s*depfile\\s*=\\s*\\$DEP_FILE\\s*\\n", "", rtext, flags=re.M)
with open(rules_file, "w") as rf:
rf.write(rtext)
bninja_file = os.path.join(build_dir, "build.ninja")
if os.path.exists(bninja_file):
with open(bninja_file) as bf:
btext = bf.read()
btext = re.sub(
r"(build\\s+\\S+\\.o:\\s+C(?:XX)?_COMPILER__\\S+\\s+[^\\n|]+?)\\s*\\|\\|\\s*cmake_object_order_depends_target_\\S+",
r"\\1",
btext,
)
btext = re.sub(
r"^build build\\.ninja: RERUN_CMAKE\\b",
"build build.ninja: phony",
btext,
flags=re.M,
)
with open(bninja_file, "w") as bf:
bf.write(btext)
for root, dirs, files in os.walk(build_dir):
for f in files:
os.utime(os.path.join(root, f), (1577836800, 1577836800))
'
tar -cf "${{OUT_TAR}}" -C "${{WORK_ROOT}}" src build
rm -rf "${{WORK_ROOT}}"
""".format(
gtest_path = gtest_file.path,
out_tar_path = out_tar.path,
top_level_entries = " ".join(ctx.attr.top_level_entries),
build_type = ctx.attr.build_type,
shared = ctx.attr.shared,
monolithic = ctx.attr.monolithic,
)
ctx.actions.run_shell(
inputs = inputs,
outputs = [out_tar],
command = cmd,
mnemonic = "CMakeConfigure",
progress_message = "Configuring CMake and building GoogleTest for %s (%s)" % (ctx.label.name, ctx.attr.build_type),
)
return [DefaultInfo(files = depset([out_tar]))]
_cmake_configure = rule(
implementation = _cmake_configure_impl,
attrs = {
"build_type": attr.string(mandatory = True),
"googletest_archive": attr.label(
allow_single_file = True,
mandatory = True,
),
"monolithic": attr.string(mandatory = True),
"shared": attr.string(mandatory = True),
"srcs": attr.label_list(
allow_files = True,
mandatory = True,
),
"top_level_entries": attr.string_list(mandatory = True),
},
)
def _cmake_lib_objs_impl(ctx):
out_tar = ctx.actions.declare_file(ctx.label.name + ".tar")
configure_tar = ctx.file.configure_tar
dep_hdrs = [
d[_CmakeHeadersInfo].headers
for d in ctx.attr.deps
if _CmakeHeadersInfo in d
]
all_src_files = depset(
ctx.files.srcs + ctx.files.hdrs + ctx.files.textual_hdrs + ctx.files.pkg_hdrs,
transitive = dep_hdrs,
)
cc_files = [f for f in ctx.files.srcs if f.path.endswith((".cc", ".c"))]
inputs = depset([configure_tar], transitive = [all_src_files])
copy_entries = [(f.path, f.short_path) for f in all_src_files.to_list()]
cc_rel_paths = [f.short_path for f in cc_files]
cmd = """
set -euo pipefail
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${{PATH}}"
CONFIGURE_TAR="$(realpath "{configure_tar_path}")"
export OUT_TAR="$(realpath -m "{out_tar_path}")"
export EXEC_ROOT="${{PWD}}"
WORK_ROOT="/tmp/cmake-work"
exec 9>/tmp/cmake-work.lock
flock 9
rm -rf "${{WORK_ROOT}}"
mkdir -p "${{WORK_ROOT}}"
tar -xf "${{CONFIGURE_TAR}}" -C "${{WORK_ROOT}}"
export CC=/usr/local/bin/gcc
export CXX=/usr/local/bin/g++
export CFLAGS="-Werror"
export CXXFLAGS="-Werror"
export CMAKE_GENERATOR=Ninja
export CMAKE_BUILD_PARALLEL_LEVEL="$(nproc)"
python3 -c '
import os, re, shutil, subprocess
exec_root = os.environ["EXEC_ROOT"]
work_root = "/tmp/cmake-work"
src_root = os.path.join(work_root, "src")
build_dir = os.path.join(work_root, "build")
out_tar = os.environ["OUT_TAR"]
copy_entries = {copy_entries_repr}
cc_rel_paths = {cc_rel_paths_repr}
for src_p, rel_p in copy_entries:
full_src = os.path.join(exec_root, src_p)
full_dst = os.path.join(src_root, rel_p)
if os.path.exists(full_src):
os.makedirs(os.path.dirname(full_dst), exist_ok=True)
shutil.copyfile(full_src, full_dst)
os.utime(full_dst, None)
all_targets_out = subprocess.check_output(
["ninja", "-C", build_dir, "-t", "targets", "all"], text=True
)
lib_cmake_names = set()
for line in all_targets_out.splitlines():
if "_STATIC_LIBRARY_LINKER__" in line or "_SHARED_LIBRARY_LINKER__" in line:
m = re.search(r"_(?:STATIC|SHARED)_LIBRARY_LINKER__(.+?)_(?:Release|Debug|RelWithDebInfo|MinSizeRel)\\b", line)
if m:
lib_cmake_names.add(m.group(1))
obj_targets = set()
for rel_cc in cc_rel_paths:
dst_path = os.path.join(src_root, rel_cc)
p = subprocess.run(
["ninja", "-C", build_dir, "-t", "query", dst_path],
capture_output=True, text=True
)
if p.returncode == 0:
for m in re.finditer(r"^\\s*(\\S*CMakeFiles/([^/]+)\\.dir/\\S+\\.o)\\s*$", p.stdout, re.M):
if m.group(2) in lib_cmake_names:
obj_targets.add(m.group(1))
sorted_objs = sorted(obj_targets)
if sorted_objs:
subprocess.check_call(["ninja", "-C", build_dir] + sorted_objs)
for o in sorted_objs:
os.utime(os.path.join(build_dir, o), (1577836800, 1577836800))
log_part_path = os.path.join(build_dir, ".ninja_log.part")
nlog_path = os.path.join(build_dir, ".ninja_log")
part_lines = []
if sorted_objs and os.path.exists(nlog_path):
obj_set = set(sorted_objs)
with open(nlog_path) as nf:
for line in nf:
if line.startswith("#"):
continue
parts = line.rstrip("\\n").split("\\t")
if len(parts) >= 5 and parts[3] in obj_set:
parts[2] = "1577836800000000000"
part_lines.append("\\t".join(parts) + "\\n")
with open(log_part_path, "w") as pf:
pf.writelines(part_lines)
subprocess.check_call(["tar", "-cf", out_tar, "-C", build_dir, ".ninja_log.part"] + sorted_objs)
'
rm -rf "${{WORK_ROOT}}"
""".format(
configure_tar_path = configure_tar.path,
out_tar_path = out_tar.path,
copy_entries_repr = repr(copy_entries),
cc_rel_paths_repr = repr(cc_rel_paths),
)
ctx.actions.run_shell(
inputs = inputs,
outputs = [out_tar],
command = cmd,
mnemonic = "CMakeLibObjs",
progress_message = "Compiling CMake library objects for %s" % ctx.label,
)
return [DefaultInfo(files = depset([out_tar]))]
_cmake_lib_objs = rule(
implementation = _cmake_lib_objs_impl,
attrs = {
"configure_tar": attr.label(
allow_single_file = True,
mandatory = True,
),
"deps": attr.label_list(
aspects = [_cmake_headers_aspect],
),
"hdrs": attr.label_list(allow_files = True),
"pkg_hdrs": attr.label_list(allow_files = True),
"srcs": attr.label_list(allow_files = True),
"textual_hdrs": attr.label_list(allow_files = True),
},
)
def _cmake_libs_link_impl(ctx):
out_tar = ctx.actions.declare_file(ctx.label.name + ".tar")
configure_tar = ctx.file.configure_tar
obj_tars = ctx.files.obj_tars
inputs = depset([configure_tar] + obj_tars)
cmd = """
set -euo pipefail
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${{PATH}}"
CONFIGURE_TAR="$(realpath "{configure_tar_path}")"
OUT_TAR="$(realpath -m "{out_tar_path}")"
export EXEC_ROOT="${{PWD}}"
WORK_ROOT="/tmp/cmake-work"
exec 9>/tmp/cmake-work.lock
flock 9
rm -rf "${{WORK_ROOT}}"
mkdir -p "${{WORK_ROOT}}"
tar -xf "${{CONFIGURE_TAR}}" -C "${{WORK_ROOT}}"
export CC=/usr/local/bin/gcc
export CXX=/usr/local/bin/g++
export CFLAGS="-Werror"
export CXXFLAGS="-Werror"
export CMAKE_GENERATOR=Ninja
export CMAKE_BUILD_PARALLEL_LEVEL="$(nproc)"
python3 -c '
import os, re, subprocess
exec_root = os.environ["EXEC_ROOT"]
work_root = "/tmp/cmake-work"
build_dir = os.path.join(work_root, "build")
nlog_path = os.path.join(build_dir, ".ninja_log")
part_path = os.path.join(build_dir, ".ninja_log.part")
obj_tars = {obj_tars_repr}
for rel_tar in obj_tars:
full_tar = os.path.join(exec_root, rel_tar)
subprocess.check_call(["tar", "-xf", full_tar, "-C", build_dir])
if os.path.exists(part_path):
with open(part_path) as pf:
pdata = pf.read()
if pdata:
with open(nlog_path, "a") as nf:
nf.write(pdata)
os.remove(part_path)
for root, dirs, files in os.walk(build_dir):
for f in files:
if f.endswith(".o"):
os.utime(os.path.join(root, f), (1577836800, 1577836800))
bninja_file = os.path.join(build_dir, "build.ninja")
if os.path.exists(bninja_file):
with open(bninja_file) as bf:
btext = bf.read()
btext = re.sub(
r"^build lib/lib\\S+:\\s+(?:CXX_STATIC|CXX_SHARED)_LIBRARY_LINKER__\\S+\\s+[^\\n]+(?:\\n\\s+[^\\n]+)*\\n",
"",
btext,
flags=re.M,
)
with open(bninja_file, "w") as bf:
bf.write(btext)
out = subprocess.check_output(["ninja", "-C", build_dir, "-t", "targets", "all"], text=True)
lib_targets = [
line.split(":", 1)[0]
for line in out.splitlines()
if "_STATIC_LIBRARY_LINKER__" in line or "_SHARED_LIBRARY_LINKER__" in line
]
if lib_targets:
subprocess.check_call(["ninja", "-C", build_dir] + lib_targets)
if os.path.exists(nlog_path):
lines = []
with open(nlog_path) as nf:
for line in nf:
if line.startswith("#"):
lines.append(line)
continue
parts = line.rstrip("\\n").split("\\t")
if len(parts) >= 5:
parts[2] = "1577836800000000000"
lines.append("\\t".join(parts) + "\\n")
with open(nlog_path, "w") as nf:
nf.writelines(lines)
for root, dirs, files in os.walk(build_dir):
for f in files:
os.utime(os.path.join(root, f), (1577836800, 1577836800))
'
tar -cf "${{OUT_TAR}}" -C "${{WORK_ROOT}}" src build
rm -rf "${{WORK_ROOT}}"
""".format(
configure_tar_path = configure_tar.path,
out_tar_path = out_tar.path,
obj_tars_repr = repr([f.path for f in obj_tars]),
)
ctx.actions.run_shell(
inputs = inputs,
outputs = [out_tar],
command = cmd,
mnemonic = "CMakeLibsLink",
progress_message = "Linking CMake libraries for %s" % ctx.label.name,
)
return [DefaultInfo(files = depset([out_tar]))]
_cmake_libs_link = rule(
implementation = _cmake_libs_link_impl,
attrs = {
"configure_tar": attr.label(
allow_single_file = True,
mandatory = True,
),
"obj_tars": attr.label_list(
allow_files = True,
mandatory = True,
),
},
)
def _cmake_test_action_impl(ctx):
out_log = ctx.actions.declare_file(ctx.label.name + ".log")
libs_file = ctx.file.libs
dep_hdrs = [
d[_CmakeHeadersInfo].headers
for d in ctx.attr.deps
if _CmakeHeadersInfo in d
]
hdr_files = depset(ctx.files.pkg_hdrs, transitive = dep_hdrs)
src_files = ctx.files.srcs
data_files = ctx.files.data
inputs = depset([libs_file] + src_files + data_files, transitive = [hdr_files])
hdr_entries = [(f.path, f.short_path) for f in hdr_files.to_list()]
src_entries = [(f.path, f.short_path) for f in src_files]
cmd = """
set -euo pipefail
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${{PATH}}"
export EXEC_ROOT="${{PWD}}"
LIBS_TAR="$(realpath "{libs_path}")"
OUT_LOG="$(realpath -m "{out_log_path}")"
WORK_ROOT="/tmp/cmake-work"
exec 9>/tmp/cmake-work.lock
flock 9
rm -rf "${{WORK_ROOT}}"
mkdir -p "${{WORK_ROOT}}"
tar -xf "${{LIBS_TAR}}" -C "${{WORK_ROOT}}"
export CC=/usr/local/bin/gcc
export CXX=/usr/local/bin/g++
export CFLAGS="-Werror"
export CXXFLAGS="-Werror"
export CMAKE_GENERATOR=Ninja
export CMAKE_BUILD_PARALLEL_LEVEL="$(nproc)"
export CTEST_PARALLEL_LEVEL="$(nproc)"
if [ -d "${{EXEC_ROOT}}/absl/time/internal/cctz/testdata/zoneinfo" ]; then
export TZDIR="${{EXEC_ROOT}}/absl/time/internal/cctz/testdata/zoneinfo"
else
export TZDIR="${{WORK_ROOT}}/src/absl/time/internal/cctz/testdata/zoneinfo"
fi
TARGETS="$(python3 -c '
import os, re, shutil, subprocess
work_root = "/tmp/cmake-work"
exec_root = os.environ["EXEC_ROOT"]
src_root = os.path.join(work_root, "src")
build_dir = os.path.join(work_root, "build")
hdr_entries = {hdr_entries_repr}
src_entries = {src_entries_repr}
for src_p, rel_p in hdr_entries:
full_src = os.path.join(exec_root, src_p)
full_dst = os.path.join(src_root, rel_p)
if os.path.exists(full_src):
os.makedirs(os.path.dirname(full_dst), exist_ok=True)
shutil.copyfile(full_src, full_dst)
os.utime(full_dst, (946684800, 946684800))
all_targets_out = subprocess.check_output(
["ninja", "-C", build_dir, "-t", "targets", "all"], text=True
)
exe_link_targets = set()
for line in all_targets_out.splitlines():
if "_EXECUTABLE_LINKER__" in line:
m = re.search(r"_EXECUTABLE_LINKER__(.+?)_(?:Release|Debug|RelWithDebInfo|MinSizeRel)\\b", line)
if m:
exe_link_targets.add(m.group(1))
exe_link_targets.add(os.path.basename(line.split(":", 1)[0]))
cmake_targets = set()
for src_p, rel_src in src_entries:
full_src = os.path.join(exec_root, src_p)
dst_path = os.path.join(src_root, rel_src)
if os.path.exists(full_src):
os.makedirs(os.path.dirname(dst_path), exist_ok=True)
shutil.copyfile(full_src, dst_path)
os.utime(dst_path, (946684800, 946684800))
if rel_src.endswith(".cc") or rel_src.endswith(".c"):
p = subprocess.run(
["ninja", "-C", build_dir, "-t", "query", dst_path],
capture_output=True, text=True
)
if p.returncode == 0:
for m in re.finditer(r"CMakeFiles/([^/]+)\\.dir/", p.stdout):
tname = m.group(1)
if tname in exe_link_targets:
cmake_targets.add(tname)
print(" ".join(sorted(cmake_targets)))
')"
if [ -z "${{TARGETS}}" ]; then
echo "SKIPPED {test_label} (not part of any CMake test executable)" > "${{OUT_LOG}}"
rm -rf "${{WORK_ROOT}}"
exit 0
fi
{{
echo "=== {test_label} (${{TARGETS}}) ==="
cmake --build "${{WORK_ROOT}}/build" --target ${{TARGETS}}
CTEST_REGEX="^($(echo "${{TARGETS}}" | tr " " "|"))(_shard_[0-9]+)?$"
ctest --test-dir "${{WORK_ROOT}}/build" -R "${{CTEST_REGEX}}" --output-on-failure --repeat until-pass:3
}} > "${{OUT_LOG}}" 2>&1 || {{
cat "${{OUT_LOG}}" >&2
rm -rf "${{WORK_ROOT}}"
exit 1
}}
rm -rf "${{WORK_ROOT}}"
""".format(
libs_path = libs_file.path,
out_log_path = out_log.path,
hdr_entries_repr = repr(hdr_entries),
src_entries_repr = repr(src_entries),
test_label = str(ctx.label),
)
ctx.actions.run_shell(
inputs = inputs,
outputs = [out_log],
command = cmd,
mnemonic = "CMakeTest",
progress_message = "Building and running CMake test %s" % ctx.label,
)
return [DefaultInfo(files = depset([out_log]))]
_cmake_test_action = rule(
implementation = _cmake_test_action_impl,
attrs = {
"data": attr.label_list(allow_files = True),
"deps": attr.label_list(
aspects = [_cmake_headers_aspect],
),
"libs": attr.label(
allow_single_file = True,
mandatory = True,
),
"pkg_hdrs": attr.label_list(allow_files = True),
"srcs": attr.label_list(
allow_files = True,
mandatory = True,
),
},
)
def cmake_package(modes = [m[0] for m in DEFAULT_CMAKE_MODES]):
"""Inspects existing rules in the package to create per-library object actions and per-test actions."""
lib_srcs = {}
lib_rules_by_name = {}
all_cc_lib_labels = []
seen_lib_cc_sets = {}
objs_by_mode = {mode: [] for mode in modes}
tests_by_mode = {mode: [] for mode in modes}
test_groups = []
seen_test_src_sets = {}
claimed_cc_srcs = {}
non_lib_cc_srcs = {}
used_test_names = {}
native.filegroup(
name = "cmake_configure_srcs",
srcs = native.glob(["**/CMakeLists.txt", "**/*.cmake"], allow_empty = True),
visibility = ["//:__pkg__"],
)
native.filegroup(
name = "cmake_pkg_hdrs",
srcs = native.glob(["**/*.h", "**/*.inc", "**/*.hpp", "**/*.def"], allow_empty = True),
visibility = ["//visibility:private"],
)
for r in native.existing_rules().values():
if r.get("kind") == "cc_library":
lib_rules_by_name[":" + r["name"]] = r
all_cc_lib_labels.append(":" + r["name"])
srcs = r.get("srcs") or ()
hdrs = r.get("hdrs") or ()
for f in (srcs if type(srcs) == "tuple" else ()) + (hdrs if type(hdrs) == "tuple" else ()):
if type(f) == "string" and f.startswith(":"):
lib_srcs[f[1:]] = True
should_add_objs = False
if type(srcs) == "tuple":
cc_key = tuple(sorted([f for f in srcs if type(f) == "string" and (f.endswith(".cc") or f.endswith(".c"))]))
if cc_key and cc_key not in seen_lib_cc_sets:
seen_lib_cc_sets[cc_key] = True
should_add_objs = True
elif srcs:
should_add_objs = True
if should_add_objs:
for mode in modes:
obj_name = "cmake_objs_" + mode + "_" + r["name"]
_cmake_lib_objs(
name = obj_name,
testonly = True,
configure_tar = "//:cmake_configure_" + mode,
srcs = list(srcs) if type(srcs) == "tuple" else srcs,
hdrs = list(hdrs) if type(hdrs) == "tuple" else hdrs,
textual_hdrs = list(r.get("textual_hdrs")) if type(r.get("textual_hdrs")) == "tuple" else r.get("textual_hdrs"),
pkg_hdrs = [":cmake_pkg_hdrs"],
deps = list(r.get("deps")) if type(r.get("deps")) == "tuple" else r.get("deps"),
tags = ["cmake", "manual"],
)
objs_by_mode[mode].append(":" + obj_name)
for mode in modes:
native.filegroup(
name = "cmake_objs_" + mode,
testonly = True,
srcs = objs_by_mode[mode],
visibility = ["//:__pkg__"],
)
for r in native.existing_rules().values():
kind = r.get("kind")
if kind in ("cc_test", "cc_binary"):
srcs = r.get("srcs") or ()
if type(srcs) == "tuple":
for f in srcs:
if type(f) == "string" and f.startswith(":") and (f.endswith(".cc") or f.endswith(".c")):
rel = f[1:]
if rel not in lib_srcs:
non_lib_cc_srcs[rel] = True
if kind == "cc_test":
tags = r.get("tags") or ()
if "benchmark" in tags or r["name"].endswith("_benchmark"):
continue
srcs = r.get("srcs") or ()
deps = r.get("deps")
if type(srcs) == "tuple" and not srcs and type(deps) == "tuple":
# Handle cc_test targets whose .cc files live in a same-package testonly cc_library.
inferred = []
for d in deps:
if d in lib_rules_by_name and lib_rules_by_name[d].get("testonly"):
d_srcs = lib_rules_by_name[d].get("srcs") or ()
if type(d_srcs) == "tuple":
inferred.extend(list(d_srcs))
srcs = tuple(inferred)
if type(srcs) == "tuple" and srcs:
cc_key = tuple(sorted([f for f in srcs if type(f) == "string" and (f.endswith(".cc") or f.endswith(".c"))]))
if cc_key and cc_key not in seen_test_src_sets:
seen_test_src_sets[cc_key] = True
for f in cc_key:
if f.startswith(":"):
claimed_cc_srcs[f[1:]] = True
data = r.get("data") or ()
used_test_names[r["name"]] = True
test_groups.append(struct(
name = r["name"],
srcs = list(srcs),
deps = list(deps) if type(deps) == "tuple" else deps,
data = list(data) if type(data) == "tuple" else data,
))
for f in native.glob(["**/*_test.cc", "**/*_test.c"], allow_empty = True):
if f not in claimed_cc_srcs:
if f not in lib_srcs:
non_lib_cc_srcs[f] = True
base = f[:-3] if f.endswith(".cc") else f[:-2]
tname = base.replace("/", "_")
if tname in used_test_names:
tname = tname + "_file"
used_test_names[tname] = True
test_groups.append(struct(
name = tname,
srcs = [":" + f],
deps = all_cc_lib_labels,
data = [],
))
native.filegroup(
name = "cmake_lib_srcs",
srcs = native.glob(
["**"],
exclude = [
"BUILD",
"BUILD.bazel",
"**/*_benchmark.cc",
"**/benchmarks.cc",
] + non_lib_cc_srcs.keys(),
allow_empty = True,
),
visibility = ["//:__pkg__"],
)
for g in test_groups:
for mode in modes:
target_name = "cmake_" + mode + "_" + g.name
_cmake_test_action(
name = target_name,
testonly = True,
libs = "//:cmake_libs_" + mode,
srcs = g.srcs,
pkg_hdrs = [":cmake_pkg_hdrs"],
deps = g.deps,
data = g.data,
tags = ["cmake", "manual"],
)
tests_by_mode[mode].append(":" + target_name)
for mode in modes:
native.filegroup(
name = "cmake_tests_" + mode,
testonly = True,
srcs = tests_by_mode[mode],
visibility = ["//:__pkg__"],
)
def define_cmake_tests(
packages,
root_srcs,
top_level_entries = ["CMakeLists.txt", "CMake", "MODULE.bazel", "absl", "create_lts.py"],
modes = DEFAULT_CMAKE_MODES):
"""Defines Stage 0 CMake configure, Stage 1 library link, and bundled top-level CMake test targets."""
native.config_setting(
name = "cmake_opt_mode",
values = {"compilation_mode": "opt"},
)
native.filegroup(
name = "cmake_configure_srcs",
srcs = root_srcs + [pkg + ":cmake_configure_srcs" for pkg in packages],
)
native.filegroup(
name = "cmake_lib_srcs",
srcs = root_srcs + [pkg + ":cmake_lib_srcs" for pkg in packages],
)
top_level_tests = []
for mode, shared, monolithic in modes:
_cmake_configure(
name = "cmake_configure_" + mode,
testonly = True,
build_type = select({
":cmake_opt_mode": "Release",
"//conditions:default": "Debug",
}),
googletest_archive = "@googletest_archive//" + "file",
monolithic = monolithic,
shared = shared,
srcs = [":cmake_configure_srcs"],
top_level_entries = top_level_entries,
tags = [
"cmake",
"manual",
],
)
_cmake_libs_link(
name = "cmake_libs_" + mode,
testonly = True,
configure_tar = ":cmake_configure_" + mode,
obj_tars = [pkg + ":cmake_objs_" + mode for pkg in packages],
tags = [
"cmake",
"manual",
],
)
test_name = "cmake_" + mode + "_test"
top_level_tests.append(":" + test_name)
_cmake_sh_test(
name = test_name,
size = "small",
content = """#!/bin/bash
set -euo pipefail
LOGS=(__CMAKE_LOG_FILES__)
echo "Verified ${{#LOGS[@]}} CMake ({mode}) test action logs."
""".format(mode = mode),
data = [pkg + ":cmake_tests_" + mode for pkg in packages],
tags = [
"cmake",
"manual",
],
)
for mode, link_type in [("static", "STATIC"), ("shared", "DYNAMIC")]:
install_name = "cmake_install_" + mode + "_test"
top_level_tests.append(":" + install_name)
_cmake_sh_test(
name = install_name,
size = "large",
args = ["$(rootpath @googletest_archive//" + "file)"],
content = """#!/bin/bash
set -euo pipefail
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${{PATH}}"
export LINK_TYPE="{link_type}"
export ABSL_INSTALL_TEST_BUILD_TESTING="OFF"
export ABSL_GOOGLETEST_VERSION="1.18.0"
export ABSL_GOOGLETEST_DOWNLOAD_URL="file://$(realpath "$1")"
export CC=/usr/local/bin/gcc
export CXX=/usr/local/bin/g++
export CMAKE_GENERATOR=Ninja
export CMAKE_BUILD_PARALLEL_LEVEL="$(nproc)"
export CTEST_PARALLEL_LEVEL="$(nproc)"
WORKDIR="$(mktemp -d "${{TEST_TMPDIR}}/absl_install_test.XXXXXX")"
cp -RL {top_level_entries} "${{WORKDIR}}/"
chmod -R u+w "${{WORKDIR}}"
export TZDIR="${{WORKDIR}}/absl/time/internal/cctz/testdata/zoneinfo"
export HOME="${{TEST_TMPDIR}}"
rm -rf /buildfs /googletest_builddir
cd "${{WORKDIR}}"
exec ./CMake/install_test_project/test.sh
""".format(
link_type = link_type,
top_level_entries = " ".join(top_level_entries),
),
data = [
":cmake_lib_srcs",
"@googletest_archive//" + "file",
],
tags = [
"cmake",
"manual",
],
target_compatible_with = select({
":cmake_opt_mode": [],
"//conditions:default": ["@platforms//:incompatible"],
}),
)
native.test_suite(
name = "cmake_tests",
tags = [
"cmake",
"manual",
],
tests = top_level_tests,
)