Skip to main content
Glama
headers.bzl17.4 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//:paths.bzl", "paths") load("@prelude//cxx:compile_types.bzl", "HeadersDepFiles") load("@prelude//cxx:cxx_toolchain_types.bzl", "LinkerType") load("@prelude//cxx:cxx_utility.bzl", "cxx_attrs_get_allow_cache_upload") load("@prelude//utils:expect.bzl", "expect") load("@prelude//utils:lazy.bzl", "lazy") load("@prelude//utils:utils.bzl", "from_named_set", "map_val", "value_or") load(":cxx_context.bzl", "get_cxx_toolchain_info") load(":platform.bzl", "cxx_by_platform") # Defines the varying bits of implementation affecting on how the end user # should include the headers. # Given there are 2 headers which are defined: # a) one header in a list, as ["foo/bar/foobar.h"] # b) one header in a dict (aka named header), as {"wfh/baz.h": "path/header.h"} # # `apple`: # 1) header from the list should be included as NAMESPACE/PATH_BASENAME: # #include "namespace/foobar.h" # 2) header from the dict should be included as DICT_KEY (aka header name): # #include "wfh/baz.h" # 3) it should be possible to include list header from the same target via basename: # #include "foobar.h" # # `regular`: # 1) header from the list should be included as NAMESPACE/PATH: # #include "namespace/foo/bar/foobar.h" # 2) header from the dict should be included as NAMESPACE/DICT_KEY: # #include "namespace/wfh/baz.h" CxxHeadersNaming = enum("apple", "regular") # Modes supporting implementing the `raw_headers` parameter of C++ rules using # symlink trees and/or header maps through `headers`. RawHeadersAsHeadersMode = enum( "enabled", "disabled", ) # Modes supporting implementing the `headers` parameter of C++ rules using raw # headers instead of e.g. symlink trees. HeadersAsRawHeadersMode = enum( # Require that all headers be implemented as raw headers, failing if this # is not possible. "required", # Attempt to implement headers via raw headers, falling to header maps or # symlink tress when raw headers cannot be used (e.g. rule contains a # generated header or remaps a header to an incompatible location in the # header namespace). "preferred", "disabled", ) HeaderMode = enum( # Creates the header map that references the headers directly in the source # tree. "header_map_only", # Creates the tree of symbolic links of headers. "symlink_tree_only", # Creates the tree of symbolic links of headers and creates the header map # that references the symbolic links to the headers. "symlink_tree_with_header_map", ) HeaderStyle = enum( "local", "system", ) Headers = record( include_path = field(cmd_args), # NOTE(agallagher): Used for module hack replacement. symlink_tree = field(Artifact | None, None), # args that map symlinked private headers to source path file_prefix_args = field([cmd_args, None], None), ) CHeader = record( # `"artifact"` pointing to the actual header file artifact = Artifact, # Basename as it should appear in include directive name = str, # Prefix before the basename as it should appear in include directive namespace = str, # Whether or not this header is provided via dict, where the corresponding key is a new name named = bool, ) # Parameters controlling the varying aspects of headers-related behavior. # The contract on how headers could be used (i.e. end user inclusion rules) # is different for `apple_library` and `cxx_library`. Those parameters # allows generalizing the C++ rules implementation and are provided # by top-level user-facing wrappers around those generalized methods. CxxHeadersLayout = record( # Prefix part of the header path in the include statement. Header name might # not always be prepended by the namespace, `naming` parameter is controlling # that behavior. The value is ready to be used and abstracts different naming # for such prefix in user-facing attributes (e.g. `apple_binary.header_path_prefix` # vs `cxx_binary.header_namespace`) and different default values when those # attributes are omitted (package path for regular C++ rules vs target name for # Apple-specific rules). namespace = str, # Selects the behavior in the implementation to support the specific way of how # headers are allowed to be included (e.g. if header namespace is applied for # headers from dicts). For more information see comment for `CxxHeadersNaming` naming = CxxHeadersNaming, ) CPrecompiledHeaderInfo = provider(fields = { # Actual precompiled header ready to be used during compilation. "header": Artifact, }) def cxx_attr_header_namespace(ctx: AnalysisContext) -> str: return value_or(ctx.attrs.header_namespace, ctx.label.package) def cxx_attr_headers_list(ctx: AnalysisContext, headers: typing.Any, platform_headers: typing.Any, headers_layout: CxxHeadersLayout) -> list[CHeader]: headers = _get_attr_headers(headers, headers_layout.namespace, headers_layout.naming) platform_headers = _get_attr_headers(_headers_by_platform(ctx, platform_headers), headers_layout.namespace, headers_layout.naming) return headers + platform_headers def cxx_attr_exported_headers(ctx: AnalysisContext, headers_layout: CxxHeadersLayout) -> list[CHeader]: return cxx_attr_headers_list(ctx, ctx.attrs.exported_headers, ctx.attrs.exported_platform_headers, headers_layout) def cxx_attr_headers(ctx: AnalysisContext, headers_layout: CxxHeadersLayout) -> list[CHeader]: return cxx_attr_headers_list(ctx, ctx.attrs.headers, ctx.attrs.platform_headers, headers_layout) def cxx_get_regular_cxx_headers_layout(ctx: AnalysisContext) -> CxxHeadersLayout: namespace = cxx_attr_header_namespace(ctx) return CxxHeadersLayout(namespace = namespace, naming = CxxHeadersNaming("regular")) def cxx_attr_exported_header_style(ctx: AnalysisContext) -> HeaderStyle: return HeaderStyle(ctx.attrs.exported_header_style) def _concat_inc_dir_with_raw_header(namespace, inc_dir, header) -> list[str] | None: namespace_parts = namespace.split("/") inc_dir_parts = inc_dir.split("/") header_parts = header.short_path.split("/") for part in inc_dir_parts: if part == ".": continue if part == "..": if not namespace_parts: # Too many .., would set include root out of cell return None header_parts = [namespace_parts.pop()] + header_parts elif part == header_parts[0]: header_parts = header_parts[1:] else: # Header not accessible under this folder return None return header_parts def as_headers( ctx: AnalysisContext, raw_headers: list[Artifact], include_directories: list[str]) -> list[CHeader]: headers = [] base_namespace = ctx.label.package for header in raw_headers: for inc_dir in include_directories: inc_dir = paths.normalize(inc_dir) mapped_header = _concat_inc_dir_with_raw_header(base_namespace, inc_dir, header) if mapped_header: headers.append(CHeader(artifact = header, name = "/".join(mapped_header), namespace = "", named = True)) return headers def _get_attr_headers(xs: typing.Any, namespace: str, naming: CxxHeadersNaming) -> list[CHeader]: if type(xs) == type([]): return [CHeader(artifact = x, name = _get_list_header_name(x, naming), namespace = namespace, named = False) for x in xs] else: return [CHeader(artifact = xs[x], name = x, namespace = _get_dict_header_namespace(namespace, naming), named = True) for x in xs] def _headers_by_platform(ctx: AnalysisContext, xs: list[(str, typing.Any)]) -> typing.Any: res = {} for deps in cxx_by_platform(ctx, xs): res.update(from_named_set(deps)) return res def as_raw_headers( ctx: AnalysisContext, headers: dict[str, Artifact], mode: HeadersAsRawHeadersMode) -> [list[CellPath], None]: """ Return the include directories needed to treat the given headers as raw headers, depending on the given `HeadersAsRawHeadersMode` mode. Args: mode: disabled - always return `None` preferred - return `None` if conversion isn't possible required - fail if conversion isn't possible """ # If we're not supporting raw header conversion, return `None`. if mode == HeadersAsRawHeadersMode("disabled"): return None return _as_raw_headers( ctx, headers, # Don't fail if conversion isn't required. no_fail = mode != HeadersAsRawHeadersMode("required"), ) def _header_mode(ctx: AnalysisContext) -> HeaderMode: toolchain_header_mode = get_cxx_toolchain_info(ctx).header_mode # If the toolchain disabled header maps, respect that since the compiler # simply cannot accept anything else. if toolchain_header_mode == HeaderMode("symlink_tree_only"): return toolchain_header_mode # If the target specifies a header mode, use that in case it needs # a symlink tree (even with header maps) header_mode = map_val(HeaderMode, getattr(ctx.attrs, "header_mode", None)) if header_mode != None: return header_mode return toolchain_header_mode def prepare_headers(ctx: AnalysisContext, srcs: dict[str, Artifact], name: str) -> [Headers, None]: """ Prepare all the headers we want to use, depending on the header_mode set on the target's toolchain. - In the case of a header map, we create a `name`.hmap file and return it as part of the include path. - In the case of a symlink tree, we create a directory of `name` containing the headers and return it as part of the include path. """ if len(srcs) == 0: return None header_mode = _header_mode(ctx) # TODO(T110378135): There's a bug in clang where using header maps w/o # explicit `-I` anchors breaks module map lookups. This will be fixed # by https://reviews.llvm.org/D103930 so, until it lands, disable header # maps when we see a module map. if (header_mode == HeaderMode("symlink_tree_with_header_map") and lazy.is_any(lambda n: paths.basename(n) == "module.modulemap", srcs.keys())): header_mode = HeaderMode("symlink_tree_only") output_name = name if header_mode == HeaderMode("header_map_only"): headers = {h: (a, "{}") for h, a in srcs.items()} hmap = _mk_hmap(ctx, output_name, headers) return Headers( include_path = cmd_args(hmap, hidden = srcs.values()), ) symlink_dir = ctx.actions.symlinked_dir(output_name, _normalize_header_srcs(srcs)) if header_mode == HeaderMode("symlink_tree_only"): return Headers(include_path = cmd_args(symlink_dir), symlink_tree = symlink_dir) if header_mode == HeaderMode("symlink_tree_with_header_map"): headers = {h: (symlink_dir, "{}/" + h) for h in srcs} hmap = _mk_hmap(ctx, output_name, headers) file_prefix_args = _get_debug_prefix_args(ctx, symlink_dir) return Headers( include_path = cmd_args(hmap, hidden = symlink_dir), symlink_tree = symlink_dir, file_prefix_args = file_prefix_args, ) fail("Unsupported header mode: {}".format(header_mode)) def _normalize_header_srcs(srcs: dict) -> dict: normalized_srcs = {} for key, val in srcs.items(): normalized_key = paths.normalize(key) stored_val = normalized_srcs.get(normalized_key, None) expect( stored_val == None or stored_val == val, "Got different values {} and {} for the same normalized header {}".format( val, stored_val, normalized_key, ), ) normalized_srcs[normalized_key] = val return normalized_srcs def _as_raw_headers( ctx: AnalysisContext, headers: dict[str, Artifact], # Return `None` instead of failing. no_fail: bool = False) -> [list[CellPath], None]: """ Return the include directories needed to treat the given headers as raw headers. """ # Find the all the include dirs needed to treat the given headers as raw # headers. inc_dirs = {} for name, header in headers.items(): inc_dir = _as_raw_header( ctx, name, header, no_fail = no_fail, ) # If the conversion wasn't possible, `inc_dir` will be `None` and we # should bail now. if inc_dir == None: return None inc_dirs[inc_dir] = None return [ctx.label.path.add(p) for p in inc_dirs] def _as_raw_header( ctx: AnalysisContext, # The full name used to include the header. name: str, header: Artifact, # Return `None` instead of failing. no_fail: bool = False) -> [str, None]: """ Return path to pass to `include_directories` to treat the given header as a raw header. """ # We can't handle generated headers. if not header.is_source: if no_fail: return None fail("generated headers cannot be used as raw headers ({})" .format(header)) # To include the header via its name using raw headers and include dirs, # it needs to be a suffix of its original path, and we'll strip the include # name to get the include dir used to include it. path = paths.join(ctx.label.package, header.short_path) base = paths.strip_suffix(path, name) if base == None: if no_fail: return None fail("header name must be a path suffix of the header path to be " + "used as a raw header ({} => {})".format(name, header)) # If the include dir is underneath our package, then just relativize to find # out package-relative path. if len(base) > len(ctx.label.package): return paths.relativize(base, ctx.label.package) # Otherwise, this include dir needs to reference a parent dir. expect(ctx.label.package.startswith(base)) num_parents = ( len(ctx.label.package.split("/")) - (0 if not base else len(base.split("/"))) ) return "/".join([".."] * num_parents) def _get_list_header_name(header: Artifact, naming: CxxHeadersNaming) -> str: if naming.value == "regular": return header.short_path elif naming.value == "apple": return header.basename else: fail("Unsupported header naming: {}".format(naming)) def _get_dict_header_namespace(namespace: str, naming: CxxHeadersNaming) -> str: if naming.value == "regular": return namespace elif naming.value == "apple": return "" else: fail("Unsupported header naming: {}".format(naming)) def _get_debug_prefix_args(ctx: AnalysisContext, header_dir: Artifact) -> [cmd_args, None]: # NOTE(@christylee): Do we need to enable debug-prefix-map for darwin and windows? if get_cxx_toolchain_info(ctx).linker_info.type != LinkerType("gnu"): return None fmt = "-fdebug-prefix-map={}=" + value_or(header_dir.owner.cell, ".") return cmd_args( cmd_args(header_dir, format = fmt), ) def _mk_hmap(ctx: AnalysisContext, name: str, headers: dict[str, (Artifact, str)]) -> Artifact: output = ctx.actions.declare_output(name + ".hmap") header_args = cmd_args() for n, (path, fmt) in headers.items(): header_args.add(n) # We don't care about the header contents -- just their names. header_args.add(cmd_args(path, format = fmt, ignore_artifacts = True)) hmap_args_file = ctx.actions.write(output.basename + ".cxx_hmap_argsfile", cmd_args(header_args, quote = "shell")) cmd = cmd_args( [get_cxx_toolchain_info(ctx).internal_tools.hmap_wrapper] + ["--output", output.as_output()] + ["--mappings-file", hmap_args_file], hidden = header_args, ) ctx.actions.run(cmd, category = "generate_hmap", identifier = name, allow_cache_upload = cxx_attrs_get_allow_cache_upload(ctx.attrs)) return output def add_headers_dep_files( ctx: AnalysisContext, cmd: cmd_args, headers_dep_files: HeadersDepFiles, src: Artifact, filename_base: str, action_dep_files: dict[str, ArtifactTag]) -> cmd_args: dep_file = ctx.actions.declare_output( paths.join("__dep_files__", filename_base), ).as_output() processor_flags, compiler_flags = headers_dep_files.mk_flags( ctx.actions, filename_base, src, ) cmd.add(compiler_flags) # API: First argument is the dep file source path, second is the # dep file destination path, other arguments are the actual compile # command. cmd = cmd_args([ headers_dep_files.processor, headers_dep_files.dep_tracking_mode.value, processor_flags, headers_dep_files.tag.tag_artifacts(dep_file), cmd, ]) action_dep_files["headers"] = headers_dep_files.tag return cmd

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