Skip to main content
Glama
apple_selective_debugging.bzl17.7 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//:artifact_tset.bzl", "ArtifactInfo", "ArtifactInfoTag", ) load("@prelude//apple:apple_common.bzl", "apple_common") load("@prelude//apple:apple_toolchain_types.bzl", "AppleToolchainInfo", "AppleToolsInfo") load( "@prelude//linking:execution_preference.bzl", "LinkExecutionPreference", "LinkExecutionPreferenceDeterminatorInfo", "LinkExecutionPreferenceInfo", # @unused Used as a type "get_action_execution_attributes", ) load("@prelude//user:rule_spec.bzl", "RuleRegistrationSpec") load( "@prelude//utils:build_target_pattern.bzl", "BuildTargetPattern", # @unused Used as a type "parse_build_target_pattern", ) load("@prelude//utils:lazy.bzl", "lazy") _SelectionCriteria = record( include_build_target_patterns = field(list[BuildTargetPattern], []), include_regular_expressions = field(list[regex], []), exclude_build_target_patterns = field(list[BuildTargetPattern], []), exclude_regular_expressions = field(list[regex], []), ) AppleSelectiveDebuggingInfo = provider( # @unsorted-dict-items fields = { "scrub_binary": provider_field(typing.Callable), "filter": provider_field(typing.Callable), "scrub_selected_debug_paths_file": provider_field(typing.Callable), }, ) AppleSelectiveDebuggingFilteredDebugInfo = record( # Contains all artifacts, including those required for debugging (e.g., `.swiftmodule` files) infos = field(list[ArtifactInfo]), # Contains only artifacts for _selected_ targets, excluding any others selected_target_infos = field(list[ArtifactInfo]), swift_modules_labels = field(list[Label]), metadata = field(Artifact), ) # The type of selective debugging json input to utilze. _SelectiveDebuggingJsonTypes = [ # Use a targets json file containing all targets to include. "targets", # Use a spec json file specifying the targets to include # and exclude via build target patterns and regular expressions. "spec", ] _SelectiveDebuggingJsonType = enum(*_SelectiveDebuggingJsonTypes) _LOCAL_LINK_THRESHOLD = 0.2 _OBJECT_FILE_EXTENSIONS = [ ".o", ".a", ] def _apple_skip_adhoc_resigning_scrubbed_frameworks_attr_value(bundle_ctx: AnalysisContext) -> bool: override_value = bundle_ctx.attrs._skip_adhoc_resigning_scrubbed_frameworks_override if override_value != None: # Override takes precedence over any other value return override_value skip = bundle_ctx.attrs.skip_adhoc_resigning_scrubbed_frameworks return skip if skip != None else bundle_ctx.attrs._skip_adhoc_resigning_scrubbed_frameworks_default def _should_skip_adhoc_sign_after_scrubbing(bundle_ctx: AnalysisContext) -> bool: sdk_name = bundle_ctx.attrs._apple_toolchain[AppleToolchainInfo].sdk_name if bundle_ctx.attrs.extension == "framework" and "iphonesimulator" in sdk_name: # While scrubbing invalidates the code signature, the signature of frameworks # are generally not checked by the system (unless running against the hardened # runtime for macOS apps). Re-signing scrubbed frameworks can take non-trivial # time, so skip it if it's not needed. return _apple_skip_adhoc_resigning_scrubbed_frameworks_attr_value(bundle_ctx) return False def _generate_metadata_json_object(is_any_selected_target_linked: bool) -> dict[str, typing.Any]: return { "contains_focused_targets": is_any_selected_target_linked, } def _apple_selective_debugging_impl(ctx: AnalysisContext) -> list[Provider]: json_type = _SelectiveDebuggingJsonType(ctx.attrs.json_type) # process inputs and provide them up the graph with typing include_build_target_patterns = [parse_build_target_pattern(pattern) for pattern in ctx.attrs.include_build_target_patterns] include_regular_expressions = [ # TODO(nga): fancy is probably not needed here. regex(expression, fancy = True) for expression in ctx.attrs.include_regular_expressions ] exclude_build_target_patterns = [parse_build_target_pattern(pattern) for pattern in ctx.attrs.exclude_build_target_patterns] exclude_regular_expressions = [ # TODO(nga): fancy is probably not needed here. regex(expression, fancy = True) for expression in ctx.attrs.exclude_regular_expressions ] scrubber = ctx.attrs._apple_tools[AppleToolsInfo].selective_debugging_scrubber targets_json_file = None cmd = cmd_args(scrubber) if json_type == _SelectiveDebuggingJsonType("targets"): targets_json_file = ctx.attrs.targets_json_file or ctx.actions.write_json("targets.json", {"targets": []}) # If a targets json file is not provided, write an empty json file: cmd.add("--targets-file") cmd.add(targets_json_file) elif json_type == _SelectiveDebuggingJsonType("spec"): json_data = { "exclude_build_target_patterns": ctx.attrs.exclude_build_target_patterns, "exclude_regular_expressions": ctx.attrs.exclude_regular_expressions, "include_build_target_patterns": ctx.attrs.include_build_target_patterns, "include_regular_expressions": ctx.attrs.include_regular_expressions, } spec_file = ctx.actions.write_json("selective_debugging_spec.json", json_data) cmd.add("--spec-file") cmd.add(spec_file) else: fail("Expected json_type to be either `targets` or `spec`.") selection_criteria = _SelectionCriteria( include_build_target_patterns = include_build_target_patterns, include_regular_expressions = include_regular_expressions, exclude_build_target_patterns = exclude_build_target_patterns, exclude_regular_expressions = exclude_regular_expressions, ) def scrub_selected_debug_paths_file(inner_ctx: AnalysisContext, package_names: list[str], output_name: str) -> Artifact: # In the event that _SelectiveDebuggingJsonType was "spec", we expect that `package_names` # was already filtered as part of scrubbing the binary in the apple_bundle. # # See `_maybe_scrub_binary()` in apple_bundle.bzl if json_type != _SelectiveDebuggingJsonType("targets"): return inner_ctx.actions.write(output_name, sorted(set(package_names))) def scrub_selected_debug_paths_action(dynamic_ctx: AnalysisContext, artifacts, outputs): packages = [ # "cell//path/to/some/thing:target" -> "path/to/some/thing" target.split("//")[1].split(":")[0] for target in artifacts[targets_json_file].read_json()["targets"] ] dynamic_ctx.actions.write( outputs.values()[0], sorted(set(filter(lambda p: p in packages, package_names))), ) output = inner_ctx.actions.declare_output(output_name) inner_ctx.actions.dynamic_output( dynamic = [targets_json_file], inputs = [], outputs = [output.as_output()], f = scrub_selected_debug_paths_action, ) return output def scrub_binary( inner_ctx, executable: Artifact, executable_link_execution_preference: LinkExecutionPreference, adhoc_codesign_tool: [RunInfo, None], focused_targets_labels: list[Label], identifier: None | str = None) -> Artifact: inner_cmd = cmd_args(cmd) subdir = "{}/".format(identifier) if identifier else "" output = inner_ctx.actions.declare_output("debug_scrubbed/{}{}".format(subdir, executable.short_path)) action_execution_properties = get_action_execution_attributes(executable_link_execution_preference) # If we're provided a codesign tool, provider it to the scrubber binary so that it may sign # the binary after scrubbing. if adhoc_codesign_tool and (not _should_skip_adhoc_sign_after_scrubbing(inner_ctx)): inner_cmd.add(["--adhoc-codesign-tool", adhoc_codesign_tool]) inner_cmd.add(["--input", executable]) inner_cmd.add(["--output", output.as_output()]) if len(focused_targets_labels) > 0: additional_labels_json = inner_ctx.actions.write_json( inner_ctx.attrs.name + ".additional_labels.json", {"targets": [label.raw_target() for label in focused_targets_labels]}, ) inner_cmd.add(["--persisted-targets-file", additional_labels_json]) inner_ctx.actions.run( inner_cmd, category = "scrub_binary", identifier = output.short_path, prefer_local = action_execution_properties.prefer_local, prefer_remote = action_execution_properties.prefer_remote, local_only = action_execution_properties.local_only, force_full_hybrid_if_capable = action_execution_properties.full_hybrid, ) return output def filter_debug_info(inner_ctx: AnalysisContext, debug_info: TransitiveSetIterator) -> AppleSelectiveDebuggingFilteredDebugInfo: artifact_infos = [] selected_target_infos = [] linked_targets = set() is_any_selected_target_linked = False is_using_spec = (json_type == _SelectiveDebuggingJsonType("spec")) selected_targets_contain_swift = False for infos in debug_info: for info in infos: is_swiftmodule = ArtifactInfoTag("swiftmodule") in info.tags is_swift_pcm = ArtifactInfoTag("swift_pcm") in info.tags is_swift_related = is_swiftmodule or is_swift_pcm is_label_included = _is_label_included(info.label, selection_criteria) is_any_selected_target_linked_when_using_spec = is_using_spec and is_any_selected_target_linked if not is_any_selected_target_linked_when_using_spec: # When using spec mode and there's already a selected target, there's no need # to continue the search whether a selected target is linked - we know at least # one is already linked. So, the if statement acts as a short-circuit for perf # reasons to avoid unnecessary work. # # In targets mode (i.e., non-spec mode), the full list of `linked_targets` must # be computed, as that value is used behind a dynamic output, so it's not possible # terminate the search early (as we cannot determine whether a selected target is linked # as the list of selected targets is stored in the targets JSON file, which is not available # at analysis time, only avail behind a dynamic output). debug_artifact_contains_object_code = lazy.is_any(lambda debug_artifact: debug_artifact.extension in _OBJECT_FILE_EXTENSIONS, info.artifacts) if debug_artifact_contains_object_code: if is_using_spec and is_label_included: is_any_selected_target_linked = True if not is_using_spec: # `linked_targets` only used in targets mode, avoid costs in all other modes linked_targets.add(info.label) if is_label_included: # `selected_target_infos` should only include targets explicitly selected by the user, # not anything included in addition to support the debugger (e.g., `.swiftmodule` files) selected_target_infos.append(info) if is_label_included or (selected_targets_contain_swift and is_swift_related): # There might be a few ArtifactInfo corresponding to the same Label, # so to avoid overwriting, we need to preserve all artifacts. artifact_infos.append(info) selected_targets_contain_swift = selected_targets_contain_swift or ArtifactInfoTag("swiftmodule") in info.tags if json_type == _SelectiveDebuggingJsonType("spec"): metadata_output = inner_ctx.actions.write_json( "selective_metadata_with_spec.json", _generate_metadata_json_object(is_any_selected_target_linked), pretty = True, ) elif json_type == _SelectiveDebuggingJsonType("targets"): def generate_metadata_output(dynamic_ctx: AnalysisContext, artifacts, outputs): targets = artifacts[targets_json_file].read_json()["targets"] is_any_selected_target_linked_inner = False for target in targets: cell, package_with_target_name = target.split("//") package, target_name = package_with_target_name.split(":") is_any_selected_target_linked_inner = lazy.is_any(lambda linked_target: linked_target.cell == cell and linked_target.package == package and linked_target.name == target_name, linked_targets) if is_any_selected_target_linked_inner: break dynamic_ctx.actions.write_json( outputs.values()[0], _generate_metadata_json_object(is_any_selected_target_linked_inner), pretty = True, ) metadata_output = inner_ctx.actions.declare_output("selective_metadata_with_targets_file.json") inner_ctx.actions.dynamic_output( dynamic = [targets_json_file], inputs = [], outputs = [metadata_output.as_output()], f = generate_metadata_output, ) else: fail("Unexpected type: {}".format(json_type)) return AppleSelectiveDebuggingFilteredDebugInfo( infos = artifact_infos, selected_target_infos = selected_target_infos, swift_modules_labels = [], metadata = metadata_output, ) def preference_for_links(links: list[Label], deps_preferences: list[LinkExecutionPreferenceInfo]) -> LinkExecutionPreference: # If any dependent links were run locally, prefer that the current link is also performed locally, # to avoid needing to upload the previous link. dep_prefered_local = lazy.is_any(lambda info: info.preference == LinkExecutionPreference("local"), deps_preferences) if dep_prefered_local: return LinkExecutionPreference("local") # If we're not provided a list of links, we can't make an informed determination. if not links: return LinkExecutionPreference("any") matching_links = filter(None, [link for link in links if _is_label_included(link, selection_criteria)]) # If more than 20% of targets being linked are also downloaded for debugging, perform the # link locally, as we'd need to download the object files anyway (and can skip downloading the link output). # Otherwise, perform the link remotely, and we'll just download the debug data separately. if len(matching_links) / len(links) >= _LOCAL_LINK_THRESHOLD: return LinkExecutionPreference("local") return LinkExecutionPreference("remote") return [ DefaultInfo(), AppleSelectiveDebuggingInfo( scrub_binary = scrub_binary, filter = filter_debug_info, scrub_selected_debug_paths_file = scrub_selected_debug_paths_file, ), LinkExecutionPreferenceDeterminatorInfo(preference_for_links = preference_for_links), ] registration_spec = RuleRegistrationSpec( name = "apple_selective_debugging", impl = _apple_selective_debugging_impl, attrs = { "exclude_build_target_patterns": attrs.list(attrs.string(), default = []), "exclude_regular_expressions": attrs.list(attrs.string(), default = []), "include_build_target_patterns": attrs.list(attrs.string(), default = []), "include_regular_expressions": attrs.list(attrs.string(), default = []), "json_type": attrs.enum(_SelectiveDebuggingJsonTypes), "targets_json_file": attrs.option(attrs.source(), default = None), } | apple_common.apple_tools_arg(), ) def _is_label_included(label: Label, selection_criteria: _SelectionCriteria) -> bool: # If no include criteria are provided, we then include everything, as long as it is not excluded. if selection_criteria.include_build_target_patterns or selection_criteria.include_regular_expressions: if not _check_if_label_matches_patterns_or_expressions(label, selection_criteria.include_build_target_patterns, selection_criteria.include_regular_expressions): return False # If included (above snippet), ensure that this target is not excluded. return not _check_if_label_matches_patterns_or_expressions(label, selection_criteria.exclude_build_target_patterns, selection_criteria.exclude_regular_expressions) def _check_if_label_matches_patterns_or_expressions(label: Label, patterns: list[BuildTargetPattern], expressions: list[regex]) -> bool: for pattern in patterns: if pattern.matches(label): return True for expression in expressions: if expression.match(str(label)): return True return False

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