def build_ipa(self, xcode_project_path: Optional[pathlib.Path] = None, xcode_workspace_path: Optional[pathlib.Path] = None, target_name: Optional[str] = None, configuration_name: Optional[str] = None, scheme_name: Optional[str] = None, clean: bool = False, archive_directory: pathlib.Path = ExportIpaArgument.ARCHIVE_DIRECTORY.get_default(), archive_xcargs: Optional[str] = XcodeArgument.ARCHIVE_XCARGS.get_default(), archive_flags: Optional[str] = XcodeArgument.ARCHIVE_FLAGS.get_default(), ipa_directory: pathlib.Path = ExportIpaArgument.IPA_DIRECTORY.get_default(), export_options_plist: pathlib.Path = ExportIpaArgument.EXPORT_OPTIONS_PATH_EXISTING.get_default(), export_xcargs: Optional[str] = XcodeArgument.EXPORT_XCARGS.get_default(), export_flags: Optional[str] = XcodeArgument.EXPORT_FLAGS.get_default(), remove_xcarchive: bool = False, disable_xcpretty: bool = False, xcpretty_options: str = XcprettyArgument.OPTIONS.get_default()) -> pathlib.Path: """ Build ipa by archiving the Xcode project and then exporting it """ self._ensure_project_or_workspace(xcode_project_path, xcode_workspace_path) export_options = ExportOptions.from_path(export_options_plist) xcodebuild = self._get_xcodebuild(**locals()) clean and self._clean(xcodebuild) self.logger.info(Colors.BLUE(f'Archive {(xcodebuild.workspace or xcodebuild.xcode_project).name}')) try: xcarchive = xcodebuild.archive( export_options, archive_directory, xcargs=archive_xcargs, custom_flags=archive_flags) except IOError as error: raise XcodeProjectException(*error.args) self.logger.info(Colors.GREEN(f'Successfully created archive at {xcarchive}\n')) self._update_export_options(xcarchive, export_options_plist, export_options) self.logger.info(Colors.BLUE(f'Export {xcarchive} to {ipa_directory}')) try: ipa = xcodebuild.export_archive( xcarchive, export_options_plist, ipa_directory, xcargs=export_xcargs, custom_flags=export_flags) except IOError as error: raise XcodeProjectException(*error.args) else: self.logger.info(Colors.GREEN(f'Successfully exported ipa to {ipa}\n')) finally: if not disable_xcpretty: self.logger.info(f'Raw xcodebuild logs stored in {xcodebuild.logs_path}') if xcarchive is not None and remove_xcarchive: self.logger.info(f'Removing generated xcarchive {xcarchive.resolve()}') shutil.rmtree(xcarchive, ignore_errors=True) return ipa
def notify(self, title: str): logger = log.get_logger(self.__class__) logger.info(title) options = self.dict() for key in sorted(options.keys()): value = options[key] option = re.sub(f'([A-Z])', r' \1', key.replace('ID', 'Id')).lstrip(' ').title() if isinstance(value, dict): logger.info(Colors.BLUE(f' - {option}:')) for k, v in value.items(): logger.info(Colors.BLUE(f' - {k}: {v}')) else: logger.info(Colors.BLUE(f' - {option}: {value}'))
def _clean(self, xcodebuild: Xcodebuild): self.logger.info(Colors.BLUE(f'Clean {(xcodebuild.workspace or xcodebuild.xcode_project).name}')) try: xcodebuild.clean() except IOError as error: raise XcodeProjectException(*error.args) self.logger.info(Colors.GREEN(f'Cleaned {(xcodebuild.workspace or xcodebuild.xcode_project).name}\n'))
def get_test_destinations(self, runtimes: Optional[Sequence[Runtime]] = None, simulator_name: Optional[re.Pattern] = None, include_unavailable: bool = False, json_output: bool = False, should_print: bool = True) -> List[Simulator]: """ List available destinations for test runs """ try: all(r.validate() for r in (runtimes or [])) except ValueError as ve: TestArgument.RUNTIMES.raise_argument_error(str(ve)) self.logger.info(Colors.BLUE('List available test devices')) try: simulators = Simulator.list(runtimes, simulator_name, include_unavailable) except IOError as e: raise XcodeProjectException(str(e)) from e if not simulators: raise XcodeProjectException('No simulator runtimes are available') if should_print: if json_output: self.echo(json.dumps([s.dict() for s in simulators], indent=4)) else: runtime_simulators = defaultdict(list) for s in simulators: runtime_simulators[s.runtime].append(s) for runtime in sorted(runtime_simulators.keys()): self.echo(Colors.GREEN('Runtime: %s'), runtime) for simulator in runtime_simulators[runtime]: self.echo(f'- {simulator.name}') return simulators
def _upload_artifact_with_altool(self, altool: Altool, artifact_path: pathlib.Path): self.logger.info(Colors.BLUE('\nUpload "%s" to App Store Connect'), artifact_path) result = altool.upload_app(artifact_path) message = result.success_message or f'No errors uploading "{artifact_path}".' self.logger.info(Colors.GREEN(message))
def _validate_artifact_with_altool(self, altool: Altool, artifact_path: pathlib.Path): self.logger.info(Colors.BLUE('\nValidate "%s" for App Store Connect'), artifact_path) result = altool.validate_app(artifact_path) message = result.success_message or f'No errors validating archive at "{artifact_path}".' self.logger.info(Colors.GREEN(message))
def _format_build_config_meta(cls, build_config_info): profile = build_config_info['profile'] project = build_config_info['project_name'] target = build_config_info['target_name'] config = build_config_info['build_configuration'] return Colors.BLUE( f' - Using profile "{profile.name}" [{profile.uuid}] ' f'for target "{target}" [{config}] from project "{project}"', )
def print_resource(self, resource: R, should_print: bool): if should_print is not True: return if self.print_json: self.print(resource.json()) else: header = f'-- {resource.__class__}{" (Created)" if resource.created else ""} --' self.print(Colors.BLUE(header)) self.print(str(resource))
def _publish_application_package( self, altool: Altool, application_package: Union[Ipa, MacOsPackage] ) -> Tuple[Build, PreReleaseVersion]: """ :raises IOError in case any step of publishing fails """ self.logger.info(Colors.BLUE('\nPublish "%s" to App Store Connect'), application_package.path) self.logger.info(application_package.get_text_summary()) self._validate_artifact_with_altool(altool, application_package.path) self._upload_artifact_with_altool(altool, application_package.path) bundle_id = application_package.bundle_identifier self.logger.info( Colors.BLUE( '\nFind application entry from App Store Connect for uploaded binary' )) try: app = self.list_apps(bundle_id_identifier=bundle_id, should_print=False)[0] except IndexError: raise IOError( f'Did not find app with bundle identifier "{bundle_id}" from App Store Connect' ) else: self.printer.print_resource(app, True) self.logger.info(Colors.BLUE('\nFind freshly uploaded build')) for build in self.list_app_builds(app.id, should_print=False): if build.attributes.version == application_package.version_code: pre_release_version = self.get_build_pre_release_version( build.id, should_print=False) if pre_release_version.attributes.version == application_package.version: break else: raise IOError( f'Did not find corresponding build from App Store versions for "{application_package.path}"' ) self.logger.info(Colors.GREEN('\nPublished build is')) self.printer.print_resource(build, True) self.printer.print_resource(pre_release_version, True) return build, pre_release_version
def notify_profile_usage(self): self.logger.info( Colors.GREEN('Completed configuring code signing settings')) if not self._matched_profiles: 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 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): """ Set up code signing settings on specified Xcode projects to use given provisioning profiles """ from .keychain import Keychain self.logger.info(Colors.BLUE('Configure code signing settings')) profile_paths = self.find_paths(*profile_path_patterns) xcode_projects = self.find_paths(*xcode_project_patterns) try: profiles = [ ProvisioningProfile.from_path(p) for p in profile_paths ] except (ValueError, IOError) as error: raise XcodeProjectException(*error.args) available_certs = Keychain().list_code_signing_certificates( should_print=False) code_signing_settings_manager = CodeSigningSettingsManager( profiles, available_certs) for xcode_project in xcode_projects: try: code_signing_settings_manager.use_profiles(xcode_project) except (ValueError, IOError) as error: if warn_only: self.logger.warning( Colors.YELLOW( f'Using profiles on {xcode_project} failed')) else: raise XcodeProjectException(*error.args) code_signing_settings_manager.notify_profile_usage() export_options = code_signing_settings_manager.generate_export_options( custom_export_options) export_options.notify( Colors.GREEN('Generated options for exporting the project')) export_options.save(export_options_plist) self.logger.info( Colors.GREEN(f'Saved export options to {export_options_plist}')) return export_options
def _get_test_destinations(self, requested_devices: Optional[List[str]]) -> List[Simulator]: if not requested_devices: simulators = [self.get_default_test_destination(should_print=False)] else: try: simulators = Simulator.find_simulators(requested_devices) except ValueError as ve: raise TestArgument.TEST_DEVICES.raise_argument_error(str(ve)) from ve self.echo(Colors.BLUE('Running tests on simulators:')) for s in simulators: self.echo('- %s %s (%s)', s.runtime, s.name, s.udid) self.echo('') return simulators
def log_creating(self, resource_type: Type[R], **params): def fmt(item: Tuple[str, Any]): name, value = item if isinstance(value, list): return f'{name.replace("_", " ")}: {[shlex.quote(str(el)) for el in value]}' elif isinstance(value, enum.Enum): value = str(value.value) elif not isinstance(value, (str, bytes)): value = str(value) return f'{name.replace("_", " ")}: {shlex.quote(value)}' message = f'Creating new {resource_type}' if params: message = f'{message}: {", ".join(map(fmt, params.items()))}' self.logger.info(Colors.BLUE(message))
def get_default_test_destination(self, json_output: bool = False, should_print: bool = True) -> Simulator: """ Show default test destination for the chosen Xcode version """ xcode = Xcode.get_selected() if should_print: msg_template = 'Show default test destination for Xcode %s (%s)' self.logger.info(Colors.BLUE(msg_template), xcode.version, xcode.build_version) try: simulator = Simulator.get_default() except ValueError as error: raise XcodeProjectException(str(error)) from error if should_print: if json_output: self.echo(json.dumps(simulator.dict(), indent=4)) else: self.echo(Colors.GREEN(f'{simulator.runtime} {simulator.name}')) return simulator
def run_test(self, xcode_project_path: Optional[pathlib.Path] = None, xcode_workspace_path: Optional[pathlib.Path] = None, target_name: Optional[str] = None, configuration_name: Optional[str] = None, scheme_name: Optional[str] = None, clean: bool = False, devices: Optional[List[str]] = None, disable_code_coverage: bool = False, max_concurrent_devices: Optional[int] = TestArgument.MAX_CONCURRENT_DEVICES.get_default(), max_concurrent_simulators: Optional[int] = TestArgument.MAX_CONCURRENT_SIMULATORS.get_default(), test_only: Optional[str] = TestArgument.TEST_ONLY.get_default(), test_sdk: str = TestArgument.TEST_SDK.get_default(), test_xcargs: Optional[str] = XcodeArgument.TEST_XCARGS.get_default(), test_flags: Optional[str] = XcodeArgument.TEST_FLAGS.get_default(), disable_xcpretty: bool = False, xcpretty_options: str = XcprettyArgument.OPTIONS.get_default(), output_dir: pathlib.Path = TestResultArgument.OUTPUT_DIRECTORY.get_default(), output_extension: str = TestResultArgument.OUTPUT_EXTENSION.get_default(), graceful_exit: bool = False): """ Run unit or UI tests for given Xcode project or workspace """ self._ensure_project_or_workspace(xcode_project_path, xcode_workspace_path) simulators = self._get_test_destinations(devices) xcodebuild = self._get_xcodebuild(**locals()) clean and self._clean(xcodebuild) self.echo(Colors.BLUE(f'Run tests for {(xcodebuild.workspace or xcodebuild.xcode_project).name}\n')) xcresult_collector = XcResultCollector() xcresult_collector.ignore_results(Xcode.DERIVED_DATA_PATH) try: xcodebuild.test( test_sdk, simulators, enable_code_coverage=not disable_code_coverage, only_testing=test_only, xcargs=test_xcargs, custom_flags=test_flags, max_concurrent_devices=max_concurrent_devices, max_concurrent_simulators=max_concurrent_simulators, ) except IOError: testing_failed = True self.echo(Colors.RED('\nTest run failed\n')) else: testing_failed = False self.echo(Colors.GREEN('\nTest run completed successfully\n')) xcresult_collector.gather_results(Xcode.DERIVED_DATA_PATH) output_dir.mkdir(parents=True, exist_ok=True) self._copy_simulator_logs(simulators, output_dir) if not xcresult_collector.get_collected_results(): raise XcodeProjectException('Did not find any test results') test_suites, xcresult = self._get_test_suites( xcresult_collector, show_found_result=True, save_xcresult_dir=output_dir) message = ( f'Executed {test_suites.tests} tests with ' f'{test_suites.failures} failures and ' f'{test_suites.errors} errors in ' f'{test_suites.time:.2f} seconds.\n' ) self.echo(Colors.BLUE(message)) TestSuitePrinter(self.echo).print_test_suites(test_suites) self._save_test_suite(xcresult, test_suites, output_dir, output_extension) if not graceful_exit: has_failing_tests = test_suites and (test_suites.failures or test_suites.errors) if testing_failed or has_failing_tests: raise XcodeProjectException('Tests failed')
def log_request(self, header: str) -> None: if not self.print: return self.print(Colors.BLUE(header))