load("@build_bazel_apple_support//rules:universal_binary.bzl", "universal_binary") load("@rules_cc//cc:action_names.bzl", "CPP_LINK_STATIC_LIBRARY_ACTION_NAME", "OBJ_COPY_ACTION_NAME", "STRIP_ACTION_NAME") load("@rules_cc//cc:cc_shared_library.bzl", "cc_shared_library") load("@rules_cc//cc:defs.bzl", "CcInfo", "cc_library") load("@rules_cc//cc:find_cc_toolchain.bzl", "CC_TOOLCHAIN_ATTRS", "find_cpp_toolchain", "use_cc_toolchain") load("@rules_cc//cc/common:cc_common.bzl", "cc_common") load("@rules_pkg//:mappings.bzl", "pkg_files") load("@rules_pkg//:pkg.bzl", "pkg_zip") load("//shared/bazel/rules/gen:defs.bzl", "gen_versionscript") # Copied from bazel since it isn't exposed publicly that I can find. # https://github.com/bazelbuild/bazel/blob/cc4e3b25a89cd8294406d9489ece706cfcc019bd/src/main/starlark/builtins_bzl/common/cc/cc_helper.bzl#L272 def generate_def_file(ctx, def_parser, object_files, dll_name): def_file = ctx.actions.declare_file(ctx.label.name + ".gen.def") args = ctx.actions.args() args.add(def_file) args.add(dll_name) argv = ctx.actions.args() argv.use_param_file("@%s", use_always = True) argv.set_param_file_format("shell") for object_file in object_files: argv.add(object_file.path) ctx.actions.run( mnemonic = "DefParser", executable = def_parser, toolchain = None, arguments = [args, argv], inputs = object_files, outputs = [def_file], use_default_shell_env = True, ) return def_file def _split_debug_symbols_impl(ctx): """This splits debug symbols out from the main shared library where supported. WPILIB doesn't split them out for osx or Windows, so just pass them through. For some builds, like for the systemcore, we don't want them split. Args: copy: If true, pass the file right on through, though make sure the name is right. use_debug_name: If true, the file should be named 'libfood.so' when done rather than 'libfoo.so'. This only makes sense on Linux. shared_library: The library to split apart. """ label = ctx.attr.shared_library.label target_name = label.name if label.package: target_name = label.package + "/" + label.name folder, lib_name = _folder_prefix(target_name) # For Windows and OSX, we just want to pass it all on through. Don't be clever. if (ctx.target_platform_has_constraint(ctx.attr._darwin_constraint[platform_common.ConstraintValueInfo]) or ctx.target_platform_has_constraint(ctx.attr._windows_constraint[platform_common.ConstraintValueInfo])): files = ctx.attr.shared_library[OutputGroupInfo].main_shared_library_output return [ DefaultInfo(files = files), OutputGroupInfo( default = files, ), ] # Linux debug_suffix = "d" if ctx.attr.use_debug_name else "" if not ctx.target_platform_has_constraint(ctx.attr._linux_constraint[platform_common.ConstraintValueInfo]): fail("Unsupported platform") lib = ctx.actions.declare_file(folder + "/split/lib" + lib_name + debug_suffix + ".so") if ctx.attr.copy: files = ctx.attr.shared_library[OutputGroupInfo].main_shared_library_output.to_list() if len(files) != 1: fail("Wrong number of files", files) ctx.actions.run_shell( command = " ".join([ "cp", files[0].path, lib.path, ]), inputs = depset( direct = files, ), outputs = [lib], ) return [ DefaultInfo(files = depset([lib])), OutputGroupInfo( default = depset([lib]), ), ] else: debug = ctx.actions.declare_file(folder + "/split/lib" + lib_name + debug_suffix + ".so.debug") cc_toolchain = find_cpp_toolchain(ctx) feature_configuration = cc_common.configure_features( ctx = ctx, cc_toolchain = cc_toolchain, requested_features = ctx.features, unsupported_features = ctx.disabled_features, ) objcopy = cc_common.get_tool_for_action( feature_configuration = feature_configuration, action_name = OBJ_COPY_ACTION_NAME, ) strip = cc_common.get_tool_for_action( feature_configuration = feature_configuration, action_name = STRIP_ACTION_NAME, ) # This is the set of commands we want to implement to strip debug symbols out and link them back together: # objcopy --only-keep-debug libmy-library.so libmy-library.so.debug # strip --strip-debug libmy-library.so # objcopy --strip-debug libmy-library.so files = ctx.attr.shared_library[OutputGroupInfo].main_shared_library_output.to_list() if len(files) != 1: fail("Wrong number of files", files) shared_library = files[0] ctx.actions.run_shell( command = " ".join([ "cp", shared_library.path, lib.path, "&& chmod u+w", lib.path, "&&", objcopy, "--only-keep-debug", lib.path, debug.path, "&&", strip, "--strip-debug", lib.path, "&&", objcopy, "--strip-debug", lib.path, ]), inputs = depset( direct = files, transitive = [ cc_toolchain.all_files, ], ), outputs = [lib, debug], ) return [ DefaultInfo(files = depset([lib, debug])), OutputGroupInfo( default = depset([lib, debug]), ), ] _split_debug_symbols = rule( implementation = _split_debug_symbols_impl, attrs = { "copy": attr.bool(mandatory = True), "shared_library": attr.label(mandatory = True), "use_debug_name": attr.bool(mandatory = True), "_darwin_constraint": attr.label(default = "@platforms//os:osx"), "_linux_constraint": attr.label(default = "@platforms//os:linux"), "_windows_constraint": attr.label(default = "@platforms//os:windows"), } | CC_TOOLCHAIN_ATTRS, fragments = ["cpp"], toolchains = use_cc_toolchain(), ) def _folder_prefix(name): if "/" in name: last_slash = name.rfind("/") return (name[0:last_slash], name[last_slash + 1:]) else: return ("", name) def third_party_cc_lib_helper( name, include_root, src_root = None, src_excludes = [], defines = [], visibility = None): """ Helper for src / headers pairs that aren't directly compiled, but rather pulled into a bigger library. Due to allwpilib's directory structure of includes and sources living next to eachother, it often is required to make a header shim to deal with the include path, and a filegroup of the sources. This pattern is extremely common for the thirdparty libraries that live beneath certain libraries. This will produce a library shim with the include path stripped, a filegroup of sources, and packages that can be used to downstream to zip headers / sources with their "parent" library. Params include_root: The package relative path to the header files. This will be used to glob the files and strip the include prefix src_root: Optional. The package relative path to the source files. src_excludes: Optional. Used to exclude files from the src_root glob visibility: The visibility of header shim / source files / package files """ cc_library( name = name + "-headers", hdrs = native.glob([ include_root + "/**", ]), includes = [include_root], defines = defines, strip_include_prefix = include_root, visibility = visibility, ) pkg_files( name = name + "-hdrs-pkg", srcs = native.glob([include_root + "/**"]), strip_prefix = include_root, visibility = visibility, ) if src_root: native.filegroup( name = name + "-srcs", srcs = native.glob([src_root + "/**"], exclude = src_excludes), visibility = visibility, ) pkg_files( name = name + "-srcs-pkg", srcs = native.glob([src_root + "/**"]), strip_prefix = src_root, ) def wpilib_cc_library( name, srcs = [], hdrs = [], deps = [], copts = [], third_party_libraries = [], third_party_header_only_libraries = [], extra_src_pkg_files = [], extra_hdr_pkg_files = [], include_license_files = False, srcs_pkg_root = "src/main/native/cpp", hdrs_pkg_root = "src/main/native/include", strip_include_prefix = None, linkopts = None, visibility = None, **kwargs): """ This function is used to ease the creation of a cc_library with publishing given the standard allwpilib directory structure. This will create a cc_library as well as automatically create header, source, and library artifacts that can be used for publishing. This also provides some syntactic sugar for third party library shims declared by third_party_cc_lib_helper. Important outputs: ":name" - The cc_library name + "-srcs-zip" - A zip file containing all the exported sources name + "-hdrs-zip" - A zip file containing all the exported headers name + "-zip" - A zip file that contains the compiled library Params: srcs: The sources used to compile the library. Note: This may be platform dependent and not include all the sources of the library for packaging hdrs: The headers used to compile the library. Note: This may be platform dependent and not include all the sources of the library for packaging third_party_libraries: These are helper dependencies, created by third_party_cc_lib_helper. Header shims will be added as deps and src filegroups will be added to srcs third_party_header_only_libraries: Similar to third_party_libraries, but for shims that contain no sources extra_src_pkg_files: Extra pkg_files to add to the source bundle. This is useful in the event that a library is complicated and requires extra, customized sources to be added to the published zip file extra_hdr_pkg_files: Extra pkg_files to add to the headers bundle. This is useful in the event that a library is complicated and requires extra, customized headers to be added to the published zip file include_license_files: If the header / source / library zip files should automatically includes the license files. This is used to maintain consistency with the gradle publishing, as not all of them export the license files. """ maybe_license_pkg = ["//:license_pkg_files"] if include_license_files else [] cc_library( name = name + "-headers", hdrs = hdrs, deps = [lib + "-headers" for lib in third_party_libraries + third_party_header_only_libraries], strip_include_prefix = strip_include_prefix, visibility = visibility, **kwargs ) cc_library( name = name, hdrs = hdrs, copts = copts, srcs = srcs + [lib + "-srcs" for lib in third_party_libraries], deps = deps + [lib + "-headers" for lib in third_party_libraries + third_party_header_only_libraries], strip_include_prefix = strip_include_prefix, linkopts = linkopts, visibility = visibility, **kwargs ) if srcs_pkg_root: pkg_files( name = name + "-srcs-pkg", srcs = native.glob([srcs_pkg_root + "/**"]), strip_prefix = srcs_pkg_root, ) pkg_zip( name = name + "-srcs-zip", srcs = maybe_license_pkg + extra_src_pkg_files + [name + "-srcs-pkg"] + [lib + "-srcs-pkg" for lib in third_party_libraries], tags = ["manual"], ) if hdrs_pkg_root: pkg_files( name = name + "-hdrs-pkg", srcs = native.glob([hdrs_pkg_root + "/**"]), strip_prefix = hdrs_pkg_root, visibility = visibility, ) pkg_zip( name = name + "-hdrs-zip", srcs = extra_hdr_pkg_files + maybe_license_pkg + [name + "-hdrs-pkg"] + [lib + "-hdrs-pkg" for lib in third_party_libraries + third_party_header_only_libraries], tags = ["manual"], ) def wpilib_cc_shared_library( name, auto_export_windows_symbols = True, user_link_flags = None, visibility = None, use_debug_name = True, features = None, win_def_file = None, symbols = None, additional_linker_inputs = None, **kwargs): """Builds a cc_shared_library with some wpilib conventions applied. The main workhorse of this rule is cc_shared_library and a pkg_files on the back end to collect up the outputs. wpilib has some extra conventions we can commonalize. Args: auto_export_windows_symbols: If true, export all the symbols found in the shared library on Windows. user_link_flags: User link flags to add to the linking process. Note: this gets augmented with extra flags to produce libfoo.so or libfood.so if in debug mode. additional_linker_inputs: Additional inputs to provide to the linking process. use_debug_name: If true, (default): when in debug mode, produce libfood.so, otherwise produce libfoo.so. This matches the wpilib convention for debug library naming. JNI libraries though want to be loaded with the same name for all builds, which necessitates turning this off. win_def_file: The .def file used to specify symbols used in linking on Windows. This is selected automatically such that it is only used on Windows. symbols: The symbols to export in the shared library. """ folder, lib = _folder_prefix(name) if not features: features = [] if symbols: if win_def_file: fail("Can't specify symbols and win_def_file") exports_target = name + "_exports" gen_versionscript( name = exports_target, src = symbols, lib_name = lib, format = select({ "@platforms//os:linux": "linux", "@platforms//os:osx": "osx", "@platforms//os:windows": "windows", }), ) additional_linker_inputs = (additional_linker_inputs or []) + select({ "@platforms//os:windows": [], "//conditions:default": [exports_target], }) user_link_flags = (user_link_flags or []) + select({ "@platforms//os:linux": ["-Wl,--version-script,$(location " + exports_target + ")"], "@platforms//os:osx": ["-Wl,-exported_symbols_list,$(location " + exports_target + ")"], "@platforms//os:windows": [], }) win_def_file = exports_target if auto_export_windows_symbols: features.append("windows_export_all_symbols") if use_debug_name: user_link_flags = (user_link_flags or []) + select({ "//shared/bazel/rules:linux_compilation_mode_dbg": ["-Wl,-soname,lib" + lib + "d.so"], "//shared/bazel/rules:osx_compilation_mode_dbg": ["-Wl,-install_name,lib" + lib + "d.so"], "@platforms//os:linux": ["-Wl,-soname,lib" + lib + ".so"], "@platforms//os:osx": ["-Wl,-install_name,lib" + lib + ".so"], "//conditions:default": [], }) else: user_link_flags = (user_link_flags or []) + select({ "@platforms//os:linux": ["-Wl,-soname,lib" + lib + ".so"], "@platforms//os:osx": ["-Wl,-install_name,lib" + lib + ".so"], "//conditions:default": [], }) cc_shared_library( name = name, user_link_flags = user_link_flags, features = features, visibility = visibility, additional_linker_inputs = additional_linker_inputs, # Only include a .def file on windows. This makes it so we can mark # the .def file as only compatible with windows. win_def_file = select({ "@platforms//os:windows": win_def_file, "//conditions:default": None, }), **kwargs ) universal_name = "universal/lib" + lib + ".lib" universal_binary( name = universal_name, binary = name, target_compatible_with = [ "@platforms//os:osx", ], ) _split_debug_symbols( name = name + "-symbolsplit", copy = select({ "@rules_bzlmodrio_toolchains//conditions:linux_arm64": False, "@rules_bzlmodrio_toolchains//conditions:linux_x86_64": True, "//conditions:default": True, }), use_debug_name = select({ "//shared/bazel/rules:compilation_mode_dbg": True, "//conditions:default": False, }) if use_debug_name else False, shared_library = name, ) pkg_files( name = folder + "/lib" + lib + "-shared-files", srcs = select({ "@rules_bzlmodrio_toolchains//conditions:osx": [universal_name], "//conditions:default": [ ":" + name + "-symbolsplit", ], }), strip_prefix = select({ "@rules_bzlmodrio_toolchains//conditions:osx": "universal", "//conditions:default": None, }), visibility = visibility, ) CcStaticLibraryInfo = provider( "Information about a cc static library.", fields = { "linker_input": "the resulting linker input artifact for the static library", "used_objects": "the object files already accounted for", }, ) def _accumulate_used_objects(ctx): transitive_used_objects = [] for dep in ctx.attr.static_deps: transitive_used_objects.append(dep[CcStaticLibraryInfo].used_objects) return transitive_used_objects def _filter_inputs( ctx, feature_configuration, cc_toolchain, deps, used_objects): dependency_linker_inputs_sets = [] for dep in deps: dependency_linker_inputs_sets.append(dep[CcInfo].linking_context.linker_inputs) dependency_linker_inputs = depset(transitive = dependency_linker_inputs_sets, order = "topological").to_list() used_objects_depset = depset(transitive = used_objects, order = "topological").to_list() linker_inputs = [] for linker_input in dependency_linker_inputs: for lib in linker_input.libraries: if lib.pic_objects: for o in lib.pic_objects: if o not in used_objects_depset: linker_inputs.append(o) elif lib.objects: for o in lib.objects: if o not in used_objects_depset: linker_inputs.append(o) return sorted(linker_inputs) def _cc_static_library_impl(ctx): """ This is a modified version of built in cc_static_library implementation https://github.com/bazelbuild/bazel/blob/8.2.1/src/main/starlark/builtins_bzl/common/cc/experimental_cc_static_library.bzl The built in version amalgamates all of the transitive dependency objects into a single shared library. However, we do not want our static libraries to only have the symbols related to the objects for this library, and not anything transitive. In order to do this, we add the option to specify transitive static_libraries. The rule then filters out the objects that are defines in the other static libraries. """ deps = ctx.attr.deps cc_toolchain = find_cpp_toolchain(ctx) feature_configuration = cc_common.configure_features( ctx = ctx, cc_toolchain = cc_toolchain, requested_features = ctx.features + ["force_no_whole_archive"], unsupported_features = ctx.disabled_features, ) # Find all the objects which are already in another static library. used_objects = _accumulate_used_objects(ctx) # Now, find the ones we depend on which aren't. libs = _filter_inputs( ctx, feature_configuration, cc_toolchain, deps, used_objects, ) used_objects_depset = depset(direct = libs, transitive = used_objects, order = "topological") # Generate the output library name if one isn't provided. output_file = ctx.actions.declare_file(ctx.attr.static_lib_name) # And, now do it. linker_input = cc_common.create_linker_input( owner = ctx.label, libraries = depset(direct = [ cc_common.create_library_to_link( actions = ctx.actions, feature_configuration = feature_configuration, cc_toolchain = cc_toolchain, static_library = output_file, ), ]), ) compilation_context = cc_common.create_compilation_context() linking_context = cc_common.create_linking_context(linker_inputs = depset(direct = [linker_input], order = "topological")) archiver_path = cc_common.get_tool_for_action( feature_configuration = feature_configuration, action_name = CPP_LINK_STATIC_LIBRARY_ACTION_NAME, ) archiver_variables = cc_common.create_link_variables( feature_configuration = feature_configuration, cc_toolchain = cc_toolchain, output_file = output_file.path, is_using_linker = False, ) command_line = cc_common.get_memory_inefficient_command_line( feature_configuration = feature_configuration, action_name = CPP_LINK_STATIC_LIBRARY_ACTION_NAME, variables = archiver_variables, ) args = ctx.actions.args() args.add_all(command_line) args.add_all(libs) if cc_common.is_enabled( feature_configuration = feature_configuration, feature_name = "archive_param_file", ): # TODO: The flag file arg should come from the toolchain instead. args.use_param_file("@%s", use_always = True) env = cc_common.get_environment_variables( feature_configuration = feature_configuration, action_name = CPP_LINK_STATIC_LIBRARY_ACTION_NAME, variables = archiver_variables, ) ctx.actions.run( executable = archiver_path, arguments = [args], env = env, inputs = depset( direct = libs, transitive = [ cc_toolchain.all_files, ], ), outputs = [output_file], ) cc_info = cc_common.merge_cc_infos(cc_infos = [ CcInfo(compilation_context = compilation_context, linking_context = linking_context), ] + [dep[CcInfo] for dep in ctx.attr.deps]) # TODO(austin): Do we want this to be able to link into a binary? Probably... Need to figure out what the right result needs to be for that. return [ cc_info, DefaultInfo( files = depset([output_file]), ), CcStaticLibraryInfo( used_objects = used_objects_depset, linker_input = linker_input, ), ] _wpilib_cc_static_library = rule( implementation = _cc_static_library_impl, attrs = { "deps": attr.label_list( providers = [CcInfo], doc = """ List of all the dependencies to accumulate objects from to link into this static library. """, ), "static_deps": attr.label_list( providers = [CcStaticLibraryInfo], doc = """ List of all static libraries to not duplicate .o files from. """, ), "static_lib_name": attr.string(doc = """ By default cc_static_library will use a name for the static library output file based on the target's name and the platform. This includes an extension and sometimes a prefix. Sometimes you may not want the default name, in which case you can use this attribute to choose a custom name."""), } | CC_TOOLCHAIN_ATTRS, toolchains = use_cc_toolchain(), fragments = ["cpp"], ) def wpilib_cc_static_library( name, static_lib_name = None, **kwargs): if not static_lib_name: folder, lib = _folder_prefix(name) static_lib_name = select({ "//shared/bazel/rules:compilation_mode_dbg": folder + "/lib" + lib + "d.a", "//shared/bazel/rules:compilation_mode_windows_dbg": folder + "/" + lib + ".lib", "@platforms//os:windows": folder + "/" + lib + ".lib", "//conditions:default": folder + "/lib" + lib + ".a", }) _wpilib_cc_static_library( name = name, static_lib_name = static_lib_name, **kwargs ) def _generate_def_windows_impl(ctx): # Generate the .def file for Windows. Do this by finding the .obj files # specified, and then passing those into the def file generator. deps = ctx.attr.deps cc_toolchain = find_cpp_toolchain(ctx) feature_configuration = cc_common.configure_features( ctx = ctx, cc_toolchain = cc_toolchain, requested_features = ctx.features + ["force_no_whole_archive"], unsupported_features = ctx.disabled_features, ) def_parser = ctx.file._def_parser win_def_file = [] if cc_common.is_enabled(feature_configuration = feature_configuration, feature_name = "targets_windows"): object_files = [] # Now, hunt down all the linker inputs directly specified. for dep in deps: linker_input = dep[CcInfo].linking_context.linker_inputs for l in linker_input.to_list(): # Find the linker stanza owned directly by the dependency, not transitively if l.owner != dep.label: continue # Grab all the .o's out of it. for library in l.libraries: if library.pic_static_library != None: if library.pic_objects != None: object_files.extend(library.pic_objects) elif library.static_library != None: if library.objects != None: object_files.extend(library.objects) # Filter the list so we only generate def files for the provided dependencies. filtered_object_files = [] for o in object_files: for f in ctx.attr.filters: if f in o.path: filtered_object_files.append(o) break if def_parser != None: generated_def_file = generate_def_file(ctx, def_parser, filtered_object_files, ctx.label.name) win_def_file = [generated_def_file] files = depset(direct = win_def_file) return [ DefaultInfo(files = files), OutputGroupInfo(default = files), ] _generate_def_windows = rule( implementation = _generate_def_windows_impl, attrs = { "deps": attr.label_list( providers = [CcInfo], doc = """ List of all static libraries to not duplicate .o files from. """, ), "filters": attr.string_list(), "_def_parser": attr.label(default = "@bazel_tools//tools/def_parser:def_parser", allow_single_file = True, cfg = "exec"), } | CC_TOOLCHAIN_ATTRS, toolchains = use_cc_toolchain(), fragments = ["cpp"], ) def generate_def_windows(name, deps = None, **kwargs): """Generates a .def file for linking a windows .dll for the provided cc_library and filters Args: deps: A list of cc_libraries to export symbols from. filters: All object files in the provided cc_libraries (but not their dependencies) are checked against this list. If a string in this list appears inside the name of the object file, it is added to the export list. """ _generate_def_windows( name = name, deps = deps, target_compatible_with = ["@platforms//os:windows"], **kwargs )