Skip to main content
Glama
codesign_bundle.py26.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. # pyre-strict import asyncio import importlib.resources import logging import os import shutil import subprocess import tempfile import uuid from contextlib import ExitStack from dataclasses import dataclass from enum import Enum from pathlib import Path from typing import Any, cast, Dict, List, Optional, Union from apple.tools.plistlib_utils import detect_format_and_load from .apple_platform import ApplePlatform from .codesign_command_factory import ( DefaultCodesignCommandFactory, DryRunCodesignCommandFactory, ICodesignCommandFactory, ) from .fast_adhoc import is_fast_adhoc_codesign_allowed, should_skip_adhoc_signing_path from .identity import CodeSigningIdentity from .info_plist_metadata import InfoPlistMetadata from .list_codesign_identities import IListCodesignIdentities from .prepare_code_signing_entitlements import prepare_code_signing_entitlements from .prepare_info_plist import prepare_info_plist from .provisioning_profile_diagnostics import ( interpret_provisioning_profile_diagnostics, META_IOS_BUILD_AND_RUN_ON_DEVICE_LINK, META_IOS_PROVISIONING_PROFILES_COMMAND, META_IOS_PROVISIONING_PROFILES_LINK, ) from .provisioning_profile_metadata import ProvisioningProfileMetadata from .provisioning_profile_selection import ( CodeSignProvisioningError, select_best_provisioning_profile, SelectedProvisioningProfileInfo, ) from .read_provisioning_profile_command_factory import ( DefaultReadProvisioningProfileCommandFactory, IReadProvisioningProfileCommandFactory, ) _default_read_provisioning_profile_command_factory = ( DefaultReadProvisioningProfileCommandFactory() ) _LOGGER: logging.Logger = logging.getLogger(__name__) @dataclass class CodesignedPath: path: Path """ Path relative to bundle root which needs to be codesigned """ entitlements: Optional[Path] """ Path to entitlements to be used when codesigning, relative to buck project """ flags: List[str] """ Flags to be passed to codesign command when codesigning this particular path """ extra_file_paths: Optional[List[Path]] """ Extra paths to be codesign. Applicable to dry-run codesigning only. """ def _log_codesign_identities( list_codesign_identities: IListCodesignIdentities, identities: List[CodeSigningIdentity], ) -> None: if len(identities) == 0: _LOGGER.warning("ZERO codesign identities available") _LOGGER.warning( f"Identities were retrieved by command: {list_codesign_identities.raw_command()}" ) else: _LOGGER.info("Listing available codesign identities") for identity in identities: _LOGGER.info( f" Subject Common Name: {identity.subject_common_name}, Fingerprint: {identity.fingerprint}" ) def _select_provisioning_profile( info_plist_metadata: InfoPlistMetadata, provisioning_profiles_dir: Path, entitlements_path: Optional[Path], platform: ApplePlatform, list_codesign_identities: IListCodesignIdentities, should_use_fast_provisioning_profile_parsing: bool, strict_provisioning_profile_search: bool, provisioning_profile_filter: Optional[str], log_file_path: Optional[Path] = None, ) -> SelectedProvisioningProfileInfo: read_provisioning_profile_command_factory = ( _default_read_provisioning_profile_command_factory ) identities = list_codesign_identities.list_codesign_identities() _log_codesign_identities(list_codesign_identities, identities) _LOGGER.info( f"Fast provisioning profile parsing enabled: {should_use_fast_provisioning_profile_parsing}" ) provisioning_profiles = [] if should_use_fast_provisioning_profile_parsing: provisioning_profiles = asyncio.run( _fast_read_provisioning_profiles_async( provisioning_profiles_dir, read_provisioning_profile_command_factory, ) ) else: provisioning_profiles = _read_provisioning_profiles( provisioning_profiles_dir, read_provisioning_profile_command_factory, ) if not provisioning_profiles: raise CodeSignProvisioningError( ( f"\n\nFailed to find any provisioning profiles. Please make sure to install required provisioning profiles and make sure they are located at '{provisioning_profiles_dir}'.\n\n" f"Execute `{META_IOS_PROVISIONING_PROFILES_COMMAND}` to download the profiles.\n" f"Please follow the wiki to build & run on device: {META_IOS_BUILD_AND_RUN_ON_DEVICE_LINK}.\n" f"Provisioning profiles for your app can also be downloaded from {META_IOS_PROVISIONING_PROFILES_LINK}.\n" ) ) entitlements = _read_entitlements_file(entitlements_path) selected_profile_info, mismatches = select_best_provisioning_profile( info_plist_metadata, identities, provisioning_profiles, entitlements, platform, strict_provisioning_profile_search, provisioning_profile_filter, ) if selected_profile_info is None: if not mismatches: raise RuntimeError( f"Expected diagnostics information for at least one mismatching provisioning profile when `{provisioning_profiles_dir}` directory is not empty." ) raise CodeSignProvisioningError( interpret_provisioning_profile_diagnostics( diagnostics=mismatches, bundle_id=info_plist_metadata.bundle_id, provisioning_profiles_dir=provisioning_profiles_dir, identities=identities, log_file_path=log_file_path, ) ) return selected_profile_info @dataclass class SigningContextWithProfileSelection: info_plist_source: Path info_plist_destination: Path info_plist_metadata: InfoPlistMetadata selected_profile_info: SelectedProvisioningProfileInfo @dataclass class AdhocSigningContext: codesign_identity: str profile_selection_context: Optional[SigningContextWithProfileSelection] def __init__( self, codesign_identity: Optional[str] = None, profile_selection_context: Optional[SigningContextWithProfileSelection] = None, ) -> None: self.codesign_identity = codesign_identity or "-" self.profile_selection_context = profile_selection_context def signing_context_with_profile_selection( info_plist_source: Path, info_plist_destination: Path, provisioning_profiles_dir: Path, entitlements_path: Optional[Path], platform: ApplePlatform, list_codesign_identities: IListCodesignIdentities, log_file_path: Optional[Path] = None, should_use_fast_provisioning_profile_parsing: bool = False, strict_provisioning_profile_search: bool = False, provisioning_profile_filter: Optional[str] = None, ) -> SigningContextWithProfileSelection: with open(info_plist_source, mode="rb") as info_plist_file: info_plist_metadata = InfoPlistMetadata.from_file(info_plist_file) selected_profile_info = _select_provisioning_profile( info_plist_metadata=info_plist_metadata, provisioning_profiles_dir=provisioning_profiles_dir, entitlements_path=entitlements_path, platform=platform, list_codesign_identities=list_codesign_identities, log_file_path=log_file_path, should_use_fast_provisioning_profile_parsing=should_use_fast_provisioning_profile_parsing, strict_provisioning_profile_search=strict_provisioning_profile_search, provisioning_profile_filter=provisioning_profile_filter, ) return SigningContextWithProfileSelection( info_plist_source, info_plist_destination, info_plist_metadata, selected_profile_info, ) # IMPORTANT: This enum is a part of incremental API, amend carefully. class CodesignConfiguration(str, Enum): fastAdhoc = "fast-adhoc" dryRun = "dry-run" def codesign_bundle( bundle_path: CodesignedPath, signing_context: Union[AdhocSigningContext, SigningContextWithProfileSelection], platform: ApplePlatform, codesign_on_copy_paths: List[CodesignedPath], codesign_tool: Optional[Path] = None, codesign_configuration: Optional[CodesignConfiguration] = None, ) -> None: codesign_on_copy_paths = sorted( codesign_on_copy_paths, key=lambda codesigned_path: codesigned_path.path, # Paths must be signed inside out (i.e., deepest first) reverse=True, ) with tempfile.TemporaryDirectory() as tmp_dir: if isinstance(signing_context, SigningContextWithProfileSelection): selection_profile_context = signing_context elif isinstance(signing_context, AdhocSigningContext): selection_profile_context = signing_context.profile_selection_context else: raise RuntimeError( f"Unexpected type of signing context `{type(signing_context)}`" ) if selection_profile_context: bundle_path_with_prepared_entitlements = ( _prepare_entitlements_and_info_plist( bundle_path=bundle_path, platform=platform, signing_context=selection_profile_context, tmp_dir=tmp_dir, ) ) selected_identity_fingerprint = ( selection_profile_context.selected_profile_info.identity.fingerprint ) else: if not isinstance(signing_context, AdhocSigningContext): raise AssertionError( f"Expected `AdhocSigningContext`, got `{type(signing_context)}` instead." ) if signing_context.profile_selection_context: raise AssertionError( "Expected no profile selection context in `AdhocSigningContext` when `selection_profile_context` is `None`." ) bundle_path_with_prepared_entitlements = bundle_path selected_identity_fingerprint = signing_context.codesign_identity if codesign_configuration is CodesignConfiguration.dryRun: if codesign_tool is None: raise RuntimeError( "Expected codesign tool not to be the default one when dry run codesigning is requested." ) _dry_codesign_everything( root=bundle_path_with_prepared_entitlements, codesign_on_copy_paths=codesign_on_copy_paths, identity_fingerprint=selected_identity_fingerprint, tmp_dir=tmp_dir, codesign_tool=codesign_tool, platform=platform, ) else: fast_adhoc_signing_enabled = ( codesign_configuration is CodesignConfiguration.fastAdhoc and is_fast_adhoc_codesign_allowed() ) _LOGGER.info(f"Fast adhoc signing enabled: {fast_adhoc_signing_enabled}") _codesign_everything( root=bundle_path_with_prepared_entitlements, codesign_on_copy_paths=codesign_on_copy_paths, identity_fingerprint=selected_identity_fingerprint, tmp_dir=tmp_dir, codesign_command_factory=DefaultCodesignCommandFactory(codesign_tool), platform=platform, fast_adhoc_signing=fast_adhoc_signing_enabled, ) def _prepare_entitlements_and_info_plist( bundle_path: CodesignedPath, platform: ApplePlatform, signing_context: SigningContextWithProfileSelection, tmp_dir: str, ) -> CodesignedPath: info_plist_metadata = signing_context.info_plist_metadata selected_profile = signing_context.selected_profile_info.profile prepared_entitlements_path = prepare_code_signing_entitlements( bundle_path.entitlements, info_plist_metadata.bundle_id, selected_profile, tmp_dir, ) prepared_info_plist_path = prepare_info_plist( signing_context.info_plist_source, info_plist_metadata, selected_profile, tmp_dir, ) os.replace( prepared_info_plist_path, bundle_path.path / signing_context.info_plist_destination, ) shutil.copy2( selected_profile.file_path, bundle_path.path / platform.embedded_provisioning_profile_path(), ) return CodesignedPath( path=bundle_path.path, entitlements=prepared_entitlements_path, flags=bundle_path.flags, extra_file_paths=None, ) async def _fast_read_provisioning_profiles_async( dirpath: Path, read_provisioning_profile_command_factory: IReadProvisioningProfileCommandFactory, ) -> List[ProvisioningProfileMetadata]: tasks = [] for f in os.listdir(dirpath): if f.endswith(".mobileprovision") or f.endswith(".provisionprofile"): filepath = dirpath / f tasks.append( _provisioning_profile_from_file_path_async( filepath, read_provisioning_profile_command_factory, should_use_fast_provisioning_profile_parsing=True, ) ) results = await asyncio.gather(*tasks) return cast(List[ProvisioningProfileMetadata], results) async def _provisioning_profile_from_file_path_async( path: Path, read_provisioning_profile_command_factory: IReadProvisioningProfileCommandFactory, should_use_fast_provisioning_profile_parsing: bool, ) -> ProvisioningProfileMetadata: loop = asyncio.get_running_loop() return await loop.run_in_executor( None, _provisioning_profile_from_file_path, path, read_provisioning_profile_command_factory, should_use_fast_provisioning_profile_parsing, ) def _read_provisioning_profiles( dirpath: Path, read_provisioning_profile_command_factory: IReadProvisioningProfileCommandFactory, ) -> List[ProvisioningProfileMetadata]: return [ _provisioning_profile_from_file_path( dirpath / f, read_provisioning_profile_command_factory, should_use_fast_provisioning_profile_parsing=False, ) for f in os.listdir(dirpath) if (f.endswith(".mobileprovision") or f.endswith(".provisionprofile")) ] def _provisioning_profile_from_file_path( path: Path, read_provisioning_profile_command_factory: IReadProvisioningProfileCommandFactory, should_use_fast_provisioning_profile_parsing: bool, ) -> ProvisioningProfileMetadata: if should_use_fast_provisioning_profile_parsing: # Provisioning profiles have a plist embedded in them that we can extract directly. # This is much faster than calling an external command like openssl. with open(path, "rb") as f: content = f.read() start_index = content.find(b"<plist") end_index = content.find(b"</plist>", start_index) + len(b"</plist>") if start_index >= 0 and end_index >= 0: plist_data = content[start_index:end_index] return ProvisioningProfileMetadata.from_provisioning_profile_file_content( path, plist_data ) else: _LOGGER.warning( f"Failed to find plist in provisioning profile at {path}. Falling back to slow parsing." ) # Fallback to slow parsing if fast parsing is disabled or fails return _provisioning_profile_from_file_path_using_factory( path, read_provisioning_profile_command_factory ) def _provisioning_profile_from_file_path_using_factory( path: Path, read_provisioning_profile_command_factory: IReadProvisioningProfileCommandFactory, ) -> ProvisioningProfileMetadata: output: bytes = subprocess.check_output( read_provisioning_profile_command_factory.read_provisioning_profile_command( path ), stderr=subprocess.DEVNULL, ) return ProvisioningProfileMetadata.from_provisioning_profile_file_content( path, output ) def _read_entitlements_file(path: Optional[Path]) -> Optional[Dict[str, Any]]: if not path: return None with open(path, mode="rb") as f: return detect_format_and_load(f) def _dry_codesign_everything( root: CodesignedPath, codesign_on_copy_paths: List[CodesignedPath], identity_fingerprint: str, tmp_dir: str, codesign_tool: Path, platform: ApplePlatform, ) -> None: codesign_command_factory = DryRunCodesignCommandFactory(codesign_tool) codesign_on_copy_directory_paths = [ p for p in codesign_on_copy_paths if p.path.is_dir() ] # First sign codesign-on-copy directory paths _codesign_paths( paths=codesign_on_copy_directory_paths, identity_fingerprint=identity_fingerprint, tmp_dir=tmp_dir, codesign_command_factory=codesign_command_factory, platform=platform, ) # Dry codesigning creates a .plist inside every directory it signs. # That approach doesn't work for files so those files are written into .plist for root bundle. codesign_on_copy_file_paths = [ p.path.relative_to(root.path) for p in codesign_on_copy_paths if p.path.is_file() ] if root.extra_file_paths: raise RuntimeError( f"Root path contains extra file paths: `{root.extra_file_paths}`" ) root_with_extra_paths = CodesignedPath( path=root.path, entitlements=root.entitlements, flags=root.flags, extra_file_paths=codesign_on_copy_file_paths, ) # Lastly sign whole bundle _codesign_paths( paths=[root_with_extra_paths], identity_fingerprint=identity_fingerprint, tmp_dir=tmp_dir, codesign_command_factory=codesign_command_factory, platform=platform, ) def _codesign_everything( root: CodesignedPath, codesign_on_copy_paths: List[CodesignedPath], identity_fingerprint: str, tmp_dir: str, codesign_command_factory: ICodesignCommandFactory, platform: ApplePlatform, fast_adhoc_signing: bool, ) -> None: # First sign codesign-on-copy paths codesign_on_copy_filtered_paths = _filter_out_fast_adhoc_paths( paths=codesign_on_copy_paths, identity_fingerprint=identity_fingerprint, platform=platform, fast_adhoc_signing=fast_adhoc_signing, ) # If we have > 1 paths to sign (including root bundle), access keychain first to avoid user playing whack-a-mole # with permission grant dialog windows. if codesign_on_copy_filtered_paths: obtain_keychain_permissions( identity_fingerprint, tmp_dir, codesign_command_factory ) _codesign_paths( codesign_on_copy_filtered_paths, identity_fingerprint, tmp_dir, codesign_command_factory, platform, ) # Lastly sign whole bundle root_filtered_paths = _filter_out_fast_adhoc_paths( paths=[root], identity_fingerprint=identity_fingerprint, platform=platform, fast_adhoc_signing=fast_adhoc_signing, ) _codesign_paths( root_filtered_paths, identity_fingerprint, tmp_dir, codesign_command_factory, platform, ) @dataclass class ParallelProcess: process: subprocess.Popen[bytes] stdout_path: Optional[str] stderr_path: str def check_result(self) -> None: if self.process.returncode == 0: return with ExitStack() as stack: stderr = stack.enter_context(open(self.stderr_path, encoding="utf8")) stderr_string = f"\nstderr:\n{stderr.read()}\n" stdout = ( stack.enter_context(open(self.stdout_path, encoding="utf8")) if self.stdout_path else None ) stdout_string = f"\nstdout:\n{stdout.read()}\n" if stdout else "" raise RuntimeError(f"{stdout_string}{stderr_string}") def _spawn_process( command: List[Union[str, Path]], tmp_dir: str, stack: ExitStack, pipe_stdout: bool = False, ) -> ParallelProcess: if pipe_stdout: stdout_path = None stdout = subprocess.PIPE else: stdout_path = os.path.join(tmp_dir, uuid.uuid4().hex) stdout = stack.enter_context(open(stdout_path, "w")) stderr_path = os.path.join(tmp_dir, uuid.uuid4().hex) stderr = stack.enter_context(open(stderr_path, "w")) _LOGGER.info(f"Executing command: {command}") process = subprocess.Popen(command, stdout=stdout, stderr=stderr) return ParallelProcess( process, stdout_path, stderr_path, ) def _spawn_codesign_process( path: CodesignedPath, identity_fingerprint: str, tmp_dir: str, codesign_command_factory: ICodesignCommandFactory, stack: ExitStack, ) -> ParallelProcess: command = codesign_command_factory.codesign_command( path.path, identity_fingerprint, path.entitlements, path.flags, path.extra_file_paths, ) return _spawn_process(command=command, tmp_dir=tmp_dir, stack=stack) def _codesign_paths_serially( paths: List[CodesignedPath], identity_fingerprint: str, tmp_dir: str, codesign_command_factory: ICodesignCommandFactory, platform: ApplePlatform, ) -> None: with ExitStack() as stack: for path in paths: p = _spawn_codesign_process( path=path, identity_fingerprint=identity_fingerprint, tmp_dir=tmp_dir, codesign_command_factory=codesign_command_factory, stack=stack, ) p.process.wait() p.check_result() def _codesign_paths_in_parallel( paths: List[CodesignedPath], identity_fingerprint: str, tmp_dir: str, codesign_command_factory: ICodesignCommandFactory, platform: ApplePlatform, ) -> None: """Codesigns several paths in parallel.""" processes: List[ParallelProcess] = [] with ExitStack() as stack: for path in paths: process = _spawn_codesign_process( path=path, identity_fingerprint=identity_fingerprint, tmp_dir=tmp_dir, codesign_command_factory=codesign_command_factory, stack=stack, ) processes.append(process) for p in processes: p.process.wait() for p in processes: p.check_result() def _can_codesign_paths_in_parallel(codesigned_paths: List[CodesignedPath]) -> bool: # To enable parallel signing, there must be no nesting of any codesigned paths, # as codesigning must be performed "inside out" - deeper items signed first, # as parent items need to seal the contained items as part of their signature. # # To detect nesting, we reverse sort all paths and only need to check # neighboring elements. For example, imagine the following elements: # `a/b/c` # `a/b` # `b` # `c` # # For each element, check if the element is a prefix of the previous element. # In the example above, checking if `a/b` is a prefix of `a/b/c` means its # unsafe to codesign in parallel. paths = sorted([str(path.path) for path in codesigned_paths], reverse=True) for index, current_path in enumerate(paths): if index == 0: continue previous_path = paths[index - 1] if previous_path.startswith(current_path): _LOGGER.warning( f"Found overlapping codesigned paths: {previous_path}, {current_path}" ) return False return True def _codesign_paths( paths: List[CodesignedPath], identity_fingerprint: str, tmp_dir: str, codesign_command_factory: ICodesignCommandFactory, platform: ApplePlatform, ) -> None: can_codesign_in_parallel = _can_codesign_paths_in_parallel(paths) signing_function = ( _codesign_paths_in_parallel if can_codesign_in_parallel else _codesign_paths_serially ) signing_function( paths=paths, identity_fingerprint=identity_fingerprint, tmp_dir=tmp_dir, codesign_command_factory=codesign_command_factory, platform=platform, ) def _filter_out_fast_adhoc_paths( paths: List[CodesignedPath], identity_fingerprint: str, platform: ApplePlatform, fast_adhoc_signing: bool, ) -> List[CodesignedPath]: if not fast_adhoc_signing: return paths # TODO(T149863217): Make skip checks run in parallel, they're usually fast (~15ms) # but if we have many of them (e.g., 30+ frameworks), it can add about ~0.5s.' return [ p for p in paths if not should_skip_adhoc_signing_path( p.path, identity_fingerprint, p.entitlements, platform ) ] def obtain_keychain_permissions( identity_fingerprint: str, tmp_dir: str, codesign_command_factory: ICodesignCommandFactory, ) -> None: with ExitStack() as stack, importlib.resources.path( __package__, "dummy_binary_for_signing" ) as dummy_binary_path: # Copy the binary to avoid races vs other bundling actions dummy_binary_copied = os.path.join(tmp_dir, "dummy_binary_for_signing") shutil.copyfile(dummy_binary_path, dummy_binary_copied, follow_symlinks=True) p = _spawn_codesign_process( path=CodesignedPath( path=Path(dummy_binary_copied), entitlements=None, flags=[], extra_file_paths=None, ), identity_fingerprint=identity_fingerprint, tmp_dir=tmp_dir, codesign_command_factory=codesign_command_factory, stack=stack, ) p.process.wait() p.check_result()

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