Skip to content
Open
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
10 changes: 8 additions & 2 deletions src/launchpad/api/update_api_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ class PutSizePending(BaseModel):
]


class AppleAppInfo(BaseModel):
class BaseAppInfo(BaseModel):
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added this base class to reduce duplication of the cli_version. Can remove if it seems redundant.

cli_version: Optional[str] = Field(None, description="sentry-cli version used for uploading")


class AppleAppInfo(BaseAppInfo):
is_simulator: bool
codesigning_type: Optional[str] = None
profile_name: Optional[str] = None
Expand All @@ -48,10 +52,12 @@ class AppleAppInfo(BaseModel):
certificate_expiration_date: Optional[str] = None
missing_dsym_binaries: Optional[List[str]] = None
build_date: Optional[str] = None
fastlane_plugin_version: Optional[str] = Field(None, description="Fastlane plugin version used for uploading")


class AndroidAppInfo(BaseModel):
class AndroidAppInfo(BaseAppInfo):
has_proguard_mapping: bool
gradle_plugin_version: Optional[str] = Field(None, description="Gradle plugin version used for uploading")


class UpdateData(BaseModel):
Expand Down
4 changes: 4 additions & 0 deletions src/launchpad/artifact_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -442,12 +442,16 @@ def _get_artifact_type(artifact: Artifact) -> ArtifactType:
certificate_expiration_date=app_info.certificate_expiration_date,
missing_dsym_binaries=app_info.missing_dsym_binaries,
build_date=app_info.build_date,
cli_version=app_info.cli_version,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

cli_version could alternatively be moved to update_data to reduce duplication. thoughts?

fastlane_plugin_version=app_info.fastlane_plugin_version,
)

android_app_info = None
if isinstance(app_info, AndroidAppInfo):
android_app_info = AndroidAppInfoModel(
has_proguard_mapping=app_info.has_proguard_mapping,
cli_version=app_info.cli_version,
gradle_plugin_version=app_info.gradle_plugin_version,
)

