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
Ejemplo n.º 2
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.º 3
0
 def log_filtered(self, resource_type: Type[R], resources: Sequence[R], constraint: str):
     count = len(resources)
     name = resource_type.plural(count)
     if count == 0:
         self.logger.info(Colors.YELLOW(f'Did not find any {name} {constraint}'))
     else:
         self.logger.info(Colors.GREEN(f'Filtered out {count} {name} {constraint}'))
Ejemplo n.º 4
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.º 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 log_saved(self, resource: Union[SigningCertificate, Profile],
               path: pathlib.Path):
     destination = shlex.quote(str(path))
     self.logger.info(
         Colors.GREEN(
             f'Saved {resource.__class__} {resource.get_display_info()} to {destination}'
         ))
Ejemplo n.º 8
0
    def _update_export_options(
            self, xcarchive: pathlib.Path, export_options_path: pathlib.Path, export_options: ExportOptions):
        """
        For non-App Store exports, if the app is using either CloudKit or CloudDocuments
        extensions, then "com.apple.developer.icloud-container-environment" entitlement
        needs to be specified. Available options are Development and Production.
        Defaults to Development.
        """
        if export_options.is_app_store_export() or export_options.iCloudContainerEnvironment:
            return

        archive_entitlements = CodeSignEntitlements.from_xcarchive(xcarchive)
        icloud_services = archive_entitlements.get_icloud_services()
        if not {'CloudKit', 'CloudDocuments'}.intersection(icloud_services):
            return

        if 'Production' in archive_entitlements.get_icloud_container_environments():
            icloud_container_environment = 'Production'
        else:
            icloud_container_environment = 'Development'

        self.echo('App is using iCloud services that require iCloudContainerEnvironment export option')
        self.echo('Set iCloudContainerEnvironment export option to %s', icloud_container_environment)
        export_options.set_value('iCloudContainerEnvironment', icloud_container_environment)
        export_options.notify(Colors.GREEN('\nUsing options for exporting IPA'))
        export_options.save(export_options_path)
Ejemplo n.º 9
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.º 10
0
 def _save_test_suite(self,
                      xcresult: pathlib.Path,
                      test_suites: TestSuites,
                      output_dir: pathlib.Path,
                      output_extension: str):
     result_path = output_dir / f'{xcresult.stem}.{output_extension}'
     test_suites.save_xml(result_path)
     self.echo(Colors.GREEN('Saved JUnit XML report to %s'), result_path)
Ejemplo n.º 11
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.º 12
0
 def log_found(self,
               resource_type: Type[R],
               resources: Sequence[R],
               resource_filter: Optional[ResourceManager.Filter] = None,
               related_resource_type: Optional[Type[R2]] = None):
     count = len(resources)
     name = resource_type.plural(count)
     suffix = f' matching specified filters: {resource_filter}.' if resource_filter is not None else ''
     related = f' for {related_resource_type}' if related_resource_type is not None else ''
     if count == 0:
         self.logger.info(Colors.YELLOW(f'Did not find any {name}{related}{suffix}'))
     else:
         self.logger.info(Colors.GREEN(f'Found {count} {name}{related}{suffix}'))
Ejemplo n.º 13
0
    def initialize(self, password: Password = Password(''), timeout: Optional[Seconds] = None) -> Keychain:
        """
        Set up the keychain to be used for code signing. Create the keychain
        at specified path with specified password with given timeout.
        Make it default and unlock it for upcoming use.
        """

        if not self._path:
            self._generate_path()

        message = f'Initialize new keychain to store code signing certificates at {self.path}'
        self.logger.info(Colors.GREEN(message))
        self.create(password)
        self.set_timeout(timeout=timeout)
        self.make_default()
        self.unlock(password)
        return self
Ejemplo n.º 14
0
    def _get_test_suites(self,
                         xcresult_collector: XcResultCollector,
                         show_found_result: bool = False,
                         save_xcresult_dir: Optional[pathlib.Path] = None):
        if show_found_result:
            self.logger.info(Colors.GREEN('Found test results at'))
            for xcresult in xcresult_collector.get_collected_results():
                self.logger.info('- %s', xcresult)
            self.logger.info('')

        xcresult = xcresult_collector.get_merged_xcresult()
        try:
            test_suites = XcResultConverter.xcresult_to_junit(xcresult)
        finally:
            if save_xcresult_dir:
                shutil.copytree(xcresult, save_xcresult_dir / xcresult.name)
            xcresult_collector.forget_merged_result()
        return test_suites, xcresult
Ejemplo n.º 15
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.º 16
0
    def detect_bundle_id(self,
                         xcode_project_patterns: Sequence[pathlib.Path],
                         target_name: Optional[str] = None,
                         configuration_name: Optional[str] = None,
                         include_pods: bool = False,
                         should_print: bool = True) -> str:
        """ Try to deduce the Bundle ID from specified Xcode project """

        xcode_projects = self.find_paths(*xcode_project_patterns)
        bundle_ids = Counter[str](
            bundle_id for xcode_project in xcode_projects
            for bundle_id in self._detect_project_bundle_ids(
                xcode_project, target_name, configuration_name, include_pods))

        if not bundle_ids:
            raise XcodeProjectException(f'Unable to detect Bundle ID')
        bundle_id = bundle_ids.most_common(1)[0][0]

        self.logger.info(Colors.GREEN(f'Chose Bundle ID {bundle_id}'))
        if should_print:
            self.echo(bundle_id)
        return bundle_id
Ejemplo n.º 17
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.º 18
0
 def log_deleted(self, resource_type: Type[R], resource_id: ResourceId):
     self.logger.info(
         Colors.GREEN(
             f'Successfully deleted {resource_type} {resource_id}'))
Ejemplo n.º 19
0
 def log_created(self, resource: Resource):
     self.logger.info(
         Colors.GREEN(f'Created {resource.__class__} {resource.id}'))
Ejemplo n.º 20
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')