Skip to main content
Glama
apple_bundle_resources.bzl25.8 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//:artifacts.bzl", "single_artifact") load("@prelude//:paths.bzl", "paths") load("@prelude//apple:apple_toolchain_types.bzl", "AppleToolchainInfo") load("@prelude//cxx:headers.bzl", "CHeader") load( "@prelude//linking:link_info.bzl", "CxxSanitizerRuntimeInfo", ) load("@prelude//utils:utils.bzl", "flatten_dict") load( ":apple_asset_catalog.bzl", "compile_apple_asset_catalog", ) load( ":apple_asset_catalog_types.bzl", "AppleAssetCatalogSpec", # @unused Used as a type ) load(":apple_bundle_destination.bzl", "AppleBundleDestination") load(":apple_bundle_part.bzl", "AppleBundlePart") load(":apple_bundle_types.bzl", "AppleBundleInfo", "AppleBundleTypeAppClip", "AppleBundleTypeDefault", "AppleBundleTypeExtensionKitExtension", "AppleBundleTypeWatchApp") load(":apple_bundle_utility.bzl", "get_bundle_resource_processing_options", "get_default_binary_dep", "get_extension_attr", "get_flattened_binary_deps", "get_is_watch_bundle", "get_product_name") load(":apple_core_data.bzl", "compile_apple_core_data") load( ":apple_core_data_types.bzl", "AppleCoreDataSpec", # @unused Used as a type ) load(":apple_info_plist.bzl", "process_info_plist", "process_plist") load(":apple_library.bzl", "AppleLibraryForDistributionInfo") load(":apple_library_types.bzl", "AppleLibraryInfo") load( ":apple_resource_types.bzl", "AppleResourceDestination", "AppleResourceSpec", # @unused Used as a type "CxxResourceSpec", # @unused Used as a type ) load(":apple_resource_utility.bzl", "apple_bundle_destination_from_resource_destination") load(":modulemap.bzl", "create_modulemap") load( ":resource_groups.bzl", "create_resource_graph", "get_filtered_resources", "get_resource_graph_node_map_func", "get_resource_group_info", ) load(":scene_kit_assets.bzl", "compile_scene_kit_assets") load( ":scene_kit_assets_types.bzl", "SceneKitAssetsSpec", # @unused Used as a type ) AppleBundleResourcePartListOutput = record( # Resource parts to be copied into an Apple bundle, *excluding* binaries resource_parts = field(list[AppleBundlePart]), # Part that holds the info.plist info_plist_part = field(AppleBundlePart), ) def get_apple_bundle_resource_part_list(ctx: AnalysisContext) -> AppleBundleResourcePartListOutput: parts = [] parts.extend(_create_pkg_info_if_needed(ctx)) parts.extend(_copy_privacy_manifest_if_needed(ctx)) (resource_specs, asset_catalog_specs, core_data_specs, scene_kit_assets_spec, cxx_resource_specs) = _select_resources(ctx) # If we've pulled in native/C++ resources from deps, inline them into the # bundle under the `CxxResources` namespace. cxx_resources = flatten_dict([s.resources for s in cxx_resource_specs]) if cxx_resources: cxx_res_dir = ctx.actions.copied_dir( "CxxResources", { name: resource.default_output for name, resource in cxx_resources.items() }, ) resource_specs.append( AppleResourceSpec( dirs = [cxx_res_dir], destination = AppleResourceDestination("resources"), ), ) cxx_sanitizer_runtime_info = get_default_binary_dep(ctx.attrs.binary).get(CxxSanitizerRuntimeInfo) if ctx.attrs.binary else None if cxx_sanitizer_runtime_info: runtime_resource_spec = AppleResourceSpec( files = cxx_sanitizer_runtime_info.runtime_files, destination = AppleResourceDestination("frameworks"), # Sanitizer dylibs require signing, for hardened runtime on macOS and iOS device builds codesign_files_on_copy = True, ) resource_specs.append(runtime_resource_spec) asset_catalog_result = compile_apple_asset_catalog(ctx, asset_catalog_specs) if asset_catalog_result != None: asset_catalog_part = AppleBundlePart( source = asset_catalog_result.compiled_catalog, destination = AppleBundleDestination("resources"), # We only interested in directory contents new_name = "", ) parts.append(asset_catalog_part) extra_plist = asset_catalog_result.catalog_plist if asset_catalog_result != None else None info_plist_part = process_info_plist(ctx = ctx, override_input = extra_plist) core_data_result = compile_apple_core_data(ctx, core_data_specs, get_product_name(ctx)) if core_data_result != None: core_data_part = AppleBundlePart( source = core_data_result, destination = AppleBundleDestination("resources"), # We only interested in directory contents new_name = "", ) parts.append(core_data_part) scene_kit_assets_result = compile_scene_kit_assets(ctx, scene_kit_assets_spec) if scene_kit_assets_result != None: scene_kit_assets_part = AppleBundlePart( source = scene_kit_assets_result, destination = AppleBundleDestination("resources"), # We only interested in directory contents new_name = "", ) parts.append(scene_kit_assets_part) parts.extend(_copy_resources(ctx, resource_specs)) parts.extend(_copy_first_level_bundles(ctx)) parts.extend(_copy_public_headers(ctx)) parts.extend(_copy_module_map(ctx)) parts.extend(_copy_swift_library_evolution_support(ctx)) return AppleBundleResourcePartListOutput( resource_parts = parts, info_plist_part = info_plist_part, ) # Same logic as in v1, see `buck_client/src/com/facebook/buck/apple/ApplePkgInfo.java` def _create_pkg_info_if_needed(ctx: AnalysisContext) -> list[AppleBundlePart]: extension = get_extension_attr(ctx) if extension == "xpc" or extension == "qlgenerator": return [] artifact = ctx.actions.write("PkgInfo", "APPLWRUN\n") return [AppleBundlePart(source = artifact, destination = AppleBundleDestination("metadata"))] def _copy_privacy_manifest_if_needed(ctx: AnalysisContext) -> list[AppleBundlePart]: privacy_manifest = ctx.attrs.privacy_manifest if privacy_manifest == None: return [] # According to apple docs, privacy manifest has to be named as `PrivacyInfo.xcprivacy` if privacy_manifest.short_path.split("/", 1)[-1] == "PrivacyInfo.xcprivacy": artifact = privacy_manifest else: output = ctx.actions.declare_output("PrivacyInfo.xcprivacy") artifact = ctx.actions.copy_file(output.as_output(), privacy_manifest) return [AppleBundlePart(source = artifact, destination = AppleBundleDestination("resources"))] def _select_resources(ctx: AnalysisContext) -> ((list[AppleResourceSpec], list[AppleAssetCatalogSpec], list[AppleCoreDataSpec], list[SceneKitAssetsSpec], list[CxxResourceSpec])): resource_group_info = get_resource_group_info(ctx) if resource_group_info: resource_groups_deps = resource_group_info.resource_group_to_implicit_deps_mapping.get(ctx.attrs.resource_group, []) if ctx.attrs.resource_group else [] resource_group_mappings = resource_group_info.mappings else: resource_groups_deps = [] resource_group_mappings = {} resource_graph = create_resource_graph( ctx = ctx, labels = [], bundle_binary = get_default_binary_dep(ctx.attrs.binary), deps = ctx.attrs.deps + resource_groups_deps, exported_deps = [], ) resource_graph_node_map_func = get_resource_graph_node_map_func(resource_graph) return get_filtered_resources(ctx.label, resource_graph_node_map_func, ctx.attrs.resource_group, resource_group_mappings) def _copy_swift_library_evolution_support(ctx: AnalysisContext) -> list[AppleBundlePart]: extension = get_extension_attr(ctx) if not extension == "framework": return [] binary_deps = getattr(ctx.attrs, "binary") if binary_deps == None: return [] swiftmodule_files = {} module_name = None for binary in get_flattened_binary_deps(binary_deps): apple_library_for_distribution_info = binary.get(AppleLibraryForDistributionInfo) if apple_library_for_distribution_info == None: continue module_name = apple_library_for_distribution_info.module_name if apple_library_for_distribution_info.swiftinterface != None: swiftmodule_files.update({ apple_library_for_distribution_info.target_triple + ".swiftinterface": apple_library_for_distribution_info.swiftinterface, apple_library_for_distribution_info.target_triple + ".private.swiftinterface": apple_library_for_distribution_info.private_swiftinterface, }) if apple_library_for_distribution_info.swiftdoc != None: swiftmodule_files.update({ apple_library_for_distribution_info.target_triple + ".swiftdoc": apple_library_for_distribution_info.swiftdoc, }) if len(swiftmodule_files) == 0 or module_name == None: return [] framework_module_dir = ctx.actions.declare_output(module_name + "framework.swiftmodule", dir = True) ctx.actions.copied_dir(framework_module_dir.as_output(), swiftmodule_files) return [AppleBundlePart(source = framework_module_dir, destination = AppleBundleDestination("modules"), new_name = module_name + ".swiftmodule")] def _public_headers(ctx: AnalysisContext) -> list[Artifact]: if not ctx.attrs.copy_public_framework_headers: return [] binary_deps = getattr(ctx.attrs, "binary") if binary_deps == None: return [] binary = get_default_binary_dep(binary_deps) apple_library_info = binary.get(AppleLibraryInfo) if apple_library_info == None: return [] tset = apple_library_info.public_framework_headers headers = [] if tset._tset: for public_framework_headers in tset._tset.traverse(): for public_framework_header in public_framework_headers: for artifact in public_framework_header.artifacts: headers.append(artifact) return headers def _copy_public_headers(ctx: AnalysisContext) -> list[AppleBundlePart]: if not ctx.attrs.copy_public_framework_headers: return [] binary_deps = getattr(ctx.attrs, "binary") if binary_deps == None: return [] binary = get_default_binary_dep(binary_deps) apple_library_info = binary.get(AppleLibraryInfo) if apple_library_info == None: return [] tset = apple_library_info.public_framework_headers bundle_parts = [] if tset._tset: for public_framework_headers in tset._tset.traverse(): for public_framework_header in public_framework_headers: for artifact in public_framework_header.artifacts: bundle_parts.append(AppleBundlePart(source = artifact, destination = AppleBundleDestination("headers"))) if apple_library_info.swift_header: bundle_parts.append(AppleBundlePart(source = apple_library_info.swift_header, destination = AppleBundleDestination("headers"))) return bundle_parts def _create_framework_module_map(ctx: AnalysisContext) -> Artifact: binary = get_default_binary_dep(ctx.attrs.binary) apple_library_for_distribution_info = binary.get(AppleLibraryForDistributionInfo) if apple_library_for_distribution_info == None: fail("Tried to generate an automatic modulemap for an unsupported binary dep. Please make sure it is a modular apple_library") headers = _public_headers(ctx) cheaders = [] for header in headers: cheaders.append(CHeader( artifact = header, name = header.basename, namespace = "", named = False, )) module_name = apple_library_for_distribution_info.module_name _, module_map = create_modulemap( ctx, "module", module_name, cheaders, None, False, None, is_framework = True, ) return module_map def _copy_module_map(ctx: AnalysisContext) -> list[AppleBundlePart]: extension = get_extension_attr(ctx) if not extension == "framework": return [] module_map = ctx.attrs.module_map if module_map == None: return [] if module_map == "auto": module_map = _create_framework_module_map(ctx) return [AppleBundlePart(source = module_map, destination = AppleBundleDestination("modules"))] def _copy_resources(ctx: AnalysisContext, specs: list[AppleResourceSpec]) -> list[AppleBundlePart]: result = [] for spec in specs: bundle_destination = apple_bundle_destination_from_resource_destination(spec.destination) result += [_process_apple_resource_file_if_needed( ctx = ctx, file = single_artifact(x).default_output, destination = bundle_destination, destination_relative_path = None, codesign_on_copy = spec.codesign_files_on_copy, codesign_entitlements = spec.codesign_entitlements, codesign_flags_override = spec.codesign_flags_override, ) for x in spec.files] result += _bundle_parts_for_dirs(spec.dirs, bundle_destination, False) result += _bundle_parts_for_dirs(spec.content_dirs, bundle_destination, True) result += _bundle_parts_for_variant_files(ctx, spec) return result def _copy_first_level_bundles(ctx: AnalysisContext) -> list[AppleBundlePart]: first_level_bundle_infos = filter(None, [dep.get(AppleBundleInfo) for dep in ctx.attrs.deps]) return filter(None, [_copied_bundle_spec(info) for info in first_level_bundle_infos]) def _copied_bundle_spec(bundle_info: AppleBundleInfo) -> [None, AppleBundlePart]: bundle = bundle_info.bundle bundle_extension = paths.split_extension(bundle.short_path)[1] if bundle_extension == ".framework": destination = AppleBundleDestination("frameworks") codesign_on_copy = True elif bundle_extension == ".app": app_destination_type = "plugins" if bundle_info.bundle_type == AppleBundleTypeWatchApp: app_destination_type = "watchapp" elif bundle_info.bundle_type == AppleBundleTypeAppClip: app_destination_type = "appclips" elif bundle_info.bundle_type != AppleBundleTypeDefault: fail("Unhandled bundle type `{}`".format(bundle_info.bundle_type)) destination = AppleBundleDestination(app_destination_type) codesign_on_copy = False elif bundle_extension == ".appex": # We have two types of extensions: App Extensions and ExtensionKit Extensions # # +----------------------+-------------------------------+-------------------------------+ # | | App Extension | ExtensionKit Extension | # +----------------------+-------------------------------+-------------------------------+ # | xcode project type | com.apple.product-type.app- | com.apple.product-type. | # | | extension | extensionkit-extension | # +----------------------+-------------------------------+-------------------------------+ # | Info.plist | NSExtensions | EXAppExtensionAttributes | # +----------------------+-------------------------------+-------------------------------+ # | bundle folder | *.app/PlugIns | *.app/Extensions | # +----------------------+-------------------------------+-------------------------------+ # if bundle_info.bundle_type == AppleBundleTypeExtensionKitExtension: destination = AppleBundleDestination("extensionkit_extensions") else: destination = AppleBundleDestination("plugins") codesign_on_copy = False elif bundle_extension == ".qlgenerator": destination = AppleBundleDestination("quicklook") codesign_on_copy = True elif bundle_extension == ".xpc": destination = AppleBundleDestination("xpcservices") codesign_on_copy = True elif bundle_extension == ".bundle": destination = AppleBundleDestination("plugins") codesign_on_copy = True else: fail("Extension `{}` is not yet supported.".format(bundle_extension)) return AppleBundlePart( source = bundle, destination = destination, codesign_on_copy = codesign_on_copy, extra_codesign_paths = bundle_info.extra_codesign_paths, ) def _bundle_parts_for_dirs(generated_dirs: list[Artifact], destination: AppleBundleDestination, copy_contents_only: bool) -> list[AppleBundlePart]: return [AppleBundlePart( source = generated_dir, destination = destination, new_name = "" if copy_contents_only else None, ) for generated_dir in generated_dirs] def _bundle_parts_for_variant_files(ctx: AnalysisContext, spec: AppleResourceSpec) -> list[AppleBundlePart]: result = [] # By definition, all variant files go into the resources destination bundle_destination = AppleBundleDestination("resources") for variant_file in spec.variant_files: variant_dest_subpath = _get_dest_subpath_for_variant_file(variant_file) bundle_part = _process_apple_resource_file_if_needed( ctx = ctx, file = variant_file, destination = bundle_destination, destination_relative_path = variant_dest_subpath, ) result.append(bundle_part) for locale, variant_files in spec.named_variant_files.items(): if not locale.endswith(".lproj"): fail("Keys for named variant files have to end with '.lproj' suffix, got {}".format(locale)) result += [ _process_apple_resource_file_if_needed( ctx = ctx, file = variant_file, destination = bundle_destination, destination_relative_path = paths.join(locale, paths.basename(variant_file.short_path)), ) for variant_file in variant_files ] return result def _run_ibtool( ctx: AnalysisContext, raw_file: Artifact, output: OutputArtifact, action_flags: list[str], target_device: [None, str], action_identifier: str, output_is_dir: bool) -> None: # TODO(T110378103): detect and add minimum deployment target automatically # TODO(T110378113): add support for ibtool modules (turned on by `ibtool_module_flag` field of `apple_bundle` rule) # Equivalent of `AppleProcessResources::BASE_IBTOOL_FLAGS` from v1 base_flags = ["--output-format", "human-readable-text", "--notices", "--warnings", "--errors"] ibtool = ctx.attrs._apple_toolchain[AppleToolchainInfo].ibtool ibtool_flags = getattr(ctx.attrs, "ibtool_flags", None) or [] ibtool_command = [ibtool] + base_flags + ibtool_flags if target_device != None: ibtool_command.extend(["--target-device", target_device]) ibtool_command.extend(action_flags) if output_is_dir: ibtool_command.append('"$TMPDIR"') else: ibtool_command.append(output) ibtool_command.append(raw_file) if output_is_dir: # Sandboxing and fs isolation on RE machines results in Xcode tools failing # when those are working in freshly created directories in buck-out. # See https://fb.workplace.com/groups/1042353022615812/permalink/1872164996301273/ # As a workaround create a directory in tmp, use it for Xcode tools, then # copy the result to buck-out. wrapper_script, _ = ctx.actions.write( "ibtool_wrapper.sh", [ cmd_args("set -euo pipefail"), cmd_args('export TMPDIR="$(mktemp -d)"'), cmd_args(cmd_args(ibtool_command), delimiter = " "), cmd_args(output, format = 'mkdir -p {} && cp -r "$TMPDIR"/ {}'), ], allow_args = True, ) command = cmd_args(["/bin/sh", wrapper_script], hidden = [ibtool_command, output]) else: command = ibtool_command processing_options = get_bundle_resource_processing_options(ctx) ctx.actions.run( command, prefer_local = processing_options.prefer_local, prefer_remote = processing_options.prefer_remote, allow_cache_upload = processing_options.allow_cache_upload, category = "apple_ibtool", identifier = action_identifier, ) def _ibtool_identifier(action: str, raw_file: Artifact) -> str: "*.xib files can live in .lproj folders and have the same name, so we need to split the id" identifier_parts = [] variant_name = _get_variant_dirname(raw_file) if variant_name: # variant_name is like "zh_TW.lproj", and we only want "zh_TW" identifier_parts.append(variant_name) identifier_parts += [raw_file.basename] return "ibtool_" + action + " " + "/".join(identifier_parts) def _compile_ui_resource( ctx: AnalysisContext, raw_file: Artifact, output: OutputArtifact, target_device: [None, str] = None, output_is_dir: bool = False) -> None: _run_ibtool( ctx = ctx, raw_file = raw_file, output = output, action_flags = ["--compile"], target_device = target_device, action_identifier = _ibtool_identifier("compile", raw_file), output_is_dir = output_is_dir, ) def _link_ui_resource( ctx: AnalysisContext, raw_file: Artifact, output: OutputArtifact, target_device: [None, str] = None, output_is_dir: bool = False) -> None: _run_ibtool( ctx = ctx, raw_file = raw_file, output = output, action_flags = ["--link"], target_device = target_device, action_identifier = _ibtool_identifier("link", raw_file), output_is_dir = output_is_dir, ) def _process_apple_resource_file_if_needed( ctx: AnalysisContext, file: Artifact, destination: AppleBundleDestination, destination_relative_path: [str, None], codesign_on_copy: bool = False, codesign_entitlements: Artifact | None = None, codesign_flags_override: list[str] | None = None) -> AppleBundlePart: output_dir = "_ProcessedResources" basename = paths.basename(file.short_path) output_is_contents_dir = False if basename.endswith(".plist") or basename.endswith(".stringsdict"): processed = ctx.actions.declare_output(paths.join(output_dir, file.short_path)) process_plist( ctx = ctx, input = file, output = processed.as_output(), action_id = destination_relative_path, ) elif basename.endswith(".storyboard"): if destination_relative_path: destination_relative_path = paths.replace_extension(destination_relative_path, ".storyboardc") compiled = ctx.actions.declare_output(paths.join(output_dir, paths.replace_extension(file.short_path, ".storyboardc")), dir = True) if get_is_watch_bundle(ctx): output_is_contents_dir = True _compile_ui_resource(ctx = ctx, raw_file = file, output = compiled.as_output(), target_device = "watch") processed = ctx.actions.declare_output(paths.join(output_dir, paths.replace_extension(file.short_path, "_linked_storyboard")), dir = True) _link_ui_resource(ctx = ctx, raw_file = compiled, output = processed.as_output(), target_device = "watch", output_is_dir = True) else: processed = compiled _compile_ui_resource(ctx, file, processed.as_output()) elif basename.endswith(".xib"): if destination_relative_path: destination_relative_path = paths.replace_extension(destination_relative_path, ".nib") processed = ctx.actions.declare_output(paths.join(output_dir, paths.replace_extension(file.short_path, ".nib"))) _compile_ui_resource(ctx, file, processed.as_output()) else: processed = file # When name is empty string only content of the directory will be copied, as opposed to the directory itself. # When name is `None`, directory or file will be copied as it is, without renaming. new_name = destination_relative_path if destination_relative_path else ("" if output_is_contents_dir else None) return AppleBundlePart(source = processed, destination = destination, new_name = new_name, codesign_on_copy = codesign_on_copy, codesign_entitlements = codesign_entitlements, codesign_flags_override = codesign_flags_override) # Returns a path relative to the _parent_ of the lproj dir. # For example, given a variant file with a short path of`XX/YY.lproj/ZZ` # it would return `YY.lproj/ZZ`. def _get_dest_subpath_for_variant_file(variant_file: Artifact) -> str: dir_name = _get_variant_dirname(variant_file) if not dir_name: fail("Variant files have to be in a directory with name ending in '.lproj' but `{}` was not.".format(variant_file.short_path)) file_name = paths.basename(variant_file.short_path) return paths.join(dir_name, file_name) def _get_variant_dirname(variant_file: Artifact) -> str | None: dir_name = paths.basename(paths.dirname(variant_file.short_path)) return dir_name if dir_name.endswith("lproj") else None

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