Ejemplo n.º 1
0
    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}'))
Ejemplo n.º 3
0
 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'))
Ejemplo n.º 4
0
    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
Ejemplo n.º 5
0
 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))
Ejemplo n.º 6
0
 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))
Ejemplo n.º 7
0
 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}"', )
Ejemplo n.º 8
0
 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))
Ejemplo n.º 9
0
    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
Ejemplo n.º 10
0
    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()))
Ejemplo n.º 11
0
    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
Ejemplo n.º 12
0
    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
Ejemplo n.º 13
0
    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))
Ejemplo n.º 14
0
    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
Ejemplo n.º 15
0
    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')
Ejemplo n.º 16
0
 def log_request(self, header: str) -> None:
     if not self.print:
         return
     self.print(Colors.BLUE(header))