update_data = UpdateData(
Expand Down
5 changes: 5 additions & 0 deletions src/launchpad/size/analyzers/android.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from launchpad.size.utils.android_bundle_size import calculate_apk_download_size, calculate_apk_install_size
from launchpad.utils.file_utils import calculate_file_hash
from launchpad.utils.logging import get_logger
from launchpad.utils.metadata_extractor import extract_metadata_from_zip

logger = get_logger(__name__)

Expand All @@ -50,12 +51,16 @@ def preprocess(self, artifact: AndroidArtifact) -> AndroidAppInfo:
manifest_dict = artifact.get_manifest().model_dump()
has_proguard_mapping = artifact.get_dex_mapping() is not None

metadata = extract_metadata_from_zip(artifact.path)

self.app_info = AndroidAppInfo(
name=manifest_dict["application"]["label"] or "Unknown",
version=manifest_dict["version_name"] or "Unknown",
build=manifest_dict["version_code"] or "Unknown",
app_id=manifest_dict["package_name"],
has_proguard_mapping=has_proguard_mapping,
cli_version=metadata.cli_version,
gradle_plugin_version=metadata.gradle_plugin_version,
)

return self.app_info
Expand Down
5 changes: 5 additions & 0 deletions src/launchpad/size/analyzers/apple.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
from launchpad.utils.apple.code_signature_validator import CodeSignatureValidator
from launchpad.utils.file_utils import get_file_size, to_nearest_block_size
from launchpad.utils.logging import get_logger
from launchpad.utils.metadata_extractor import extract_metadata_from_zip

from ..models.apple import (
AppleAnalysisResults,
Expand Down Expand Up @@ -333,6 +334,8 @@ def _extract_app_info(self, xcarchive: ZippedXCArchive) -> AppleAppInfo:
binaries = xcarchive.get_all_binary_paths()
missing_dsym_binaries = [b.name for b in binaries if b.dsym_path is None]

metadata = extract_metadata_from_zip(xcarchive.path)

return AppleAppInfo(
name=app_name,
app_id=plist.get("CFBundleIdentifier", "unknown.bundle.id"),
Expand All @@ -354,6 +357,8 @@ def _extract_app_info(self, xcarchive: ZippedXCArchive) -> AppleAppInfo:
primary_icon_name=primary_icon_name,
alternate_icon_names=alternate_icon_names,
missing_dsym_binaries=missing_dsym_binaries,
cli_version=metadata.cli_version,
fastlane_plugin_version=metadata.fastlane_plugin_version,
)

def _get_profile_type(self, profile_data: dict[str, Any]) -> Tuple[str, str]:
Expand Down
1 change: 1 addition & 0 deletions src/launchpad/size/models/android.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class AndroidAppInfo(BaseAppInfo):
model_config = ConfigDict(frozen=True)

has_proguard_mapping: bool = Field(default=False, description="Whether the app has a proguard mapping file")
gradle_plugin_version: str | None = Field(None, description="Gradle plugin version used for uploading")


class AndroidAnalysisResults(BaseAnalysisResults):
Expand Down
1 change: 1 addition & 0 deletions src/launchpad/size/models/apple.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ class AppleAppInfo(BaseAppInfo):
missing_dsym_binaries: List[str] = Field(
default_factory=list, description="List of binary names that don't have corresponding dSYM files"
)
fastlane_plugin_version: str | None = Field(None, description="Fastlane plugin version used for uploading")


class AppleInsightResults(BaseModel):
Expand Down
1 change: 1 addition & 0 deletions src/launchpad/size/models/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class BaseAppInfo(BaseModel):
version: str = Field(..., description="App version")
build: str = Field(..., description="Build number")
app_id: str = Field(..., description="App ID (bundle id on iOS, package name on Android)")
cli_version: str | None = Field(None, description="sentry-cli version used for uploading")


class FileAnalysis(BaseModel):
Expand Down
67 changes: 67 additions & 0 deletions src/launchpad/utils/metadata_extractor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import zipfile

from pathlib import Path
from typing import Dict, Optional

from launchpad.utils.logging import get_logger

logger = get_logger(__name__)

METADATA_FILENAME = ".sentry-cli-metadata.txt"


class ToolingMetadata:
def __init__(
self,
cli_version: Optional[str] = None,
fastlane_plugin_version: Optional[str] = None,
gradle_plugin_version: Optional[str] = None,
):
self.cli_version = cli_version
self.fastlane_plugin_version = fastlane_plugin_version
self.gradle_plugin_version = gradle_plugin_version


def extract_metadata_from_zip(zip_path: Path) -> ToolingMetadata:
try:
with zipfile.ZipFile(zip_path, "r") as zf:
# Only look for .sentry-cli-metadata.txt in the root of the zip
if METADATA_FILENAME not in zf.namelist():
logger.debug(f"No {METADATA_FILENAME} found in root of {zip_path}")
return ToolingMetadata()

logger.debug(f"Found metadata file: {METADATA_FILENAME}")

with zf.open(METADATA_FILENAME) as f:
content = f.read().decode("utf-8")
return _parse_metadata_content(content)

except Exception as e:
logger.warning(f"Failed to extract metadata from {zip_path}: {e}")
return ToolingMetadata()


def _parse_metadata_content(content: str) -> ToolingMetadata:
"""Expected format:
sentry-cli-version: 2.58.2
sentry-fastlane-plugin: 1.2.3
sentry-gradle-plugin: 4.12.0
"""
metadata: Dict[str, str] = {}

for line in content.strip().split("\n"):
line = line.strip()
if not line or ":" not in line:
continue

key, value = line.split(":", 1)
key = key.strip()
value = value.strip()

metadata[key] = value

return ToolingMetadata(
cli_version=metadata.get("sentry-cli-version"),
fastlane_plugin_version=metadata.get("sentry-fastlane-plugin"),
gradle_plugin_version=metadata.get("sentry-gradle-plugin"),
)
69 changes: 69 additions & 0 deletions tests/unit/utils/test_metadata_extractor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import tempfile
import zipfile

from pathlib import Path

from launchpad.utils.metadata_extractor import (
ToolingMetadata,
_parse_metadata_content,
extract_metadata_from_zip,
)


class TestParseMetadataContent:
def test_parse_all_fields(self):
content = """sentry-cli-version: 2.58.2
sentry-fastlane-plugin: 1.2.3
sentry-gradle-plugin: 4.12.0"""
metadata = _parse_metadata_content(content)
assert metadata.cli_version == "2.58.2"
assert metadata.fastlane_plugin_version == "1.2.3"
assert metadata.gradle_plugin_version == "4.12.0"

def test_parse_empty_content(self):
content = ""
metadata = _parse_metadata_content(content)
assert metadata.cli_version is None
assert metadata.fastlane_plugin_version is None
assert metadata.gradle_plugin_version is None


class TestExtractMetadataFromZip:
def test_extract_from_zip_root(self):
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tf:
try:
with zipfile.ZipFile(tf.name, "w") as zf:
zf.writestr(
".sentry-cli-metadata.txt",
"sentry-cli-version: 2.58.2\nsentry-fastlane-plugin: 1.2.3\nsentry-gradle-plugin: 4.12.0",
)
zf.writestr("some-file.txt", "content")

metadata = extract_metadata_from_zip(Path(tf.name))
assert metadata.cli_version == "2.58.2"
assert metadata.fastlane_plugin_version == "1.2.3"
assert metadata.gradle_plugin_version == "4.12.0"
finally:
Path(tf.name).unlink()

def test_extract_when_missing(self):
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tf:
try:
with zipfile.ZipFile(tf.name, "w") as zf:
zf.writestr("some-file.txt", "content")
zf.writestr("other-file.txt", "content")

metadata = extract_metadata_from_zip(Path(tf.name))
assert metadata.cli_version is None
assert metadata.fastlane_plugin_version is None
assert metadata.gradle_plugin_version is None
finally:
Path(tf.name).unlink()


class TestToolingMetadata:
def test_create_with_defaults(self):
metadata = ToolingMetadata()
assert metadata.cli_version is None
assert metadata.fastlane_plugin_version is None
assert metadata.gradle_plugin_version is None
Loading