diff --git a/swig.bzl b/swig.bzl new file mode 100644 index 000000000..b28d366dd --- /dev/null +++ b/swig.bzl @@ -0,0 +1,245 @@ +"""Bazel rules for generating SWIG language bindings. + +This module provides Bazel rules for generating language bindings +using SWIG. + +Example usage: + load("@swig//:swig.bzl", "swig_library") + + swig_library( + name = "my_python_wrapper", + srcs = [ + "my_interface.i", + "additional.i", + "header.h", + ], + main = "my_interface.i", + language = "python", + module = "my_module", + ) +""" + +def _swig_library_impl(ctx): + """Implementation function for swig_library rule. + + Args: + ctx: Rule context + + Returns: + DefaultInfo provider with generated wrapper and module files + """ + + # Map language to the corresponding SWIG library attribute and extension + # Add more languages as lib_ filegroups are added to @swig//:BUILD + _LANGUAGE_TO_LIB_ATTR = { + "python": ctx.files._swig_lib_python, + "java": ctx.files._swig_lib_java, + } + _LANGUAGE_MODULE_EXT = { + "python": ".py", + "java": ".java", + } + + if (ctx.attr.language not in _LANGUAGE_TO_LIB_ATTR or ctx.attr.language not in _LANGUAGE_MODULE_EXT): + fail("Language '%s' is not supported. Supported languages: %s. " % + (ctx.attr.language, ", ".join(sorted(_LANGUAGE_TO_LIB_ATTR.keys()))) + + "To add support for a new language, ensure @swig//:BUILD has a " + + "'lib_%s' filegroup and add the mapping in swig.bzl." % ctx.attr.language) + + swig_lib_files = _LANGUAGE_TO_LIB_ATTR[ctx.attr.language] + swig_lib_dir = ctx.file._swig_swg.dirname + + interface_file = None + if ctx.attr.main: + for src in ctx.files.srcs: + if src.basename == ctx.attr.main or src.path.endswith("/" + ctx.attr.main): + interface_file = src + break + if not interface_file: + fail("Main interface file '%s' not found in srcs" % ctx.attr.main) + else: + # Default: use the first .i file in srcs + for src in ctx.files.srcs: + if src.extension == "i": + interface_file = src + break + if not interface_file: + fail("No .i interface file found in srcs") + + module = ctx.attr.module if ctx.attr.module else ctx.label.name + + if ctx.attr.out: + wrapper_out = ctx.actions.declare_file(ctx.attr.out) + else: + # Default: {rule_name}_wrap.{c|cpp} + wrapper_ext = ".c" if ctx.attr.out_c_mode else ".cpp" + wrapper_out = ctx.actions.declare_file(ctx.label.name + "_wrap" + wrapper_ext) + + module_ext = _LANGUAGE_MODULE_EXT[ctx.attr.language] + if ctx.attr.outdir: + module_out = ctx.actions.declare_file(ctx.attr.outdir + "/" + module + module_ext) + else: + # Default: same directory as the rule + module_out = ctx.actions.declare_file(module + module_ext) + + include_dirs = {} + for src in ctx.files.srcs: + include_dirs[src.dirname] = True + for include in ctx.attr.includes: + include_dirs[include] = True + include_dir_list = include_dirs.keys() + + swig_args = ctx.actions.args() + swig_args.add("-" + ctx.attr.language) + if not ctx.attr.out_c_mode: + swig_args.add("-c++") + + swig_args.add_all(ctx.attr.defines, format_each = "-D%s") + + swig_args.add_all(include_dir_list, format_each = "-I%s") + + swig_args.add_all(ctx.attr.opts) + + swig_args.add("-module", module) + swig_args.add("-outdir", module_out.dirname) + swig_args.add("-o", wrapper_out) + + swig_args.add(interface_file) + + ctx.actions.run( + executable = ctx.executable._swig, + arguments = [swig_args], + inputs = depset(swig_lib_files + ctx.files.srcs), + outputs = [wrapper_out, module_out], + env = {"SWIG_LIB": swig_lib_dir}, + mnemonic = "SwigGenerate", + progress_message = "Generating SWIG %s bindings for %s" % (ctx.attr.language, ctx.label.name), + ) + + return [DefaultInfo(files = depset([wrapper_out, module_out]))] + + +swig_library = rule( + implementation = _swig_library_impl, + attrs = { + "srcs": attr.label_list( + mandatory = True, + allow_files = True, + doc = "SWIG interface files (.i) and headers. All files are available for " + + "%include directives. The main interface file is specified by 'main' " + + "attribute, or defaults to the first .i file.", + ), + "main": attr.string( + doc = "Main SWIG interface file (optional). If not specified, the first .i " + + "file in srcs is used. Only the main file is passed to SWIG; other " + + "files should be %include'd from the main file.", + ), + "language": attr.string( + mandatory = True, + doc = "Target language for binding generation.", + ), + "module": attr.string( + doc = "SWIG module name. Defaults to the rule name if not specified.", + ), + "out": attr.string( + doc = "Explicit name for the wrapper output file. If not specified, " + + "defaults to '{rule_name}_wrap.{c|cpp}'.", + ), + "outdir": attr.string( + doc = "Output directory for language-specific module files (e.g., .py, .java). " + + "If specified, the module file will be placed in this subdirectory. " + + "If not specified, module files are placed in the rule's directory.", + ), + "out_c_mode": attr.bool( + default = False, + doc = "If True, generate C wrapper instead of C++ (default: False).", + ), + "defines": attr.string_list( + default = [], + doc = "Preprocessor defines to pass to SWIG using -D flags.", + ), + "includes": attr.string_list( + default = [], + doc = "Additional include paths to pass to SWIG using -I flags. " + + "Source file directories are automatically included.", + ), + "opts": attr.string_list( + default = [], + doc = "Additional SWIG command-line options.", + ), + "_swig": attr.label( + default = "@swig//:swig", + executable = True, + cfg = "exec", + doc = "The SWIG executable (internal attribute).", + ), + "_swig_swg": attr.label( + default = "@swig//:swig_swg", + allow_single_file = True, + doc = "SWIG swig.swg library file used for determining SWIG_LIB " + + "env variable (internal attribute).", + ), + "_swig_lib_python": attr.label( + default = "@swig//:lib_python", + allow_files = True, + doc = "SWIG library files for Python (internal attribute).", + ), + "_swig_lib_java": attr.label( + default = "@swig//:lib_java", + allow_files = True, + doc = "SWIG library files for Java (internal attribute).", + ), + }, + doc = """Generate SWIG wrapper code for language bindings. + +This rule simplifies SWIG binding generation. + +The rule automatically selects the appropriate SWIG library files based on the +'language' attribute. To add support for additional languages, ensure the +@swig repository has a corresponding 'lib_' filegroup in its BUILD file, +then add the language mapping in the _LANGUAGE_TO_LIB_ATTR dictionary. + +Attributes: + srcs: SWIG interface files (.i) and headers + language: Target language (python, java, etc.) + module: Module name (optional, defaults to rule name) + out: Wrapper output filename (optional, defaults to {name}_wrap.{c|cpp}) + outdir: Module file output directory (optional, defaults to rule directory) + out_c_mode: Generate C wrapper instead of C++ (optional, default: False) + defines: Preprocessor defines (optional) + includes: Additional include paths (optional) + opts: Additional SWIG options (optional) + +Example (Python - basic): + swig_library( + name = "my_python_wrapper", + srcs = ["interface.i", "header.h"], + language = "python", + module = "my_module", + defines = ["SWIGPYTHON"], + ) + # Generates: my_python_wrapper_wrap.cpp, my_module.py + +Example (Python - custom output): + swig_library( + name = "nlopt_swig", + srcs = ["nlopt.i", "nlopt.h"], + language = "python", + module = "nlopt", + out = "nlopt_wrap.cpp", + outdir = "generated", + defines = ["SWIGPYTHON"], + ) + # Generates: nlopt_wrap.cpp, generated/nlopt.py + +Example (Java): + swig_library( + name = "my_java_wrapper", + srcs = ["interface.i", "header.h"], + language = "java", + module = "MyModule", + defines = ["SWIGJAVA"], + ) + # Generates: my_java_wrapper_wrap.cpp, MyModule.java +""", +)