Skip to content

Commit fabe8d8

Browse files
authored
feat(ios): parse the archive plist's CreationDate field (#475)
1 parent 416af31 commit fabe8d8

File tree

6 files changed

+37
-2
lines changed

6 files changed

+37
-2
lines changed

src/launchpad/api/update_api_models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class AppleAppInfo(BaseModel):
4747
profile_expiration_date: Optional[str] = None
4848
certificate_expiration_date: Optional[str] = None
4949
missing_dsym_binaries: Optional[List[str]] = None
50-
# TODO(EME-423): add "date_built" field once exposed in 'AppleAppInfo'
50+
build_date: Optional[str] = None
5151

5252

5353
class AndroidAppInfo(BaseModel):

src/launchpad/artifact_processor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -464,7 +464,6 @@ def _get_artifact_type(artifact: Artifact) -> ArtifactType:
464464

465465
apple_app_info = None
466466
if isinstance(app_info, AppleAppInfo):
467-
# TODO(EME-423): add "date_built" field once exposed in 'AppleAppInfo'
468467
apple_app_info = AppleAppInfoModel(
469468
is_simulator=app_info.is_simulator,
470469
codesigning_type=app_info.codesigning_type,
@@ -475,6 +474,7 @@ def _get_artifact_type(artifact: Artifact) -> ArtifactType:
475474
profile_expiration_date=app_info.profile_expiration_date,
476475
certificate_expiration_date=app_info.certificate_expiration_date,
477476
missing_dsym_binaries=app_info.missing_dsym_binaries,
477+
build_date=app_info.build_date,
478478
)
479479

480480
android_app_info = None

src/launchpad/artifacts/apple/zipped_xcarchive.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ def __init__(self, path: Path) -> None:
6666
self._extract_dir = self._zip_provider.extract_to_temp_directory()
6767
self._app_bundle_path: Path | None = None
6868
self._plist: dict[str, Any] | None = None
69+
self._archive_plist: dict[str, Any] | None = None
6970
self._provisioning_profile: dict[str, Any] | None = None
7071
self._dsym_info: dict[str, DsymInfo] | None = None
7172

@@ -89,6 +90,30 @@ def get_plist(self) -> dict[str, Any]:
8990
except Exception as e:
9091
raise RuntimeError("Failed to parse Info.plist") from e
9192

93+
@sentry_sdk.trace
94+
def get_archive_plist(self) -> dict[str, Any] | None:
95+
"""Get the archive-level Info.plist (not the app bundle's Info.plist)."""
96+
if self._archive_plist is not None:
97+
return self._archive_plist
98+
99+
xcarchive_dirs = list(self._extract_dir.glob("*.xcarchive"))
100+
if not xcarchive_dirs:
101+
logger.debug(f"No .xcarchive directory found in {self._extract_dir}")
102+
return None
103+
104+
xcarchive_dir = xcarchive_dirs[0]
105+
plist_path = xcarchive_dir / "Info.plist"
106+
107+
try:
108+
with open(plist_path, "rb") as f:
109+
plist_data = plistlib.load(f)
110+
111+
self._archive_plist = plist_data
112+
return plist_data
113+
except Exception:
114+
logger.debug(f"Failed to parse archive Info.plist at {plist_path}", exc_info=True)
115+
return None
116+
92117
@sentry_sdk.trace
93118
def get_app_icon(self) -> bytes | None:
94119
"""Get the primary app icon, decoded from crushed PNG format."""

src/launchpad/size/analyzers/apple.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,12 @@ def _extract_app_info(self, xcarchive: ZippedXCArchive) -> AppleAppInfo:
283283
profile_expiration_date = expiration_date.isoformat()
284284
certificate_expiration_date = self._extract_certificate_expiration_date(provisioning_profile)
285285

286+
build_date = None
287+
archive_plist = xcarchive.get_archive_plist()
288+
if archive_plist:
289+
creation_date = archive_plist.get("CreationDate")
290+
build_date = creation_date.isoformat() if creation_date else None
291+
286292
supported_platforms = plist.get("CFBundleSupportedPlatforms", [])
287293
is_simulator = "iphonesimulator" in supported_platforms or plist.get("DTPlatformName") == "iphonesimulator"
288294

@@ -319,6 +325,7 @@ def _extract_app_info(self, xcarchive: ZippedXCArchive) -> AppleAppInfo:
319325
minimum_os_version=plist.get("MinimumOSVersion", "Unknown"),
320326
supported_platforms=supported_platforms,
321327
sdk_version=plist.get("DTSDKName"),
328+
build_date=build_date,
322329
is_simulator=is_simulator,
323330
codesigning_type=codesigning_type,
324331
profile_name=profile_name,

src/launchpad/size/models/apple.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ class AppleAppInfo(BaseAppInfo):
5050
minimum_os_version: str = Field(..., description="Minimum app version")
5151
supported_platforms: List[str] = Field(default_factory=list, description="Supported platforms")
5252
sdk_version: str | None = Field(None, description="App SDK version used for build")
53+
build_date: str | None = Field(None, description="Date when the archive was built (ISO format)")
5354
is_simulator: bool = Field(False, description="If the app is a simulator build")
5455
codesigning_type: str | None = Field(
5556
None, description="Type of codesigning used (development, adhoc, appstore, enterprise)"

tests/unit/test_apple_basic_info.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ def test_basic_info(self, hackernews_xcarchive: Path) -> None:
2020
assert basic_info.minimum_os_version == "17.5"
2121
assert basic_info.supported_platforms == ["iPhoneOS"]
2222
assert basic_info.sdk_version == "iphoneos18.4"
23+
assert basic_info.build_date is not None
24+
assert basic_info.build_date == "2025-05-19T16:15:12"
2325
assert basic_info.is_simulator is False
2426
assert basic_info.codesigning_type == "development"
2527
assert basic_info.is_code_signature_valid is True

0 commit comments

Comments
 (0)