Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 49 additions & 37 deletions src/launchpad/size/analyzers/apple.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
from launchpad.size.insights.insight import InsightsInput
from launchpad.size.models.common import APPLE_ANALYSIS_VERSION
from launchpad.size.models.treemap import FlaggedInsight
from launchpad.size.symbols.macho_symbol_sizes import MachOSymbolSizes
from launchpad.size.symbols.macho_symbol_sizes import MachOSymbolSizes, SymbolSize
from launchpad.size.treemap.treemap_builder import TreemapBuilder
from launchpad.size.utils.apple_bundle_size import calculate_bundle_sizes
from launchpad.size.utils.file_analysis import analyze_apple_files
Expand Down Expand Up @@ -591,42 +591,11 @@ def _analyze_binary(
dwarf_relocations = None

if dwarf_binary_path:
dsym_config = lief.MachO.ParserConfig()
dsym_config.parse_dyld_exports = False
dsym_config.parse_dyld_bindings = False
dsym_config.parse_dyld_rebases = False

with open(dwarf_binary_path, "rb") as f:
dwarf_fat_binary = lief.MachO.parse(f, dsym_config) # type: ignore
if dwarf_fat_binary:
# Build a map of architecture name -> dSYM slice index
dsym_arch_map: Dict[str, int] = {}
for i, dsym_slice in enumerate(dwarf_fat_binary):
dsym_arch_name = get_cpu_type_name(dsym_slice.header.cpu_type)
dsym_arch_map[dsym_arch_name] = i

# Symbolicate each architecture slice
for arch_slice in architecture_slices:
if arch_slice.arch_name in dsym_arch_map:
dsym_index = dsym_arch_map[arch_slice.arch_name]
dsym_binary = dwarf_fat_binary.at(dsym_index)
slice_symbol_sizes = MachOSymbolSizes(dsym_binary).get_symbol_sizes()
arch_slice.symbol_info = SymbolInfo.from_symbol_sizes(
symbol_sizes=slice_symbol_sizes,
)
logger.debug(
f"Symbolicated {arch_slice.arch_name} slice with {len(slice_symbol_sizes)} symbols"
)
else:
logger.warning(f"No dSYM slice found for architecture {arch_slice.arch_name}")

else:
logger.error(
"size.apple.skip_symbol_analysis.dwarf_binary_parse_failed",
extra={
"binary_name": binary_info.name,
},
)
for arch_slice, slice_symbol_sizes in self._extract_dsym_symbols(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that there is a tradeoff here where we're eagerly producing the symbols for multiple architectures up front rather than on-demand as we were previously doing. It's possible that that could have worse memory use performance, but from checking how prod data looks I don't think it will matter today, and the change is definitely helpful for large single-arch binaries.

dwarf_binary_path, binary_info.name, architecture_slices
):
arch_slice.symbol_info = SymbolInfo.from_symbol_sizes(symbol_sizes=slice_symbol_sizes)
logger.debug(f"Symbolicated {arch_slice.arch_name} slice with {len(slice_symbol_sizes)} symbols")

if binary_info.relocations_path and os.getenv("LAUNCHPAD_ENV") == "development":
with sentry_sdk.traces.start_span(name="dwarf_relocations.parse", attributes={"sentry.op": "parse"}):
Expand All @@ -653,6 +622,49 @@ def _analyze_binary(
strippable_symbols_size=strippable_symbols_size,
)

def _extract_dsym_symbols(
self, dwarf_binary_path: Path, binary_name: str, architecture_slices: list[ArchitectureSlice]
) -> list[tuple[ArchitectureSlice, list[SymbolSize]]]:
"""Extract symbols without retaining the native dSYM parser after return.

Return a list rather than yielding so parser references leave scope
when this method returns.
"""
dsym_config = lief.MachO.ParserConfig()
dsym_config.parse_dyld_exports = False
dsym_config.parse_dyld_bindings = False
dsym_config.parse_dyld_rebases = False

with open(dwarf_binary_path, "rb") as f:
dwarf_fat_binary = lief.MachO.parse(f, dsym_config) # type: ignore
symbols_by_arch: list[tuple[ArchitectureSlice, list[SymbolSize]]] = []
if dwarf_fat_binary:
# Build a map of architecture name -> dSYM slice index
dsym_arch_map: Dict[str, int] = {}
for i, dsym_slice in enumerate(dwarf_fat_binary):
dsym_arch_name = get_cpu_type_name(dsym_slice.header.cpu_type)
dsym_arch_map[dsym_arch_name] = i

# Extract symbols for each architecture slice
for arch_slice in architecture_slices:
if arch_slice.arch_name in dsym_arch_map:
dsym_index = dsym_arch_map[arch_slice.arch_name]
dsym_binary = dwarf_fat_binary.at(dsym_index)
slice_symbol_sizes = MachOSymbolSizes(dsym_binary).get_symbol_sizes()
symbols_by_arch.append((arch_slice, slice_symbol_sizes))
else:
logger.warning(f"No dSYM slice found for architecture {arch_slice.arch_name}")

else:
logger.error(
"size.apple.skip_symbol_analysis.dwarf_binary_parse_failed",
extra={
"binary_name": binary_name,
},
)

return symbols_by_arch

def _extract_binary_metadata(self, binary_path: Path) -> _BinaryMetadata:
with open(binary_path, "rb") as f:
config = lief.MachO.ParserConfig()
Expand Down
15 changes: 5 additions & 10 deletions src/launchpad/utils/apple/code_signature_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@ def __init__(self, archive: ZippedXCArchive) -> None:
self.plist = self.archive.get_plist()
self.executable_name: str = self.archive.get_plist().get("CFBundleExecutable", "")
self.app_root: SafeDirectory = self.archive.get_app_bundle_path()
self.macho_parser: Optional[MachOParser] = None

def validate(self) -> Tuple[bool, List[str]]:
"""Validate the code signature.
Expand Down Expand Up @@ -126,11 +125,10 @@ def _validate_executable(self) -> BinaryCheckResult:
if fat_binary is None:
raise ValueError("Failed to parse binary")

self.macho_parser = MachOParser(fat_binary.at(0))
if self.macho_parser.is_encrypted():
macho_parser = MachOParser(fat_binary.at(0))
if macho_parser.is_encrypted():
return BinaryCheckResult(valid=False)

return self._check_binary()
return self._check_binary(macho_parser)

def _validate_info_plist(self, binary_hashes: BinaryCheckResult) -> bool:
"""Validate the Info.plist."""
Expand Down Expand Up @@ -254,12 +252,9 @@ def _check_file(
logger.debug(f"File {file_path} is not in the files2 array")
errors.append(f"file added: {file_path}")

def _check_binary(self) -> BinaryCheckResult:
def _check_binary(self, macho_parser: MachOParser) -> BinaryCheckResult:
"""Check the binary."""
if not self.macho_parser:
raise ValueError("MachO parser not initialized")

code_signature = self.macho_parser.parse_code_signature()
code_signature = macho_parser.parse_code_signature()
if not code_signature:
logger.info("No code signature found")
return BinaryCheckResult(valid=False, error_message="No code signature found")
Expand Down
Loading