Skip to content

Commit

Permalink
Allow custom export options for xcode-project build-ipa (#391)
Browse files Browse the repository at this point in the history
  • Loading branch information
priitlatt authored Jan 12, 2024
1 parent 1786101 commit 3f84a65
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 12 deletions.
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
Version 0.50.1
Version 0.50.2
-------------

**Features**
- Allow custom export options in export options properly list for `xcode-project build-ipa` actions. [PR #391](https://github.com/codemagic-ci-cd/cli-tools/pull/391)

- Version 0.50.1
-------------

**Bugfixes**
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "codemagic-cli-tools"
version = "0.50.1"
version = "0.50.2"
description = "CLI tools used in Codemagic builds"
readme = "README.md"
authors = [
Expand Down
2 changes: 1 addition & 1 deletion src/codemagic/__version__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
__title__ = "codemagic-cli-tools"
__description__ = "CLI tools used in Codemagic builds"
__version__ = "0.50.1.dev"
__version__ = "0.50.2.dev"
__url__ = "https://github.com/codemagic-ci-cd/cli-tools"
__licence__ = "GNU General Public License v3.0"
35 changes: 27 additions & 8 deletions src/codemagic/models/export_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,22 +107,29 @@ def dict(self) -> Dict[str, str]:

@dataclass
class ExportOptions(StringConverterMixin):
"""
Definition for Xcode export options. Available keys and their allowed types
can be seen from `xcodebuild -help` output under section
"Available keys for -exportOptionsPlist"
"""

compileBitcode: Optional[bool] = None
destination: Optional[Destination] = None
distributionBundleIdentifier: Optional[str] = None
embedOnDemandResourcesAssetPacksInBundle: Optional[bool] = None
generateAppStoreInformation: Optional[bool] = None
iCloudContainerEnvironment: Optional[str] = None
installerSigningCertificate: Optional[str] = None
manifest: Optional[Manifest] = None
manageAppVersionAndBuildNumber: Optional[bool] = None
manifest: Optional[Manifest] = None
method: Optional[ArchiveMethod] = None
onDemandResourcesAssetPacksBaseURL: Optional[str] = None
provisioningProfiles: Optional[List[ProvisioningProfileInfo]] = None
signingCertificate: Optional[str] = None
signingStyle: Optional[SigningStyle] = None
stripSwiftSymbols: Optional[bool] = None
teamID: Optional[str] = None
testFlightInternalTestingOnly: Optional[bool] = None
thinning: Optional[str] = None
uploadBitcode: Optional[bool] = None
uploadSymbols: Optional[bool] = None
Expand All @@ -132,8 +139,12 @@ def __post_init__(self):
self.set_value(field_name, getattr(self, field_name))

@classmethod
def _get_field_type(cls, field_name: str) -> type:
type_hint = get_type_hints(cls)[field_name]
def _get_field_type(cls, field_name: str) -> Optional[type]:
try:
type_hint = get_type_hints(cls)[field_name]
except KeyError:
# Type info is missing for unknown fields
return None
if hasattr(type_hint, "__origin__") and type_hint.__origin__ is Union:
# Optionals are unions of actual type and NoneType
actual_type = type_hint.__args__[0]
Expand Down Expand Up @@ -183,7 +194,8 @@ def _set_provisioning_profiles(self, new_profiles):

def set_value(self, field_name: str, value: ExportOptionValue):
if field_name not in self.__dict__:
raise ValueError(f"Invalid filed {field_name}")
logger = log.get_logger(self.__class__)
logger.warning(Colors.YELLOW(f'Warning: Using unknown export option "{field_name}"'))

field_type = self._get_field_type(field_name)
if value is None:
Expand All @@ -192,9 +204,12 @@ def set_value(self, field_name: str, value: ExportOptionValue):
self._set_manifest(value)
elif field_name == "provisioningProfiles":
self._set_provisioning_profiles(value)
elif not isinstance(value, field_type):
with ResourceEnumMeta.without_graceful_fallback():
setattr(self, field_name, field_type(value))
elif field_type and not isinstance(value, field_type):
if issubclass(field_type, enum.Enum):
with ResourceEnumMeta.without_graceful_fallback():
setattr(self, field_name, field_type(value))
else:
raise ValueError(f"Invalid value for {field_name!r}")
else:
setattr(self, field_name, value)

Expand All @@ -213,7 +228,11 @@ def from_path(cls, plist_path: Union[pathlib.Path, AnyStr]) -> ExportOptions:
data = plistlib.load(fd) # type: ignore
except plistlib.InvalidFileException:
raise ValueError("Invalid plist")
return ExportOptions(**data)

export_options = ExportOptions()
for key_name, value in data.items():
export_options.set_value(field_name=key_name, value=value)
return export_options

@classmethod
def from_profile_assignments(cls, profile_assignments: Sequence[ProvisioningProfileAssignment]) -> ExportOptions:
Expand Down
50 changes: 49 additions & 1 deletion tests/models/test_export_options.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import logging
import plistlib
import tempfile
from tempfile import NamedTemporaryFile
from tempfile import TemporaryDirectory
from unittest import mock

import pytest
from codemagic.cli import Colors
from codemagic.models import ExportOptions
from codemagic.models.export_options import ProvisioningProfileInfo

mock_logger = mock.Mock(spec=logging.Logger)


def test_export_options_initialize_from_path(export_options_list_path, export_options_dict):
export_options = ExportOptions.from_path(export_options_list_path)
Expand Down Expand Up @@ -74,14 +81,55 @@ def test_export_options_set_valid_values(field_name, value, export_options_dict)
("manifest", 1),
("manifest", 3.4),
("manifest", {"invalid_key": 1}),
("invalid_field", -1),
("provisioningProfiles", [1]),
("provisioningProfiles", [ProvisioningProfileInfo("bundle_id", "name"), {"k": "v"}]),
("method", "invalid method"),
("destination", "invalid destination"),
("teamID", -1),
("teamID", True),
("stripSwiftSymbols", "false"),
("stripSwiftSymbols", "YES"),
],
)
def test_export_options_set_invalid_values(field_name, value, export_options_dict):
export_options = ExportOptions(**export_options_dict)
with pytest.raises(ValueError):
export_options.set_value(field_name=field_name, value=value)


@mock.patch("codemagic.models.export_options.log.get_logger", new=mock.Mock(return_value=mock_logger))
def test_export_options_set_unknown_fields():
export_options = ExportOptions()
export_options.set_value(field_name="unknownExportOption", value=False)

expected_warning = 'Warning: Using unknown export option "unknownExportOption"'
mock_logger.warning.assert_called_once_with(Colors.YELLOW(expected_warning))
assert export_options.dict() == {"unknownExportOption": False}


@mock.patch("codemagic.models.export_options.log.get_logger", new=mock.Mock(return_value=mock_logger))
def test_export_options_unknown_fields_notify():
export_options = ExportOptions()
export_options.set_value(field_name="unknownExportOption", value=False)
export_options.notify("Title")

mock_logger.info.assert_has_calls(
[
mock.call("Title"),
mock.call(Colors.BLUE(" - Unknown Export Option: False")),
],
)


def test_export_options_unknown_fields_from_path():
expected_export_options = {
"signingStyle": "manual", # Known field
"unknownExportOption": False, # Unknown field
}

with tempfile.NamedTemporaryFile(mode="wb") as tf:
tf.write(plistlib.dumps(expected_export_options))
tf.flush()
export_options = ExportOptions.from_path(tf.name)

assert export_options.dict() == expected_export_options

0 comments on commit 3f84a65

Please sign in to comment.