diff --git a/CHANGELOG.md b/CHANGELOG.md index e347a3b2..640e3b98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,25 @@ +Version 0.19.0 +------------- + +This release includes changes from [PR #203](https://github.com/codemagic-ci-cd/cli-tools/pull/203) to improve usability and feedback from `xcode-project use-profiles`. + +**Features** +- Improve `xcode-project use-profiles` log output. Highlight Xcode targets for which code signing settings were not configured, but are likely necessary for successful build. +- Add `--code-signing-setup-verbose-logging` option to action `xcode-project use-profiles` which turns on detailed log output for code signing settings configuration. + +**Docs** +- Update docs for `xcode-project use-profiles` action. Add documentation for option `--code-signing-setup-verbose-logging`. + +**Development** +- **Breaking.** Remove dataclass `codemagic.models.matched_profiles.MatchedProfile` and all its usages. +- **Breaking.** Replace `ExportOptions.from_matched_profiles` with `ExportOptions.from_used_profiles`. Old method was relying on the removed `MatchedProfile` class, while new method has more generic interface requiring only sequence of `ProvisioningProfiles` as arguments. +- **Breaking.** Change command line interface for `code_signing_manager.rb`: + - Replace command line option `-u` / `--used-profiles` with `-r` / `--result-path` to better reflect the updated contents of result file. + - Results saved into file specified by `--result-path` will now include all found Xcode targets, including those that were not assigned provisioning profile. The targets for which matching provisioning profile was found and configured, the reference also includes the used provisioning profile `uuid`. + - The saved JSON file structure what used to be `{profile_uuid: [, ...]}` is now `[, ...]`. +- Always multiplex `code_signing_manager.rb` verbose log output to main file log. +- `CodeSigningManager.use_profiles` logs Xcode project targets for which provisioning profiles were not found, but are likely necessary for building `ipa`. + Version 0.18.1 ------------- diff --git a/bin/code_signing_manager.rb b/bin/code_signing_manager.rb index 0b16a6ca..6e4a1556 100755 --- a/bin/code_signing_manager.rb +++ b/bin/code_signing_manager.rb @@ -8,7 +8,7 @@ require "xcodeproj" -USAGE = "Usage: #{File.basename(__FILE__)} [options] -x XCODEPROJ_PATH -u USED_PROFILES_PATH -p [{...}, ...] +USAGE = "Usage: #{File.basename(__FILE__)} [options] -x XCODEPROJ_PATH -r RESULT_PATH -p [{...}, ...] Example provisioning profile in JSON format: { @@ -218,12 +218,12 @@ class CodeSigningManager "com.apple.product-type.bundle.unit-test", # Product type Unit Test ] - def initialize(project_path:, used_profiles_path:, profiles:) + def initialize(project_path:, result_path:, profiles:) @project_path = project_path - @used_profiles_json_path = used_profiles_path + @results_json_path = result_path @project = Xcodeproj::Project.open(File.realpath(project_path)) @profiles = profiles - @used_provisioning_profiles = Hash.new + @target_infos = [] end def set_code_signing_settings @@ -237,23 +237,19 @@ def set_code_signing_settings # https://github.com/CocoaPods/Xcodeproj/issues/691 if e.message.include? "Consistency issue: no parent for object" Log.info "Ignore error, this is open xcodeproj issue" - @used_provisioning_profiles = Hash.new + @target_infos = [] else raise # Unknown error, panic end end - save_used_provisioning_profiles + save_use_profiles_result end private - def save_used_provisioning_profiles - used_profiles = Hash.new - @used_provisioning_profiles.each do |profile_specifier, bundle_ids| - used_profiles[profile_specifier] = bundle_ids.to_a - end - File.open(@used_profiles_json_path, 'w') do |f| - f.write(JSON.pretty_generate(used_profiles)) + def save_use_profiles_result + File.open(@results_json_path, 'w') do |f| + f.write(JSON.pretty_generate(@target_infos)) end end @@ -448,32 +444,33 @@ def set_configuration_build_settings(build_target, build_configuration) end end - unless profile - Log.info "Did not find suitable provisioning profile for target with bundle identifier '#{bundle_id}'" - return + track_target_info(profile, build_target, build_configuration, bundle_id) + if profile + set_build_settings(build_target, build_configuration, profile) end - - mark_profile_as_used(profile, build_target, build_configuration, bundle_id) - set_build_settings(build_target, build_configuration, profile) end - def mark_profile_as_used(profile, target, build_configuration, bundle_id) - Log.info "Using profile '#{profile['name']}' (bundle id '#{profile['bundle_id']}') for" + def track_target_info(profile, target, build_configuration, bundle_id) + profile_uuid = profile ? profile['specifier'] : nil + target_info = { + :bundle_id => bundle_id, + :target_name => target.name, + :build_configuration => build_configuration.name, + :project_name => @project.root_object.name, + :provisioning_profile_uuid => profile_uuid + } + + if profile + Log.info "Using profile '#{profile['name']}' (bundle id '#{profile['bundle_id']}') for" + else + Log.info "Did not find suitable provisioning profile for" + end Log.info "\ttarget '#{target.name}'" Log.info "\tbuild configuration '#{build_configuration.name}'" Log.info "\tbundle id '#{bundle_id}'" - Log.info "\tspecifier '#{profile['specifier']}'" + Log.info "\tspecifier '#{profile_uuid || "N/A"}'" - if @used_provisioning_profiles[profile['specifier']].nil? - @used_provisioning_profiles[profile['specifier']] = [] - end - target_info = { - :bundle_id => bundle_id, - :target_name => target.name, - :build_configuration => build_configuration.name, - :project_name => @project.root_object.name - } - @used_provisioning_profiles[profile['specifier']].push target_info + @target_infos.push(target_info) end def set_target_build_settings(target) @@ -519,12 +516,12 @@ def parse_args options[:project_path] = project_path end - used_profiles_help = 'Used profiles will be saved as JSON object to file at USED_PROFILES_PATH. REQUIRED.' - parser.on('-u', '--used-profiles USED_PROFILES_PATH', String, help = used_profiles_help) do |used_profiles_path| - unless used_profiles_path.end_with?('.json') - raise OptionParser::InvalidArgument.new(": '#{used_profiles_path}' is not JSON file") + result_path_help = 'Profiles usage result will be saved as JSON object to file at RESULT_PATH. REQUIRED.' + parser.on('-r', '--result-path RESULT_PATH', String, help = result_path_help) do |result_path| + unless result_path.end_with?('.json') + raise OptionParser::InvalidArgument.new(": '#{result_path}' is not JSON file") end - options[:used_profiles_path] = used_profiles_path + options[:result_path] = result_path end profiles_help = 'Apply code signing settings from JSON encoded list PROFILES. REQUIRED.' @@ -542,7 +539,7 @@ def parse_args end.parse! raise OptionParser::MissingArgument.new("Missing required argument --xcode-project") unless options[:project_path] - raise OptionParser::MissingArgument.new("Missing required argument --used-profiles") unless options[:used_profiles_path] + raise OptionParser::MissingArgument.new("Missing required argument --result-path") unless options[:result_path] raise OptionParser::MissingArgument.new("Missing required argument --profiles") unless options[:profiles] options @@ -553,7 +550,7 @@ def main(args) Log.set_verbose args[:verbose] code_signing_manager = CodeSigningManager.new( project_path: args[:project_path], - used_profiles_path: args[:used_profiles_path], + result_path: args[:result_path], profiles: args[:profiles]) code_signing_manager.set_code_signing_settings end diff --git a/docs/xcode-project/use-profiles.md b/docs/xcode-project/use-profiles.md index ada640d1..43f040ec 100644 --- a/docs/xcode-project/use-profiles.md +++ b/docs/xcode-project/use-profiles.md @@ -12,6 +12,7 @@ xcode-project use-profiles [-h] [--log-stream STREAM] [--no-color] [--version] [ [--export-options-plist EXPORT_OPTIONS_PATH] [--custom-export-options CUSTOM_EXPORT_OPTIONS] [--warn-only] + [--code-signing-setup-verbose-logging] ``` ### Optional arguments for action `use-profiles` @@ -35,6 +36,10 @@ Custom options for generated export options as JSON string. For example '{"uploa Show warning when profiles cannot be applied to any of the Xcode projects instead of fully failing the action +##### `--code-signing-setup-verbose-logging` + + +Show verbose log output when configuring code signing settings for Xcode project. If not given, the value will be checked from the environment variable `XCODE_PROJECT_CODE_SIGNING_SETUP_VERBOSE_LOGGING`. ### Common options ##### `-h, --help` diff --git a/src/codemagic/__version__.py b/src/codemagic/__version__.py index 04f1ac0c..93239fc2 100644 --- a/src/codemagic/__version__.py +++ b/src/codemagic/__version__.py @@ -1,5 +1,5 @@ __title__ = 'codemagic-cli-tools' __description__ = 'CLI tools used in Codemagic builds' -__version__ = '0.18.1' +__version__ = '0.19.0' __url__ = 'https://github.com/codemagic-ci-cd/cli-tools' __licence__ = 'GNU General Public License v3.0' diff --git a/src/codemagic/models/code_signing_settings_manager.py b/src/codemagic/models/code_signing_settings_manager.py index e50a28b9..726e2018 100644 --- a/src/codemagic/models/code_signing_settings_manager.py +++ b/src/codemagic/models/code_signing_settings_manager.py @@ -5,12 +5,15 @@ import shlex import shutil import subprocess +from dataclasses import dataclass from functools import lru_cache from tempfile import NamedTemporaryFile from typing import Counter from typing import Dict from typing import List from typing import Optional +from typing import Sequence +from typing import Tuple from codemagic.cli import Colors from codemagic.mixins import RunningCliAppMixin @@ -19,16 +22,32 @@ from .certificate import Certificate from .export_options import ExportOptions -from .matched_profile import MatchedProfile from .provisioning_profile import ProvisioningProfile +@dataclass +class TargetInfo: + build_configuration: str + bundle_id: str + project_name: str + target_name: str + provisioning_profile_uuid: Optional[str] = None + + @classmethod + def sort_key(cls, target_info: TargetInfo) -> Tuple[str, str, str]: + return ( + target_info.project_name, + target_info.target_name, + target_info.build_configuration, + ) + + class CodeSigningSettingsManager(RunningCliAppMixin, StringConverterMixin): def __init__(self, profiles: List[ProvisioningProfile], keychain_certificates: List[Certificate]): self.profiles: Dict[str, ProvisioningProfile] = {profile.uuid: profile for profile in profiles} self._certificates = keychain_certificates - self._matched_profiles: List[MatchedProfile] = [] + self._target_infos: List[TargetInfo] = [] self.logger = log.get_logger(self.__class__) @lru_cache() @@ -70,33 +89,81 @@ def _format_build_config_meta(cls, build_config_info): f'for target "{target}" [{config}] from project "{project}"', ) + def _notify_target_profile_usage(self, targets_with_profile: Sequence[TargetInfo]): + for target_info in targets_with_profile: + if target_info.provisioning_profile_uuid is None: + continue + profile = self.profiles[target_info.provisioning_profile_uuid] + message = ( + f' - Using profile "{profile.name}" [{profile.uuid}] ' + f'for target "{target_info.target_name}" [{target_info.build_configuration}] ' + f'from project "{target_info.project_name}"' + ) + self.logger.info(Colors.BLUE(message)) + + def _notify_target_missing_profiles( + self, + targets_with_profile: Sequence[TargetInfo], + targets_without_profile: Sequence[TargetInfo], + ): + """ + Show warning only for targets that have the same bundle id prefix, configuration and project + as some target for which provisioning profile was matched. + For example, if target with bundle id "com.example.app" from project "ProjectName" + with config "Debug" was assigned a profile. Then + - target with bundle identifier "com.example.app.specifier" from "ProjectName" with + config "Debug" would trigger a warning, since both project and configuration match with + the target for which profile was assigned to, and bundle identifier inherits from the + matched target identifier. + - however target with bundle identifier "com.example.otherApp" from the same project + would not trigger a warning as bundle id does not inherit from a target for which a + was profile was assigned to. + """ + + for target_info in targets_without_profile: + for matched_target_info in targets_with_profile: + project_name_match = target_info.project_name == matched_target_info.project_name + configuration_match = target_info.build_configuration == matched_target_info.build_configuration + bundle_id_prefix_match = \ + target_info.bundle_id == matched_target_info.bundle_id or \ + target_info.bundle_id.startswith(f'{matched_target_info.bundle_id}.') + + if project_name_match and configuration_match and bundle_id_prefix_match: + message = ( + f' - Did not find provisioning profile matching bundle identifier "{target_info.bundle_id}" ' + f'for target "{target_info.target_name}" [{target_info.build_configuration}] ' + f'from project "{target_info.project_name}"' + ) + self.logger.info(Colors.YELLOW(message)) + break + def notify_profile_usage(self): self.logger.info(Colors.GREEN('Completed configuring code signing settings')) - if not self._matched_profiles: + targets_with_profile = [ti for ti in self._target_infos if ti.provisioning_profile_uuid is not None] + targets_without_profile = [ti for ti in self._target_infos if ti.provisioning_profile_uuid is None] + + if targets_with_profile: + self._notify_target_profile_usage(targets_with_profile) + self._notify_target_missing_profiles(targets_with_profile, targets_without_profile) + else: message = 'Did not find matching provisioning profiles for code signing!' self.logger.warning(Colors.YELLOW(message)) - return - for info in sorted(self._matched_profiles, key=lambda i: i.sort_key()): - self.logger.info(Colors.BLUE(info.format())) - - def _apply(self, xcode_project, result_file_name): + def _apply(self, xcode_project, result_file_name, verbose_logging: bool): cmd = [ self._code_signing_manager, '--xcode-project', xcode_project, - '--used-profiles', result_file_name, + '--result-path', result_file_name, '--profiles', self._get_json_serialized_profiles(), + '--verbose', ] - cli_app = self.get_current_cli_app() - if cli_app and cli_app.verbose: - cmd.append('--verbose') - process = None + cli_app = self.get_current_cli_app() try: if cli_app: - process = cli_app.execute(cmd) + process = cli_app.execute(cmd, show_output=verbose_logging or cli_app.verbose) process.raise_for_returncode() else: subprocess.check_output(cmd, stderr=subprocess.PIPE) @@ -104,21 +171,23 @@ def _apply(self, xcode_project, result_file_name): xcode_project = shlex.quote(str(xcode_project)) raise IOError(f'Failed to set code signing settings for {xcode_project}', process) - def use_profiles(self, xcode_project: pathlib.Path): - with NamedTemporaryFile(mode='r', prefix='used_profiles_', suffix='.json') as used_profiles: - self._apply(xcode_project, used_profiles.name) + def use_profiles(self, xcode_project: pathlib.Path, verbose_logging: bool = False): + with NamedTemporaryFile(mode='r', prefix='use_profiles_result_', suffix='.json') as results_file: + self._apply(xcode_project, results_file.name, verbose_logging=verbose_logging) try: - used_profiles_info = json.load(used_profiles) + target_infos = json.load(results_file) except ValueError: - used_profiles_info = {} + target_infos = [] - self._matched_profiles.extend( - MatchedProfile(profile=self.profiles[profile_uuid], **xcode_build_config) - for profile_uuid, xcode_build_configs in used_profiles_info.items() - for xcode_build_config in xcode_build_configs - ) + self._target_infos.extend(TargetInfo(**target_info) for target_info in target_infos) + self._target_infos.sort(key=TargetInfo.sort_key) def generate_export_options(self, custom_options: Optional[Dict]) -> ExportOptions: - export_options = ExportOptions.from_matched_profiles(self._matched_profiles) + used_profiles = [ + self.profiles[target_info.provisioning_profile_uuid] + for target_info in self._target_infos + if target_info.provisioning_profile_uuid is not None + ] + export_options = ExportOptions.from_used_profiles(used_profiles) export_options.update(custom_options or {}) return export_options diff --git a/src/codemagic/models/export_options.py b/src/codemagic/models/export_options.py index 809b5979..c02dd3b5 100644 --- a/src/codemagic/models/export_options.py +++ b/src/codemagic/models/export_options.py @@ -20,7 +20,6 @@ from codemagic.mixins import StringConverterMixin from codemagic.utilities import log -from .matched_profile import MatchedProfile from .provisioning_profile import ProvisioningProfile @@ -196,17 +195,16 @@ def from_path(cls, plist_path: Union[pathlib.Path, AnyStr]) -> ExportOptions: return ExportOptions(**data) @classmethod - def from_matched_profiles(cls, matched_profiles: Sequence[MatchedProfile]) -> ExportOptions: - used_profiles = [entry.profile for entry in matched_profiles] - certificates = (c for mp in matched_profiles for c in mp.profile.certificates) - team_ids = Counter[str](mp.profile.team_identifier for mp in matched_profiles) + def from_used_profiles(cls, used_profiles: Sequence[ProvisioningProfile]) -> ExportOptions: + certificates = (certificate for profile in used_profiles for certificate in profile.certificates) + team_ids = Counter[str](profile.team_identifier for profile in used_profiles) common_names = Counter[str](c.common_name.split(':')[0] for c in certificates) return ExportOptions( method=ArchiveMethod.from_profiles(used_profiles), signingStyle=SigningStyle.from_profiles(used_profiles), teamID=team_ids.most_common(1)[0][0] if team_ids else '', - provisioningProfiles=[ProvisioningProfileInfo(mp.bundle_id, mp.profile.name) for mp in matched_profiles], + provisioningProfiles=[ProvisioningProfileInfo(p.bundle_id, p.name) for p in used_profiles], signingCertificate=common_names.most_common(1)[0][0] if common_names else '', ) diff --git a/src/codemagic/models/matched_profile.py b/src/codemagic/models/matched_profile.py deleted file mode 100644 index 75722c39..00000000 --- a/src/codemagic/models/matched_profile.py +++ /dev/null @@ -1,24 +0,0 @@ -from dataclasses import dataclass -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from .provisioning_profile import ProvisioningProfile - - -@dataclass -class MatchedProfile: - profile: 'ProvisioningProfile' - bundle_id: str - project_name: str - target_name: str - build_configuration: str - - def format(self): - return ( - f' - Using profile "{self.profile.name}" [{self.profile.uuid}] ' - f'for target "{self.target_name}" [{self.build_configuration}] ' - f'from project "{self.project_name}"' - ) - - def sort_key(self): - return f'{self.project_name} {self.target_name} {self.build_configuration}' diff --git a/src/codemagic/tools/_xcode_project/arguments.py b/src/codemagic/tools/_xcode_project/arguments.py index b474aec8..98a716e0 100644 --- a/src/codemagic/tools/_xcode_project/arguments.py +++ b/src/codemagic/tools/_xcode_project/arguments.py @@ -6,6 +6,11 @@ from codemagic.models.simulator import Runtime +class CodeSigningSetupVerboseLogging(cli.TypedCliArgument[bool]): + argument_type = bool + environment_variable_key = 'XCODE_PROJECT_CODE_SIGNING_SETUP_VERBOSE_LOGGING' + + class XcodeProjectArgument(cli.Argument): CLEAN = cli.ArgumentProperties( key='clean', @@ -96,6 +101,16 @@ class XcodeProjectArgument(cli.Argument): ), argparse_kwargs={'required': False, 'action': 'store_true'}, ) + CODE_SIGNING_SETUP_VERBOSE_LOGGING = cli.ArgumentProperties( + key='code_signing_setup_verbose_logging', + flags=('--code-signing-setup-verbose-logging',), + type=CodeSigningSetupVerboseLogging, + description='Show verbose log output when configuring code signing settings for Xcode project.', + argparse_kwargs={ + 'required': False, + 'action': 'store_true', + }, + ) IPA_PATH = cli.ArgumentProperties( key='ipa_path', type=cli.CommonArgumentTypes.existing_path, diff --git a/src/codemagic/tools/xcode_project.py b/src/codemagic/tools/xcode_project.py index 53171549..26ec44b0 100644 --- a/src/codemagic/tools/xcode_project.py +++ b/src/codemagic/tools/xcode_project.py @@ -84,18 +84,24 @@ def detect_bundle_id(self, self.echo(bundle_id) return bundle_id - @cli.action('use-profiles', - XcodeProjectArgument.XCODE_PROJECT_PATTERN, - XcodeProjectArgument.PROFILE_PATHS, - ExportIpaArgument.EXPORT_OPTIONS_PATH, - ExportIpaArgument.CUSTOM_EXPORT_OPTIONS, - XcodeProjectArgument.WARN_ONLY) - def use_profiles(self, - xcode_project_patterns: Sequence[pathlib.Path], - profile_path_patterns: Sequence[pathlib.Path], - export_options_plist: pathlib.Path = ExportIpaArgument.EXPORT_OPTIONS_PATH.get_default(), - custom_export_options: Optional[Dict] = None, - warn_only: bool = False): + @cli.action( + 'use-profiles', + XcodeProjectArgument.XCODE_PROJECT_PATTERN, + XcodeProjectArgument.PROFILE_PATHS, + ExportIpaArgument.EXPORT_OPTIONS_PATH, + ExportIpaArgument.CUSTOM_EXPORT_OPTIONS, + XcodeProjectArgument.WARN_ONLY, + XcodeProjectArgument.CODE_SIGNING_SETUP_VERBOSE_LOGGING, + ) + def use_profiles( + self, + xcode_project_patterns: Sequence[pathlib.Path], + profile_path_patterns: Sequence[pathlib.Path], + export_options_plist: pathlib.Path = ExportIpaArgument.EXPORT_OPTIONS_PATH.get_default(), + custom_export_options: Optional[Dict] = None, + warn_only: bool = False, + code_signing_setup_verbose_logging: bool = False, + ): """ Set up code signing settings on specified Xcode projects to use given provisioning profiles @@ -117,7 +123,10 @@ def use_profiles(self, for xcode_project in xcode_projects: try: - code_signing_settings_manager.use_profiles(xcode_project) + code_signing_settings_manager.use_profiles( + xcode_project, + verbose_logging=code_signing_setup_verbose_logging, + ) except (ValueError, IOError) as error: if warn_only: self.logger.warning(Colors.YELLOW(f'Using profiles on {xcode_project} failed'))