Skip to main content
Glama
shared_libraries.bzl11 kB
# Copyright (c) Meta Platforms, Inc. and affiliates. # # This source code is licensed under both the MIT license found in the # LICENSE-MIT file in the root directory of this source tree and the Apache # License, Version 2.0 found in the LICENSE-APACHE file in the root directory # of this source tree. load("@prelude//cxx:cxx_toolchain_types.bzl", "CxxToolchainInfo") load( "@prelude//linking:link_info.bzl", "LinkArgs", "LinkedObject", # @unused Used as a type ) load("@prelude//linking:strip.bzl", "strip_object") load("@prelude//utils:expect.bzl", "expect") Soname = record( # Return the SONAME if it's a string, otherwise None. as_str = field(typing.Callable), # Return the SONAME as a string, throwing an error if it is actually an # artifact. ensure_str = field(typing.Callable), # Return `True` if the SONAME is respresented as a string. is_str = field(bool), # The the actual SONAME can be rerepsented by a static string, or the # contents of a file genrated at build time. _soname = field(str | Artifact), ) SharedLibrary = record( lib = field(LinkedObject), # The LinkArgs used to produce this SharedLibrary. This can be useful for debugging or # for downstream rules to reproduce the shared library with some modifications (for example # android relinker will link again with an added version script argument). # TODO(cjhopman): This is currently always available. link_args = field(list[LinkArgs] | None, None), # The sonames of the shared libraries that this links against. # TODO(cjhopman): This is currently always available. shlib_deps = field(list[str] | None, None), stripped_lib = field(Artifact | None, None), can_be_asset = field(bool, False), for_primary_apk = field(bool, False), soname = field(Soname), label = field(Label), extra_outputs = field(dict[str, list[DefaultInfo]], default = {}), ) def _ensure_str(soname: str | Artifact) -> str: expect(type(soname) == type(""), "SONAME is not a `str`: {}", soname) return soname def to_soname(soname: str | Artifact | Soname) -> Soname: if isinstance(soname, Soname): return soname soname_is_str = isinstance(soname, str) return Soname( as_str = lambda: soname if soname_is_str else None, ensure_str = lambda: _ensure_str(soname), is_str = soname_is_str, _soname = soname, ) def create_shlib( # The soname can either be a string or an artifact with the soname in # text form. soname: str | Artifact | Soname, **kwargs) -> SharedLibrary: return SharedLibrary( soname = to_soname(soname), **kwargs ) SharedLibraries = record( # A mapping of shared library SONAME (e.g. `libfoo.so.2`) to the artifact. # Since the SONAME is what the dynamic loader uses to uniquely identify # libraries, using this as the key allows easily detecting conflicts from # dependencies. libraries = field(list[SharedLibrary]), ) # T-set of SharedLibraries SharedLibrariesTSet = transitive_set() # Shared libraries required by top-level packaging rules (e.g. shared libs # for Python binary, symlink trees of shared libs for C++ binaries) SharedLibraryInfo = provider(fields = { "set": provider_field(SharedLibrariesTSet | None, default = None), }) def get_strip_non_global_flags(cxx_toolchain: CxxToolchainInfo) -> list: if cxx_toolchain.strip_flags_info and cxx_toolchain.strip_flags_info.strip_non_global_flags: return cxx_toolchain.strip_flags_info.strip_non_global_flags return ["--strip-unneeded"] def create_shlib_from_ctx( ctx: AnalysisContext, soname: str | Artifact | Soname, lib: LinkedObject): cxx_toolchain = getattr(ctx.attrs, "_cxx_toolchain", None) return create_shlib( lib = lib, stripped_lib = strip_object( ctx, cxx_toolchain[CxxToolchainInfo], lib.output, cmd_args(get_strip_non_global_flags(cxx_toolchain[CxxToolchainInfo])), ) if cxx_toolchain != None else None, link_args = lib.link_args, shlib_deps = None, # TODO(cjhopman): we need this figured out. can_be_asset = getattr(ctx.attrs, "can_be_asset", False) or False, for_primary_apk = getattr(ctx.attrs, "used_by_wrap_script", False), label = ctx.label, soname = soname, ) def create_shared_libraries( ctx: AnalysisContext, libraries: dict[str, LinkedObject]) -> SharedLibraries: """ Take a mapping of dest -> src and turn it into a mapping that will be passed around in providers. Used for both srcs, and resources. """ return SharedLibraries( libraries = [ create_shlib_from_ctx(ctx = ctx, soname = name, lib = shlib) for (name, shlib) in libraries.items() ], ) # Merge multiple SharedLibraryInfo. The value in `node` represents a set of # SharedLibraries that is provided by the target being analyzed. It's optional # because that might not always exist, e.g. a Python library can pass through # SharedLibraryInfo but it cannot produce any. The value in `deps` represents # all the inherited shared libraries for this target. def merge_shared_libraries( actions: AnalysisActions, node: [SharedLibraries, None] = None, deps: list[SharedLibraryInfo] = []) -> SharedLibraryInfo: kwargs = {} children = filter(None, [dep.set for dep in deps]) if children: kwargs["children"] = children if node: kwargs["value"] = node set = actions.tset(SharedLibrariesTSet, **kwargs) if kwargs else None return SharedLibraryInfo(set = set) def traverse_shared_library_info(info: SharedLibraryInfo): # -> list[SharedLibrary]: libraries = [] if info.set: for libs in info.set.traverse(): libraries.extend(libs.libraries) return libraries # Helper to merge shlibs, throwing an error if more than one have the same SONAME. def _merge_shlibs( shared_libs: list[SharedLibrary], resolve_soname: typing.Callable) -> dict[str, SharedLibrary]: merged = {} for shlib in shared_libs: soname = resolve_soname(shlib.soname) existing = merged.get(soname) if existing != None and existing.lib != shlib.lib: error = ( "Duplicate library {}! Conflicting mappings:\n" + "{} from {}\n" + "{} from {}" ) fail( error.format( shlib.soname, existing.lib, existing.label, shlib.lib, shlib.label, ), ) merged[soname] = shlib return merged def with_unique_str_sonames( shared_libs: list[SharedLibrary], skip_dynamic: bool = False) -> dict[str, SharedLibrary]: """ Convert a list of `SharedLibrary`s to a map of unique SONAMEs to the corresponding `SharedLibrary`. Will fail if the same SONAME maps to multiple `SharedLibrary`s. """ return _merge_shlibs( shared_libs = [ shlib for shlib in shared_libs if shlib.soname.is_str or not skip_dynamic ], resolve_soname = lambda s: s.ensure_str(), ) def gen_shared_libs_action( actions: AnalysisActions, out: str, shared_libs: list[SharedLibrary], gen_action: typing.Callable, dir = False): """ Produce an action by first resolving all SONAME of the given shlibs and enforcing that each SONAME is unique. The provided `gen_action` callable is called with a map of unique SONAMEs to the corresponding shlibs. """ output = actions.declare_output(out, dir = dir) def func(actions, artifacts, output): def resolve_soname(soname): if soname.is_str: return soname._soname else: return artifacts[soname._soname].read_string().strip() gen_action( actions, output, _merge_shlibs( shared_libs = shared_libs, resolve_soname = resolve_soname, ), ) dynamic_sonames = [shlib.soname._soname for shlib in shared_libs if not shlib.soname.is_str] if dynamic_sonames: actions.dynamic_output( dynamic = dynamic_sonames, inputs = [], outputs = [output.as_output()], f = lambda ctx, artifacts, outputs: func(ctx.actions, artifacts, outputs[output]), ) else: func(actions, {}, output) return output def zip_shlibs( merged: dict[str, SharedLibrary], vals: list[(SharedLibrary, typing.Any)]) -> list[(str, SharedLibrary, typing.Any)]: """ Helper to "zip" together the soname->shlib map to a list with associated shared lib values. This is useful for callers of `gen_shared_libs_action` to combine the merged shared libs, in dedup'd dict form, with some additional data. """ zipped = [] # Walk through the shlib and val tuples idx = 0 for soname, shlib in merged.items(): for idx in range(idx, len(vals)): if vals[idx][0] == shlib: break zipped.append((soname, shlib, vals[idx][1])) return zipped def create_shlib_symlink_tree(actions: AnalysisActions, out: str, shared_libs: list[SharedLibrary]): """ Merged shared libs into a symlink tree mapping the library's SONAME to it's artifact. """ return gen_shared_libs_action( actions = actions, out = out, shared_libs = shared_libs, gen_action = lambda actions, output, shared_libs: actions.symlinked_dir( output, {name: shlib.lib.output for name, shlib in shared_libs.items()}, ), dir = True, ) def create_shlib_dwp_tree(actions: AnalysisActions, out: str, shared_libs: list[SharedLibrary]) -> Artifact: return gen_shared_libs_action( actions = actions, out = out, shared_libs = shared_libs, gen_action = lambda actions, output, shared_libs: actions.symlinked_dir( output, {name + ".dwp": shlib.lib.dwp for name, shlib in shared_libs.items() if shlib.lib.dwp != None}, ), dir = True, ) def extract_soname_from_shlib( actions: AnalysisActions, name: str, shared_lib: Artifact) -> Artifact: """ Extract the SONAME from a shared library into a file. """ soname = actions.declare_output(name) cmd = cmd_args( "sh", "-c", '''set -euo pipefail; objdump -p "$1" | grep SONAME | awk '{print $2}' > "$2"''', "", shared_lib, soname.as_output(), ) actions.run(cmd, category = "extract_soname", identifier = shared_lib.short_path) return soname

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/systeminit/si'

If you have feedback or need assistance with the MCP directory API, please join our Discord server