Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improvement: xcode-project use-profiles logs for set code signing settings #203

Merged
merged 7 commits into from
Feb 22, 2022
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
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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: [<target_info>, ...]}` is now `[<target_info>, ...]`.
- 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
-------------

Expand Down
75 changes: 36 additions & 39 deletions bin/code_signing_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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:
{
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.'
Expand All @@ -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
Expand All @@ -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
Expand Down
5 changes: 5 additions & 0 deletions docs/xcode-project/use-profiles.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand All @@ -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`
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.18.1'
__version__ = '0.19.0'
__url__ = 'https://github.com/codemagic-ci-cd/cli-tools'
__licence__ = 'GNU General Public License v3.0'
119 changes: 94 additions & 25 deletions src/codemagic/models/code_signing_settings_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -70,55 +89,105 @@ 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)
except subprocess.CalledProcessError:
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
Loading