blob: d836613ccd1d604a73565540f329d1b59e838c92 [file] [log] [blame]
"""Provide bazel rules for OSS CLIF."""
# Label for our OSS CLIF binary pyclif.
CLIF_PYCLIF = "@clif//:pyclif"
# Label for our OSS CLIF protobuf compiler.
CLIF_PROTO = "@clif//:proto"
# Label for our OSS CLIF C++ runtime headers and sources.
CLIF_CPP_RUNTIME = "@clif//:cpp_runtime"
# Additional CC compilation flags, if any.
EXTRA_CC_FLAGS = []
_PROTO_LIBRARY_SUFFIX = "_pyclif"
PYCLIF_PYEXT_SUFFIX = ".so"
PYCLIF_CC_LIB_SUFFIX = "_cclib"
PYCLIF_WRAP_SUFFIX = "_clif_wrap"
def _clif_wrap_cc_impl(ctx):
"""Executes CLIF cmdline tool to produce C++ python model from a CLIF spec."""
if len(ctx.files.srcs) != 1:
fail("Exactly one CLIF source file label must be specified.", "srcs")
clif_spec_file = ctx.files.srcs[0]
# Inputs is a set of all of the things we depend on, not inputs to the CLIF
# program itself.
inputs = depset([clif_spec_file])
for dep in ctx.attr.deps:
inputs += dep.cc.transitive_headers
inputs += ctx.files._cliflib
inputs += ctx.files.clif_deps
inputs += ctx.files.toolchain_deps
# Compute the set of include directories for CLIF so it can find header files
# used in the CLIF specification. These are the repo roots for all of our
# inputs (aka deps) plus all of the quote and system includes for our C++
# deps.
include_dirs = depset(_get_repository_roots(ctx, inputs))
for dep in ctx.attr.deps:
include_dirs += dep.cc.quote_include_directories
include_dirs += dep.cc.system_include_directories
# Construct our arguments for CLIF.
args = [
"--py3output",
"--modname",
ctx.attr.package_name + "." + ctx.attr.module_name,
"-c",
ctx.outputs.cc_out.path,
"-g",
ctx.outputs.h_out.path,
"-i",
ctx.outputs.ccinit_out.path,
"--prepend",
"clif/python/types.h",
]
include_args = ["-I" + i for i in include_dirs.to_list()]
# Add these includes to CLIF itself.
args += include_args
# Add these includes to those passed through by CLIF to its C++ matcher.
args += ["-f" + " ".join(include_args + EXTRA_CC_FLAGS)]
# The last argument is the actual CLIF specification file.
args += [clif_spec_file.path]
outputs = [ctx.outputs.cc_out, ctx.outputs.h_out, ctx.outputs.ccinit_out]
ctx.actions.run(
executable = ctx.executable._clif,
arguments = args,
inputs = inputs.to_list(),
outputs = outputs,
mnemonic = "CLIF",
progress_message = "CLIF wrapping " + clif_spec_file.path,
)
_clif_wrap_cc = rule(
attrs = {
"srcs": attr.label_list(
mandatory = True,
allow_files = True,
),
"deps": attr.label_list(
allow_files = True,
providers = ["cc"],
),
"toolchain_deps": attr.label_list(
allow_files = True,
),
# For rule "//foo/python:bar_clif" this should be "bar".
"module_name": attr.string(mandatory = True),
# For rule "//foo/python:bar_clif" this should be "foo/python".
"package_name": attr.string(mandatory = True),
"clif_deps": attr.label_list(allow_files = True),
# Hidden attribute: the Label for our PYCLIF binary itself.
"_clif": attr.label(
default = Label(CLIF_PYCLIF),
executable = True,
cfg = "host",
),
# Hidden attribute: The label to the C++ CLIF header files.
"_cliflib": attr.label(
default = Label(CLIF_CPP_RUNTIME),
allow_files = True,
),
},
output_to_genfiles = True,
outputs = {
"cc_out": "%{module_name}.cc",
"h_out": "%{module_name}.h",
"ccinit_out": "%{module_name}_init.cc",
},
implementation = _clif_wrap_cc_impl,
)
def _get_repository_roots(ctx, files):
"""Returns abnormal root directories under which files reside.
When running a ctx.action, source files within the main repository are all
relative to the current directory; however, files that are generated or exist
in remote repositories will have their root directory be a subdirectory,
e.g. bazel-out/local-fastbuild/genfiles/external/jpeg_archive. This function
returns the set of these devious directories, ranked and sorted by popularity
in order to hopefully minimize the number of I/O system calls within the
compiler, because includes have quadratic complexity.
Args:
ctx: context
files: list of paths
Returns:
list of directories
"""
ctx = ctx # unused
result = {}
for f in files:
root = f.root.path
if root:
if root not in result:
result[root] = 0
result[root] -= 1
work = f.owner.workspace_root
if work:
if root:
root += "/"
root += work
if root:
if root not in result:
result[root] = 0
result[root] -= 1
return [k for v, k in sorted([(v, k) for k, v in result.items()])]
def _clif_to_lib(label, extension):
"""Gets a C++/python/etc library corresponding to a CLIF library rule.
Args:
label: string. The name of a clif_rule. If the name is of the
form <target>_pyclif we will stripe off the `_pyclif` ending.
extension: string. The expected extension of our name library.
Returns:
<target>_extension.
"""
if label.endswith(_PROTO_LIBRARY_SUFFIX):
basename = label[:-len(_PROTO_LIBRARY_SUFFIX)]
else:
basename = label
return basename + extension
def pyclifs_to_pyproto_libs(labels):
"""Gets the py protobuf label for each of pyclif label as a list."""
return [_clif_to_lib(name, "_py_pb2") for name in labels]
def pyclifs_to_ccproto_libs(labels):
"""Gets the cc protobuf label for each of pyclif label as a list."""
return [_clif_to_lib(name, "_cc_pb2") for name in labels]
def clif_deps_to_cclibs(labels):
"""Gets the cc_library name for each of label as a list."""
return [_clif_to_lib(name, PYCLIF_CC_LIB_SUFFIX) for name in labels]
def _symlink_impl(ctx):
"""Creates a symbolic link between src and out."""
out = ctx.outputs.out
src = ctx.attr.src.files.to_list()[0]
cmd = "ln -f -r -s %s %s" % (src.path, out.path)
ctx.actions.run_shell(
inputs = [src],
outputs = [out],
command = cmd,
)
symlink = rule(
implementation = _symlink_impl,
attrs = {
"src": attr.label(
mandatory = True,
allow_files = True,
single_file = True,
),
"out": attr.output(mandatory = True),
},
)
def py_clif_cc(
name,
srcs,
clif_deps = [],
pyclif_deps = [],
deps = [],
copts = [],
py_deps = [],
**kwargs):
"""Defines a CLIF wrapper rule making C++ libraries accessible to Python.
Here are two example working py_clif_cc rules:
py_clif_cc(
name = "proto_cpp",
srcs = ["proto_cpp.clif"],
pyclif_deps = ["//oss_clif:oss_pyclif"],
deps = ["//oss_clif:proto_cpp_lib"],
)
py_clif_cc(
name = "pyclif_dep",
srcs = ["pyclif_dep.clif"],
deps = ["//oss_clif:pyclif_dep_lib"],
)
Args:
name: The name of the rule. This name becomes a suitable target for Python
libraries to access the C++ code.
srcs: A list that must contain a single file named <name>.clif containing
our CLIF specification.
clif_deps: A list of other CLIF rules included by this one.
pyclif_deps: A potentially empty list of pyclif_proto_library rules
deps: A list of C++ dependencies.
copts: List of copts to provide to our native.cc_library when building our
python extension module.
py_deps: List of dependencies to provide to our the native.py_library
created by this rule.
**kwargs: kwargs passed to py_library rule created by this rule.
"""
pyext_so = name + PYCLIF_PYEXT_SUFFIX
cc_library_name = name + PYCLIF_CC_LIB_SUFFIX
extended_cc_deps = deps + [CLIF_CPP_RUNTIME] + pyclif_deps
# Here's a rough outline of how we build our pyclif library:
#
# Suppose we have a module named 'foo'.
#
# _clif_wrap_cc runs pyclif to produce foo.cc, foo.h, and foo_init.cc which
# C++ python extension module.
#
# native.cc_library is a normal C++ library with those sources, effectively
# our "python module" as a bazel C++ library allowing other rules to depend
# on that C++ code.
#
# native.cc_binary depends on foo's cc_library to create a shared python
# extension module (.so) which python will load via its dlopen mechanism.
# This .so library is also used by the _clif_wrap_cc rule to get include paths
# when building CLIF specs depending on other clif specs.
#
# native.py_library named `name` which provides a python bazel target that
# loads the cc_binary, as data, producing a py extension module. This also
# allows client python code to depend on this module.
# This _clif_wrap_cc handles clif_deps differently than most clif.bzl's
# do. That is because we are doing something quite different than the
# standard clif.bzl does -- we are replacing the generated extension module
# with a symbolic link to protobuf's _message.so. That file, in turn, is
# a dependency of every single Python protocol buffer target. And it needs
# to depend on all of the _cclibs generated by the cc_library rule below.
#
# So because of all this, the normal strategy of having this rule depend
# on clif_deps or clif_deps_to_pyexts(clif_deps) would create circular
# dependencies. Our strategy to avoid those circular dependencies is to
# have anything the cc_library rule depends on be only C++ and not Python.
# The raw clif_deps are a mixture of C++ and Python, so we have to very
# carefully depend on only their C++ part (given by the
# clif_deps_to_cclibs).
_clif_wrap_cc(
name = name + PYCLIF_WRAP_SUFFIX,
srcs = srcs,
deps = extended_cc_deps + clif_deps_to_cclibs(clif_deps),
clif_deps = clif_deps_to_cclibs(clif_deps),
toolchain_deps = ["@bazel_tools//tools/cpp:current_cc_toolchain"],
module_name = name,
# Turns //foo/bar:baz_pyclif into foo.bar to create our fully-qualified
# python package name.
package_name = native.package_name().replace("/", "."),
)
native.cc_library(
name = cc_library_name,
hdrs = [
name + ".h",
],
srcs = [
name + ".cc",
name + "_init.cc",
],
copts = copts + EXTRA_CC_FLAGS,
deps = extended_cc_deps + clif_deps_to_cclibs(clif_deps),
)
# To prevent ODR violations, all of the extensions must live in one
# extension module. And to be compatible with existing protobuf
# generated code, that module must be _message.so.
symlink(
name = name + "_symlink",
out = pyext_so,
src = "@protobuf_archive//:python/google/protobuf/pyext/_message.so",
)
# We create our python module which is just a thin wrapper around our real
# python module pyext_so (producing name.so for python to load). This
# rule allows python code to depend on this module, even through its written
# in C++.
native.py_library(
name = name,
srcs = [],
srcs_version = "PY2AND3",
deps = pyclifs_to_pyproto_libs(pyclif_deps) + clif_deps + py_deps,
data = [pyext_so],
**kwargs
)
# Copied from: devtools/clif/python/clif_build_rule.bzl with heavy
# modifications.
def _clif_proto_parser_rule_impl(ctx):
"""Implementation of _run_clif_proto_parser_rule."""
proto_file = ctx.files.src[0]
args = [
"-c",
ctx.outputs.cc.path,
"-h",
ctx.outputs.hdr.path,
"--strip_dir=%s" % ctx.configuration.genfiles_dir.path,
"--source_dir='.'",
"%s" % proto_file.path,
]
inputs = []
for d in ctx.attr.deps:
if "proto" in dir(d):
inputs += list(d[ProtoInfo].transitive_sources)
ctx.actions.run(
mnemonic = "ClifProtoLibraryGeneration",
arguments = args,
executable = ctx.executable.parser,
inputs = inputs,
outputs = [ctx.outputs.hdr, ctx.outputs.cc],
)
_run_clif_proto_parser_rule = rule(
attrs = {
"src": attr.label(allow_files = [".proto"]),
"hdr": attr.output(),
"cc": attr.output(),
"deps": attr.label_list(),
"parser": attr.label(
executable = True,
default = Label(CLIF_PROTO),
cfg = "host",
),
},
output_to_genfiles = True,
implementation = _clif_proto_parser_rule_impl,
)
def pyclif_proto_library(
name,
proto_lib,
proto_srcfile = "",
deps = [],
visibility = None,
compatible_with = None,
testonly = None):
"""Generate C++ CLIF extension for using a proto and dependent py_proto_lib.
Args:
name: generated cc_library (name.h) to use in py_clif_cc clif_deps
proto_lib: name of a proto_library rule
proto_srcfile: the proto name if it does not match proto_lib rule name
deps: passed to cc_library
visibility: passed to all generated "files": name.h name.a name_pb2.py
compatible_with: compatibility list
testonly: available for test rules only flag (default from package)
"""
if not name.endswith(_PROTO_LIBRARY_SUFFIX):
fail("The name of the 'pyclif_proto_library' target should be of the " +
"form '<PROTO_FILE>%s' where the proto " % _PROTO_LIBRARY_SUFFIX +
"file being wrapped has the name '<PROTO_FILE>.proto'.")
if proto_srcfile:
required_name = proto_srcfile[:-len(".proto")] + _PROTO_LIBRARY_SUFFIX
if name != required_name:
fail("The name of the 'pyclif_proto_library' target should be " +
"'%s' as it is wrapping %s." % (required_name, proto_srcfile))
hdr_file = name + ".h"
cpp_file = name + ".cc"
clifrule = name + "_clif_rule"
src = name[:-len(_PROTO_LIBRARY_SUFFIX)] + ".proto"
_run_clif_proto_parser_rule(
name = clifrule,
src = src,
hdr = hdr_file,
cc = cpp_file,
deps = deps + [proto_lib],
testonly = testonly,
)
# In OSS world, we cannot provide proto_lib as a direct dependency to our
# cc_library as it doesn't provide a cc file:
# in deps attribute of cc_library rule //oss_clif:oss_pyclif: proto_library
# rule '//oss_clif:oss_proto' is misplaced here (expected cc_inc_library,
# cc_library, objc_library, experimental_objc_library or cc_proto_library).
# So we need to synthesize our protobuf cc library name from our name as
# pyclif_name = proto_pyclif
# cc_proto_lib = proto_cc_pb2
native.cc_library(
name = name,
srcs = [cpp_file],
hdrs = [hdr_file],
deps = deps + [CLIF_CPP_RUNTIME] + pyclifs_to_ccproto_libs([name]),
visibility = visibility,
compatible_with = compatible_with,
testonly = testonly,
copts = EXTRA_CC_FLAGS,
)