Пример #1
0
class ConanMultiPackager(object):
    """ Help to generate common builds (setting's combinations), adjust the environment,
    and run conan create command in docker containers"""
    def __init__(self,
                 username=None,
                 channel=None,
                 runner=None,
                 gcc_versions=None,
                 visual_versions=None,
                 visual_runtimes=None,
                 visual_toolsets=None,
                 apple_clang_versions=None,
                 archs=None,
                 options=None,
                 use_docker=None,
                 curpage=None,
                 total_pages=None,
                 docker_image=None,
                 reference=None,
                 password=None,
                 remotes=None,
                 upload=None,
                 stable_branch_pattern=None,
                 vs10_x86_64_enabled=False,
                 mingw_configurations=None,
                 stable_channel=None,
                 platform_info=None,
                 upload_retry=None,
                 clang_versions=None,
                 login_username=None,
                 upload_only_when_stable=None,
                 upload_only_when_tag=None,
                 upload_only_recipe=None,
                 build_types=None,
                 cppstds=None,
                 skip_check_credentials=False,
                 allow_gcc_minors=False,
                 exclude_vcvars_precommand=False,
                 docker_run_options=None,
                 docker_image_skip_update=False,
                 docker_image_skip_pull=False,
                 docker_entry_script=None,
                 docker_32_images=None,
                 docker_conan_home=None,
                 docker_shell=None,
                 pip_install=None,
                 build_policy=None,
                 always_update_conan_in_docker=False,
                 conan_api=None,
                 client_cache=None,
                 conanfile=None,
                 ci_manager=None,
                 out=None,
                 test_folder=None,
                 cwd=None,
                 config_url=None,
                 config_args=None,
                 upload_dependencies=None,
                 force_selinux=None,
                 skip_recipe_export=False,
                 update_dependencies=None,
                 lockfile=None):

        conan_version = get_client_version()

        self.printer = Printer(out)
        self.printer.print_rule()
        self.printer.print_ascci_art()

        self.cwd = cwd or os.getcwd()

        if not conan_api:
            self.conan_api, _, _ = Conan.factory()
            self.conan_api.create_app()
            self.client_cache = self.conan_api.app.cache
        else:
            self.conan_api = conan_api
            self.client_cache = client_cache

        self.ci_manager = ci_manager or CIManager(self.printer)
        self.remotes_manager = RemotesManager(self.conan_api, self.printer,
                                              remotes, upload)
        self.username = username or os.getenv("CONAN_USERNAME", None)

        self.skip_check_credentials = skip_check_credentials or get_bool_from_env(
            "CONAN_SKIP_CHECK_CREDENTIALS")

        self.auth_manager = AuthManager(
            self.conan_api,
            self.printer,
            login_username,
            password,
            default_username=self.username,
            skip_check_credentials=self.skip_check_credentials)

        # Upload related variables
        self.upload_retry = upload_retry or os.getenv("CONAN_UPLOAD_RETRY", 3)

        if upload_only_when_stable is not None:
            self.upload_only_when_stable = upload_only_when_stable
        else:
            self.upload_only_when_stable = get_bool_from_env(
                "CONAN_UPLOAD_ONLY_WHEN_STABLE")

        if upload_only_when_tag is not None:
            self.upload_only_when_tag = upload_only_when_tag
        else:
            self.upload_only_when_tag = get_bool_from_env(
                "CONAN_UPLOAD_ONLY_WHEN_TAG")

        self.upload_only_recipe = upload_only_recipe or get_bool_from_env(
            "CONAN_UPLOAD_ONLY_RECIPE")

        self.remotes_manager.add_remotes_to_conan()
        self.uploader = Uploader(self.conan_api, self.remotes_manager,
                                 self.auth_manager, self.printer,
                                 self.upload_retry)

        self._builds = []
        self._named_builds = {}
        self._packages_summary = []

        self._update_conan_in_docker = always_update_conan_in_docker or get_bool_from_env(
            "CONAN_ALWAYS_UPDATE_CONAN_DOCKER")

        self._platform_info = platform_info or PlatformInfo()

        self.stable_branch_pattern = stable_branch_pattern or \
                                     os.getenv("CONAN_STABLE_BRANCH_PATTERN", None)

        self.stable_channel = stable_channel or os.getenv(
            "CONAN_STABLE_CHANNEL", "stable")
        self.stable_channel = self.stable_channel.rstrip()
        self.partial_reference = reference or os.getenv(
            "CONAN_REFERENCE", None)
        self.channel = self._get_specified_channel(channel, reference)
        self.conanfile = conanfile or os.getenv("CONAN_CONANFILE",
                                                "conanfile.py")

        if self.partial_reference:
            if "@" in self.partial_reference:
                self.reference = ConanFileReference.loads(
                    self.partial_reference)
            else:
                name, version = self.partial_reference.split("/")
                self.reference = ConanFileReference(name, version,
                                                    self.username,
                                                    self.channel)
        else:
            if not os.path.exists(os.path.join(self.cwd, self.conanfile)):
                raise Exception("Conanfile not found, specify a 'reference' "
                                "parameter with name and version")

            conanfile = load_cf_class(os.path.join(self.cwd, self.conanfile),
                                      self.conan_api)
            name, version = conanfile.name, conanfile.version
            if name and version:
                self.reference = ConanFileReference(name, version,
                                                    self.username,
                                                    self.channel)
            else:
                self.reference = None

        self._docker_image = docker_image or os.getenv("CONAN_DOCKER_IMAGE",
                                                       None)

        # If CONAN_DOCKER_IMAGE is specified, then use docker is True
        self.use_docker = (use_docker or os.getenv("CONAN_USE_DOCKER", False)
                           or self._docker_image is not None)

        self.docker_conan_home = docker_conan_home or os.getenv(
            "CONAN_DOCKER_HOME", None)

        os_name = self._platform_info.system(
        ) if not self.use_docker else "Linux"
        self.build_generator = BuildGenerator(
            reference, os_name, gcc_versions, apple_clang_versions,
            clang_versions, visual_versions, visual_runtimes, visual_toolsets,
            vs10_x86_64_enabled, mingw_configurations, archs, allow_gcc_minors,
            build_types, options, cppstds)

        self.build_policy = (build_policy
                             or self.ci_manager.get_commit_build_policy()
                             or split_colon_env("CONAN_BUILD_POLICY"))
        if isinstance(self.build_policy, list):
            self.build_policy = ",".join(self.build_policy)

        self.sudo_docker_command = ""
        if "CONAN_DOCKER_USE_SUDO" in os.environ:
            self.sudo_docker_command = "sudo -E" if get_bool_from_env(
                "CONAN_DOCKER_USE_SUDO") else ""
        elif platform.system() != "Windows":
            self.sudo_docker_command = "sudo -E"

        self.sudo_pip_command = ""
        if "CONAN_PIP_USE_SUDO" in os.environ:
            self.sudo_pip_command = "sudo -E" if get_bool_from_env(
                "CONAN_PIP_USE_SUDO") else ""
        elif platform.system(
        ) != "Windows" and self._docker_image and 'conanio/' not in str(
                self._docker_image):
            self.sudo_pip_command = "sudo -E"
        self.pip_command = os.getenv("CONAN_PIP_COMMAND", "pip")
        pip_found = True if tools.os_info.is_windows else tools.which(
            self.pip_command)
        if not pip_found or not "pip" in self.pip_command:
            raise Exception(
                "CONAN_PIP_COMMAND: '{}' is not a valid pip command.".format(
                    self.pip_command))
        self.docker_pip_command = os.getenv("CONAN_DOCKER_PIP_COMMAND", "pip")

        self.docker_shell = docker_shell or os.getenv("CONAN_DOCKER_SHELL")

        if self.is_wcow:
            if self.docker_conan_home is None:
                self.docker_conan_home = "C:/Users/ContainerAdministrator"
                self.docker_shell = docker_shell or "cmd /C"
        else:
            if self.docker_conan_home is None:
                self.docker_conan_home = "/home/conan"
                self.docker_shell = docker_shell or "/bin/sh -c"

        self.docker_platform_param = ""
        self.lcow_user_workaround = ""

        if self.is_lcow:
            self.docker_platform_param = "--platform=linux"
            # With LCOW, Docker doesn't respect USER directive in dockerfile yet
            self.lcow_user_workaround = "sudo su conan && "

        self.exclude_vcvars_precommand = exclude_vcvars_precommand or \
                                          get_bool_from_env("CONAN_EXCLUDE_VCVARS_PRECOMMAND")
        self._docker_image_skip_update = docker_image_skip_update or \
                                          get_bool_from_env("CONAN_DOCKER_IMAGE_SKIP_UPDATE")
        self._docker_image_skip_pull = docker_image_skip_pull or \
                                        get_bool_from_env("CONAN_DOCKER_IMAGE_SKIP_PULL")

        self.runner = runner or os.system
        self.output_runner = ConanOutputRunner()

        self.docker_run_options = docker_run_options or split_colon_env(
            "CONAN_DOCKER_RUN_OPTIONS")
        if isinstance(self.docker_run_options, list):
            self.docker_run_options = " ".join(self.docker_run_options)

        self.docker_entry_script = docker_entry_script or os.getenv(
            "CONAN_DOCKER_ENTRY_SCRIPT")

        self.pip_install = pip_install or split_colon_env("CONAN_PIP_INSTALL")

        self.upload_dependencies = upload_dependencies or split_colon_env(
            "CONAN_UPLOAD_DEPENDENCIES") or ""
        if isinstance(self.upload_dependencies, list):
            self.upload_dependencies = ",".join(self.upload_dependencies)
        if "all" in self.upload_dependencies and self.upload_dependencies != "all":
            raise Exception(
                "Upload dependencies only accepts or 'all' or package references. Do not mix both!"
            )

        self.update_dependencies = update_dependencies or get_bool_from_env(
            "CONAN_UPDATE_DEPENDENCIES")

        if self.channel:
            os.environ["CONAN_CHANNEL"] = self.channel

        if docker_32_images is not None:
            self.docker_32_images = docker_32_images
        else:
            self.docker_32_images = os.getenv("CONAN_DOCKER_32_IMAGES", False)

        self.force_selinux = force_selinux or get_bool_from_env(
            "CONAN_FORCE_SELINUX")
        self.curpage = curpage or os.getenv("CONAN_CURRENT_PAGE", 1)
        self.total_pages = total_pages or os.getenv("CONAN_TOTAL_PAGES", 1)

        self.conan_pip_package = os.getenv("CONAN_PIP_PACKAGE",
                                           "conan==%s" % conan_version)
        if self.conan_pip_package in ("0", "False"):
            self.conan_pip_package = ""
        self.vs10_x86_64_enabled = vs10_x86_64_enabled

        self.builds_in_current_page = []

        self.test_folder = test_folder or os.getenv("CPT_TEST_FOLDER")

        self.config_url = config_url or os.getenv("CONAN_CONFIG_URL")

        self.skip_recipe_export = skip_recipe_export or \
                                     get_bool_from_env("CONAN_SKIP_RECIPE_EXPORT")
        self.config_args = config_args or os.getenv("CONAN_CONFIG_ARGS")

        self.lockfile = lockfile or os.getenv("CONAN_LOCKFILE")

        def valid_pair(var, value):
            return (isinstance(value, six.string_types)
                    or isinstance(value, bool) or isinstance(value, list)
                    ) and not var.startswith("_") and "password" not in var

        with self.printer.foldable_output("local_vars"):
            self.printer.print_dict({
                var: value
                for var, value in self.__dict__.items()
                if valid_pair(var, value)
            })

        self._newest_supported_conan_version = Version(
            NEWEST_CONAN_SUPPORTED).minor(fill=False)
        self._client_conan_version = conan_version

    def _check_conan_version(self):
        tmp = self._newest_supported_conan_version
        if Version(self._client_conan_version).minor(fill=False) > tmp:
            msg = "Conan/CPT version mismatch. Conan version installed: " \
                  "%s . This version of CPT supports only Conan < %s" \
                  "" % (self._client_conan_version, str(tmp))
            self.printer.print_message(msg)
            raise Exception(msg)

    # For Docker on Windows, including Linux containers on Windows
    @property
    def is_lcow(self):
        return self.container_os == "linux" and platform.system() == "Windows"

    @property
    def is_wcow(self):
        return self.container_os == "windows" and platform.system(
        ) == "Windows"

    @property
    def container_os(self):
        # CONAN_DOCKER_PLATFORM=linux must be specified for LCOW
        if self.use_docker:
            if "CONAN_DOCKER_PLATFORM" in os.environ:
                return os.getenv("CONAN_DOCKER_PLATFORM", "windows").lower()
            else:
                return "windows"
        else:
            return ""

    @property
    def packages_summary(self):
        return self._packages_summary

    def save_packages_summary(self, file):
        self.printer.print_message("Saving packages summary to " + file)
        import json
        import datetime

        def default(o):
            if isinstance(o, (datetime.date, datetime.datetime)):
                return o.isoformat()

        with open(file, 'w') as outfile:
            json.dump(self.packages_summary, outfile, default=default)

    @property
    def items(self):
        return self._builds

    @items.setter
    def items(self, confs):
        self.builds = confs

    @property
    def builds(self):
        # Retrocompatibility iterating
        self.printer.print_message(
            "WARNING",
            "\n\n\n******* ITERATING THE CONAN_PACKAGE_TOOLS BUILDS WITH "
            ".builds is deprecated use '.items' instead (unpack 5 elements: "
            "settings, options, env_vars, build_requires, reference  *******"
            "**\n\n\n")
        return [elem[0:4] for elem in self._builds]

    @builds.setter
    def builds(self, confs):
        """For retro compatibility directly assigning builds"""
        self._named_builds = {}
        self._builds = []
        for values in confs:
            if len(values) == 2:
                self._builds.append(
                    BuildConf(values[0], values[1], {}, {}, self.reference))
            elif len(values) == 4:
                self._builds.append(
                    BuildConf(values[0], values[1], values[2], values[3],
                              self.reference))
            elif len(values) != 5:
                raise Exception(
                    "Invalid build configuration, has to be a tuple of "
                    "(settings, options, env_vars, build_requires, reference)")
            else:
                self._builds.append(BuildConf(*values))

    @property
    def named_builds(self):
        return self._named_builds

    @named_builds.setter
    def named_builds(self, confs):
        self._builds = []
        self._named_builds = {}
        for key, pages in confs.items():
            for values in pages:
                if len(values) == 2:
                    bc = BuildConf(values[0], values[1], {}, {},
                                   self.reference)
                    self._named_builds.setdefault(key, []).append(bc)
                elif len(values) == 4:
                    bc = BuildConf(values[0], values[1], values[2], values[3],
                                   self.reference)
                    self._named_builds.setdefault(key, []).append(bc)
                elif len(values) != 5:
                    raise Exception(
                        "Invalid build configuration, has to be a tuple of "
                        "(settings, options, env_vars, build_requires, reference)"
                    )
                else:
                    self._named_builds.setdefault(key, []).append(
                        BuildConf(*values))

    def login(self, remote_name):
        self.auth_manager.login(remote_name)

    def add_common_builds(self,
                          shared_option_name=None,
                          pure_c=True,
                          dll_with_static_runtime=False,
                          reference=None,
                          header_only=True,
                          build_all_options_values=None):
        if reference:
            if "@" in reference:
                reference = ConanFileReference.loads(reference)
            else:
                name, version = reference.split("/")
                reference = ConanFileReference(name, version, self.username,
                                               self.channel)
        else:
            reference = self.reference

        if not reference:
            raise Exception(
                "Specify a CONAN_REFERENCE or name and version fields in the recipe"
            )

        if shared_option_name is None:
            env_shared_option_name = os.getenv("CONAN_SHARED_OPTION_NAME",
                                               None)
            shared_option_name = env_shared_option_name if str(
                env_shared_option_name).lower() != "false" else False

        build_all_options_values = build_all_options_values or split_colon_env(
            "CONAN_BUILD_ALL_OPTIONS_VALUES") or []
        if not isinstance(build_all_options_values, list):
            raise Exception(
                "'build_all_options_values' must be a list. e.g. ['foo:opt', 'foo:bar']"
            )

        conanfile = None
        if os.path.exists(os.path.join(self.cwd, self.conanfile)):
            conanfile = load_cf_class(os.path.join(self.cwd, self.conanfile),
                                      self.conan_api)

        header_only_option = None
        if conanfile:
            if hasattr(
                    conanfile, "options"
            ) and conanfile.options and "header_only" in conanfile.options:
                header_only_option = "%s:header_only" % reference.name

        if shared_option_name is None:
            if conanfile:
                if hasattr(
                        conanfile, "options"
                ) and conanfile.options and "shared" in conanfile.options:
                    shared_option_name = "%s:shared" % reference.name

        # filter only valid options
        raw_options_for_building = [
            opt[opt.find(":") + 1:] for opt in build_all_options_values
        ]
        for raw_option in reversed(raw_options_for_building):
            if hasattr(conanfile, "options") and conanfile.options and \
               not isinstance(conanfile.options.get(raw_option), list):
                raw_options_for_building.remove(raw_option)
        if raw_options_for_building and conanfile:
            # get option and its values
            cloned_options = copy.copy(conanfile.options)
            for key, value in conanfile.options.items():
                if key == "shared" and shared_option_name:
                    continue
                elif key not in raw_options_for_building:
                    del cloned_options[key]
            cloned_options2 = {}
            for key, value in cloned_options.items():
                # add package reference to the option name
                if not key.startswith("{}:".format(reference.name)):
                    cloned_options2["{}:{}".format(reference.name,
                                                   key)] = value
            # combine all options x values (cartesian product)
            build_all_options_values = [
                dict(zip(cloned_options2, v))
                for v in product(*cloned_options2.values())
            ]

        builds = self.build_generator.get_builds(pure_c, shared_option_name,
                                                 dll_with_static_runtime,
                                                 reference,
                                                 build_all_options_values)

        if header_only_option and header_only:
            if conanfile.default_options.get("header_only"):
                cloned_builds = copy.deepcopy(builds)
                for settings, options, env_vars, build_requires, reference in cloned_builds:
                    options.update({header_only_option: False})
                builds.extend(cloned_builds)
            else:
                settings, options, env_vars, build_requires, reference = builds[
                    0]
                cloned_options = copy.copy(options)
                cloned_options.update({header_only_option: True})
                builds.append(
                    BuildConf(copy.copy(settings), cloned_options,
                              copy.copy(env_vars), copy.copy(build_requires),
                              reference))

        self._builds.extend(builds)

    def add(self,
            settings=None,
            options=None,
            env_vars=None,
            build_requires=None,
            reference=None):
        settings = settings or {}
        options = options or {}
        env_vars = env_vars or {}
        build_requires = build_requires or {}
        if reference:
            reference = ConanFileReference.loads(
                "%s@%s/%s" % (reference, self.username, self.channel))
        reference = reference or self.reference
        self._builds.append(
            BuildConf(settings, options, env_vars, build_requires, reference))

    def remove_build_if(self, predicate):
        filtered_builds = []
        for build in self.items:
            if not predicate(build):
                filtered_builds.append(build)

        self._builds = filtered_builds

    def update_build_if(self,
                        predicate,
                        new_settings=None,
                        new_options=None,
                        new_env_vars=None,
                        new_build_requires=None,
                        new_reference=None):
        updated_builds = []
        for build in self.items:
            if predicate(build):
                if new_settings:
                    build.settings.update(new_settings)
                if new_options:
                    build.options.update(new_options)
                if new_build_requires:
                    build.build_requires.update(new_build_requires)
                if new_env_vars:
                    build.env_vars.update(new_env_vars)
                if new_reference:
                    build.reference = new_reference
            updated_builds.append(build)
        self._builds = updated_builds

    def run(self, base_profile_name=None, summary_file=None):
        self._check_conan_version()

        env_vars = self.auth_manager.env_vars()
        env_vars.update(self.remotes_manager.env_vars())
        with tools.environment_append(env_vars):
            self.printer.print_message("Running builds...")
            if self.ci_manager.skip_builds():
                self.printer.print_message(
                    "Skipped builds due [skip ci] commit message")
                return 99
            if not self.skip_check_credentials and self._upload_enabled():
                self.auth_manager.login(
                    self.remotes_manager.upload_remote_name)
            if self.conan_pip_package and not self.use_docker:
                with self.printer.foldable_output("pip_update"):
                    self.runner('%s %s install -q %s' %
                                (self.sudo_pip_command, self.pip_command,
                                 self.conan_pip_package))
                    if self.pip_install:
                        packages = " ".join(self.pip_install)
                        self.printer.print_message(
                            "Install extra python packages: {}".format(
                                packages))
                        self.runner('%s %s install -q %s' %
                                    (self.sudo_pip_command, self.pip_command,
                                     packages))

            self.run_builds(base_profile_name=base_profile_name)

        summary_file = summary_file or os.getenv("CPT_SUMMARY_FILE", None)
        if summary_file:
            self.save_packages_summary(summary_file)

    def _upload_enabled(self):
        if not self.remotes_manager.upload_remote_name:
            return False

        if not self.auth_manager.credentials_ready(
                self.remotes_manager.upload_remote_name):
            return False

        if self.upload_only_when_tag and not self.ci_manager.is_tag():
            self.printer.print_message("Skipping upload, not tag branch")
            return False

        st_channel = self.stable_channel or "stable"
        if self.upload_only_when_stable and self.channel != st_channel and not self.upload_only_when_tag:
            self.printer.print_message("Skipping upload, not stable channel")
            return False

        if not os.getenv("CONAN_TEST_SUITE", False):
            if self.ci_manager.is_pull_request():
                # PENDING! can't found info for gitlab/bamboo
                self.printer.print_message(
                    "Skipping upload, this is a Pull Request")
                return False

        def raise_error(field):
            raise Exception("Upload not possible, '%s' is missing!" % field)

        if not self.channel and "@" not in self.partial_reference:
            raise_error("channel")
        if not self.username and "@" not in self.partial_reference:
            raise_error("username")

        return True

    def run_builds(self,
                   curpage=None,
                   total_pages=None,
                   base_profile_name=None):
        if len(self.named_builds) > 0 and len(self.items) > 0:
            raise Exception(
                "Both bulk and named builds are set. Only one is allowed.")

        self.builds_in_current_page = []
        if len(self.items) > 0:
            curpage = curpage or int(self.curpage)
            total_pages = total_pages or int(self.total_pages)
            for index, build in enumerate(self.items):
                if curpage is None or total_pages is None or (
                        index % total_pages) + 1 == curpage:
                    self.builds_in_current_page.append(build)
        elif len(self.named_builds) > 0:
            curpage = curpage or self.curpage
            if curpage not in self.named_builds:
                raise Exception("No builds set for page %s" % curpage)
            for build in self.named_builds[curpage]:
                self.builds_in_current_page.append(build)

        self.printer.print_current_page(curpage, total_pages)
        self.printer.print_jobs(self.builds_in_current_page)

        pulled_docker_images = defaultdict(lambda: False)
        skip_recipe_export = False

        # FIXME: Remove in Conan 1.3, https://github.com/conan-io/conan/issues/2787
        for index, build in enumerate(self.builds_in_current_page):
            self.printer.print_message(
                "Build: %s/%s" % (index + 1, len(self.builds_in_current_page)))
            base_profile_name = base_profile_name or os.getenv(
                "CONAN_BASE_PROFILE")
            if base_profile_name:
                self.printer.print_message(
                    "**************************************************")
                self.printer.print_message("Using specified default "
                                           "base profile: %s" %
                                           base_profile_name)
                self.printer.print_message(
                    "**************************************************")

            profile_text, base_profile_text = get_profiles(
                self.client_cache, build, base_profile_name)
            if not self.use_docker:
                profile_abs_path = save_profile_to_tmp(profile_text)
                r = CreateRunner(
                    profile_abs_path,
                    build.reference,
                    self.conan_api,
                    self.uploader,
                    exclude_vcvars_precommand=self.exclude_vcvars_precommand,
                    build_policy=self.build_policy,
                    runner=self.runner,
                    cwd=self.cwd,
                    printer=self.printer,
                    upload=self._upload_enabled(),
                    upload_only_recipe=self.upload_only_recipe,
                    test_folder=self.test_folder,
                    config_url=self.config_url,
                    config_args=self.config_args,
                    upload_dependencies=self.upload_dependencies,
                    conanfile=self.conanfile,
                    lockfile=self.lockfile,
                    skip_recipe_export=skip_recipe_export,
                    update_dependencies=self.update_dependencies)
                r.run()
                self._packages_summary.append({
                    "configuration": build,
                    "package": r.results
                })
            else:
                docker_image = self._get_docker_image(build)
                r = DockerCreateRunner(
                    profile_text,
                    base_profile_text,
                    base_profile_name,
                    build.reference,
                    conan_pip_package=self.conan_pip_package,
                    docker_image=docker_image,
                    sudo_docker_command=self.sudo_docker_command,
                    sudo_pip_command=self.sudo_pip_command,
                    docker_image_skip_update=self._docker_image_skip_update,
                    docker_image_skip_pull=self._docker_image_skip_pull,
                    build_policy=self.build_policy,
                    always_update_conan_in_docker=self._update_conan_in_docker,
                    upload=self._upload_enabled(),
                    upload_retry=self.upload_retry,
                    upload_only_recipe=self.upload_only_recipe,
                    runner=self.runner,
                    docker_shell=self.docker_shell,
                    docker_conan_home=self.docker_conan_home,
                    docker_platform_param=self.docker_platform_param,
                    docker_run_options=self.docker_run_options,
                    lcow_user_workaround=self.lcow_user_workaround,
                    test_folder=self.test_folder,
                    pip_install=self.pip_install,
                    docker_pip_command=self.docker_pip_command,
                    config_url=self.config_url,
                    config_args=self.config_args,
                    printer=self.printer,
                    upload_dependencies=self.upload_dependencies,
                    conanfile=self.conanfile,
                    lockfile=self.lockfile,
                    force_selinux=self.force_selinux,
                    skip_recipe_export=skip_recipe_export,
                    update_dependencies=self.update_dependencies)

                r.run(pull_image=not pulled_docker_images[docker_image],
                      docker_entry_script=self.docker_entry_script)
                pulled_docker_images[docker_image] = True

            skip_recipe_export = self.skip_recipe_export

    def _get_docker_image(self, build):
        if self._docker_image:
            docker_image = self._docker_image
        else:
            compiler_name = build.settings.get("compiler", "")
            compiler_version = build.settings.get("compiler.version", "")
            docker_image = self._autodetect_docker_base_image(
                compiler_name, compiler_version)

        arch = build.settings.get("arch", "") or build.settings.get(
            "arch_build", "")
        if self.docker_32_images and arch == "x86":
            build.settings["arch_build"] = "x86"
            docker_arch_suffix = "x86"
        elif arch != "x86" and arch != "x86_64":
            docker_arch_suffix = arch
        else:
            docker_arch_suffix = None
        if docker_arch_suffix and "-" not in docker_image:
            docker_image = "%s-%s" % (docker_image, docker_arch_suffix)

        return docker_image

    @staticmethod
    def _autodetect_docker_base_image(compiler_name, compiler_version):
        if compiler_name not in ["clang", "gcc"]:
            raise Exception("Docker image cannot be autodetected for "
                            "the compiler %s" % compiler_name)

        if compiler_name == "gcc" and Version(compiler_version) > Version("5"):
            compiler_version = Version(compiler_version).major(fill=False)

        return "conanio/%s%s" % (compiler_name,
                                 compiler_version.replace(".", ""))

    def _get_channel(self, specified_channel, stable_channel, upload_when_tag):
        if not specified_channel:
            return

        if self.stable_branch_pattern:
            stable_patterns = [self.stable_branch_pattern]
        else:
            stable_patterns = ["master$", "release*", "stable*"]

        branch = self.ci_manager.get_branch()
        self.printer.print_message("Branch detected", branch)

        for pattern in stable_patterns:
            prog = re.compile(pattern)

            if branch and prog.match(branch):
                self.printer.print_message(
                    "Info",
                    "Redefined channel by CI branch matching with '%s', "
                    "setting CONAN_CHANNEL to '%s'" %
                    (pattern, stable_channel))
                return stable_channel

        if self.ci_manager.is_tag() and upload_when_tag:
            self.printer.print_message(
                "Info", "Redefined channel by branch tag, "
                "setting CONAN_CHANNEL to '%s'" % stable_channel)
            return stable_channel

        return specified_channel

    def _get_specified_channel(self, channel, reference):
        partial_reference = reference or os.getenv("CONAN_REFERENCE", None)
        specified_channel = None
        # without name/channel e.g. zlib/1.2.11@
        if partial_reference:
            if "@" in partial_reference:
                specified_channel = channel or os.getenv("CONAN_CHANNEL", None)
            else:
                specified_channel = channel or os.getenv(
                    "CONAN_CHANNEL", "testing")
                specified_channel = specified_channel.rstrip()
        else:
            if self.username:
                specified_channel = channel or os.getenv(
                    "CONAN_CHANNEL", "testing")
                specified_channel = specified_channel.rstrip()
            else:
                specified_channel = channel or os.getenv("CONAN_CHANNEL", None)
        return self._get_channel(specified_channel, self.stable_channel,
                                 self.upload_only_when_tag)
Пример #2
0
class ConanMultiPackager(object):
    """ Help to generate common builds (setting's combinations), adjust the environment,
    and run conan create command in docker containers"""
    def __init__(self,
                 args=None,
                 username=None,
                 channel=None,
                 runner=None,
                 gcc_versions=None,
                 visual_versions=None,
                 visual_runtimes=None,
                 apple_clang_versions=None,
                 archs=None,
                 use_docker=None,
                 curpage=None,
                 total_pages=None,
                 docker_image=None,
                 reference=None,
                 password=None,
                 remotes=None,
                 upload=None,
                 stable_branch_pattern=None,
                 vs10_x86_64_enabled=False,
                 mingw_configurations=None,
                 stable_channel=None,
                 platform_info=None,
                 upload_retry=None,
                 clang_versions=None,
                 login_username=None,
                 upload_only_when_stable=None,
                 build_types=None,
                 skip_check_credentials=False,
                 allow_gcc_minors=False,
                 exclude_vcvars_precommand=False,
                 docker_image_skip_update=False,
                 docker_image_skip_pull=False,
                 docker_entry_script=None,
                 docker_32_images=None,
                 build_policy=None,
                 always_update_conan_in_docker=False,
                 conan_api=None,
                 client_cache=None,
                 ci_manager=None,
                 out=None,
                 test_folder=None):

        self.printer = Printer(out)
        self.printer.print_rule()
        self.printer.print_ascci_art()

        if not conan_api:
            self.conan_api, self.client_cache, _ = Conan.factory()
        else:
            self.conan_api = conan_api
            self.client_cache = client_cache

        self.ci_manager = ci_manager or CIManager(self.printer)
        self.remotes_manager = RemotesManager(self.conan_api, self.printer,
                                              remotes, upload)
        self.username = username or os.getenv("CONAN_USERNAME", None)

        if not self.username:
            raise Exception(
                "Instance ConanMultiPackage with 'username' parameter or use "
                "CONAN_USERNAME env variable")
        self.skip_check_credentials = skip_check_credentials or \
                                      os.getenv("CONAN_SKIP_CHECK_CREDENTIALS", False)

        self.auth_manager = AuthManager(
            self.conan_api,
            self.printer,
            login_username,
            password,
            default_username=self.username,
            skip_check_credentials=self.skip_check_credentials)
        self.uploader = Uploader(self.conan_api, self.remotes_manager,
                                 self.auth_manager, self.printer)

        self._builds = []
        self._named_builds = {}

        self._update_conan_in_docker = (
            always_update_conan_in_docker
            or os.getenv("CONAN_ALWAYS_UPDATE_CONAN_DOCKER", False))

        self._platform_info = platform_info or PlatformInfo()

        self.stable_branch_pattern = stable_branch_pattern or \
                                     os.getenv("CONAN_STABLE_BRANCH_PATTERN", None)
        self.specified_channel = channel or os.getenv("CONAN_CHANNEL",
                                                      "testing")
        self.specified_channel = self.specified_channel.rstrip()
        self.stable_channel = stable_channel or os.getenv(
            "CONAN_STABLE_CHANNEL", "stable")
        self.stable_channel = self.stable_channel.rstrip()
        self.channel = self._get_channel(self.specified_channel,
                                         self.stable_channel)
        self.partial_reference = reference or os.getenv(
            "CONAN_REFERENCE", None)

        if self.partial_reference:
            if "@" in self.partial_reference:
                self.reference = ConanFileReference.loads(
                    self.partial_reference)
            else:
                name, version = self.partial_reference.split("/")
                self.reference = ConanFileReference(name, version,
                                                    self.username,
                                                    self.channel)
        else:
            if not os.path.exists("conanfile.py"):
                raise Exception(
                    "Conanfile not found, specify a 'reference' parameter with name and version"
                )
            conanfile = load_conanfile_class("./conanfile.py")
            name, version = conanfile.name, conanfile.version
            if name and version:
                self.reference = ConanFileReference(name, version,
                                                    self.username,
                                                    self.channel)
            else:
                self.reference = None

        # If CONAN_DOCKER_IMAGE is speified, then use docker is True
        self.use_docker = (use_docker or os.getenv("CONAN_USE_DOCKER", False)
                           or os.getenv("CONAN_DOCKER_IMAGE",
                                        None) is not None)

        os_name = self._platform_info.system(
        ) if not self.use_docker else "Linux"
        self.build_generator = BuildGenerator(
            reference, os_name, gcc_versions, apple_clang_versions,
            clang_versions, visual_versions, visual_runtimes,
            vs10_x86_64_enabled, mingw_configurations, archs, allow_gcc_minors,
            build_types)

        build_policy = (build_policy
                        or self.ci_manager.get_commit_build_policy()
                        or os.getenv("CONAN_BUILD_POLICY", None))

        if build_policy:
            if build_policy.lower() not in ("never", "outdated", "missing"):
                raise Exception(
                    "Invalid build policy, valid values: never, outdated, missing"
                )

        self.build_policy = build_policy

        self.sudo_docker_command = ""
        if "CONAN_DOCKER_USE_SUDO" in os.environ:
            self.sudo_docker_command = "sudo -E" if get_bool_from_env(
                "CONAN_DOCKER_USE_SUDO") else ""
        elif platform.system() != "Windows":
            self.sudo_docker_command = "sudo -E"

        self.sudo_pip_command = ""
        if "CONAN_PIP_USE_SUDO" in os.environ:
            self.sudo_pip_command = "sudo -E" if get_bool_from_env(
                "CONAN_PIP_USE_SUDO") else ""
        elif platform.system() != "Windows":
            self.sudo_pip_command = "sudo -E"

        self.docker_shell = ""
        self.docker_conan_home = ""

        if self.is_wcow:
            self.docker_conan_home = "C:/Users/ContainerAdministrator"
            self.docker_shell = "cmd /C"
        else:
            self.docker_conan_home = "/home/conan"
            self.docker_shell = "/bin/sh -c"

        self.docker_platform_param = ""
        self.lcow_user_workaround = ""

        if self.is_lcow:
            self.docker_platform_param = "--platform=linux"
            # With LCOW, Docker doesn't respect USER directive in dockerfile yet
            self.lcow_user_workaround = "sudo su conan && "

        self.exclude_vcvars_precommand = (
            exclude_vcvars_precommand
            or os.getenv("CONAN_EXCLUDE_VCVARS_PRECOMMAND", False))
        self._docker_image_skip_update = (
            docker_image_skip_update
            or os.getenv("CONAN_DOCKER_IMAGE_SKIP_UPDATE", False))
        self._docker_image_skip_pull = (docker_image_skip_pull or os.getenv(
            "CONAN_DOCKER_IMAGE_SKIP_PULL", False))

        self.runner = runner or os.system
        self.output_runner = ConanOutputRunner()
        self.args = " ".join(args) if args else " ".join(sys.argv[1:])

        # Upload related variables
        self.upload_retry = upload_retry or os.getenv("CONAN_UPLOAD_RETRY", 3)

        if upload_only_when_stable is not None:
            self.upload_only_when_stable = upload_only_when_stable
        else:
            self.upload_only_when_stable = get_bool_from_env(
                "CONAN_UPLOAD_ONLY_WHEN_STABLE")

        self.docker_entry_script = docker_entry_script or os.getenv(
            "CONAN_DOCKER_ENTRY_SCRIPT")

        os.environ["CONAN_CHANNEL"] = self.channel

        # If CONAN_DOCKER_IMAGE is speified, then use docker is True
        self.use_docker = use_docker or os.getenv("CONAN_USE_DOCKER", False) or \
                          (os.getenv("CONAN_DOCKER_IMAGE", None) is not None)

        if docker_32_images is not None:
            self.docker_32_images = docker_32_images
        else:
            self.docker_32_images = os.getenv("CONAN_DOCKER_32_IMAGES", False)

        self.curpage = curpage or os.getenv("CONAN_CURRENT_PAGE", 1)
        self.total_pages = total_pages or os.getenv("CONAN_TOTAL_PAGES", 1)
        self._docker_image = docker_image or os.getenv("CONAN_DOCKER_IMAGE",
                                                       None)

        self.conan_pip_package = os.getenv("CONAN_PIP_PACKAGE",
                                           "conan==%s" % client_version)
        if self.conan_pip_package in ("0", "False"):
            self.conan_pip_package = False
        self.vs10_x86_64_enabled = vs10_x86_64_enabled

        self.builds_in_current_page = []

        self.test_folder = test_folder or os.getenv("CPT_TEST_FOLDER", None)

        def valid_pair(var, value):
            return (isinstance(value, six.string_types)
                    or isinstance(value, bool) or isinstance(value, list)
                    ) and not var.startswith("_") and "password" not in var

        with self.printer.foldable_output("local_vars"):
            self.printer.print_dict({
                var: value
                for var, value in self.__dict__.items()
                if valid_pair(var, value)
            })

    # For Docker on Windows, including Linux containers on Windows
    @property
    def is_lcow(self):
        return self.container_os == "linux" and platform.system() == "Windows"

    @property
    def is_wcow(self):
        return self.container_os == "windows" and platform.system(
        ) == "Windows"

    @property
    def container_os(self):
        # CONAN_DOCKER_PLATFORM=linux must be specified for LCOW
        if self.use_docker:
            if "CONAN_DOCKER_PLATFORM" in os.environ:
                return os.getenv("CONAN_DOCKER_PLATFORM", "windows").lower()
            else:
                return "windows"
        else:
            return ""

    @property
    def items(self):
        return self._builds

    @items.setter
    def items(self, confs):
        self.builds = confs

    @property
    def builds(self):
        # Retrocompatibility iterating
        self.printer.print_message(
            "WARNING",
            "\n\n\n******* ITERATING THE CONAN_PACKAGE_TOOLS BUILDS WITH "
            ".builds is deprecated use .items() instead (unpack 5 elements: "
            "settings, options, env_vars, build_requires, reference  *******"
            "**\n\n\n")
        return [elem[0:4] for elem in self._builds]

    @builds.setter
    def builds(self, confs):
        """For retro compatibility directly assigning builds"""
        self._named_builds = {}
        self._builds = []
        for values in confs:
            if len(values) == 2:
                self._builds.append(
                    BuildConf(values[0], values[1], {}, {}, self.reference))
            elif len(values) == 4:
                self._builds.append(
                    BuildConf(values[0], values[1], values[2], values[3],
                              self.reference))
            elif len(values) != 5:
                raise Exception(
                    "Invalid build configuration, has to be a tuple of "
                    "(settings, options, env_vars, build_requires, reference)")
            else:
                self._builds.append(BuildConf(*values))

    @property
    def named_builds(self):
        return self._named_builds

    @named_builds.setter
    def named_builds(self, confs):
        self._builds = []
        self._named_builds = {}
        for key, pages in confs.items():
            for values in pages:
                if len(values) == 2:
                    bc = BuildConf(values[0], values[1], {}, {},
                                   self.reference)
                    self._named_builds.setdefault(key, []).append(bc)
                elif len(values) == 4:
                    bc = BuildConf(values[0], values[1], values[2], values[3],
                                   self.reference)
                    self._named_builds.setdefault(key, []).append(bc)
                elif len(values) != 5:
                    raise Exception(
                        "Invalid build configuration, has to be a tuple of "
                        "(settings, options, env_vars, build_requires, reference)"
                    )
                else:
                    self._named_builds.setdefault(key, []).append(
                        BuildConf(*values))

    def add_common_builds(self,
                          shared_option_name=None,
                          pure_c=True,
                          dll_with_static_runtime=False,
                          reference=None):

        if not reference and not self.reference:
            raise Exception(
                "Specify a CONAN_REFERENCE or name and version fields in the recipe"
            )

        if shared_option_name is None:
            if os.path.exists("conanfile.py"):
                conanfile = load_conanfile_class("./conanfile.py")
                if hasattr(conanfile,
                           "options") and "shared" in conanfile.options:
                    shared_option_name = "%s:shared" % self.reference.name

        tmp = self.build_generator.get_builds(pure_c, shared_option_name,
                                              dll_with_static_runtime,
                                              reference or self.reference)
        self._builds.extend(tmp)

    def add(self,
            settings=None,
            options=None,
            env_vars=None,
            build_requires=None,
            reference=None):
        settings = settings or {}
        options = options or {}
        env_vars = env_vars or {}
        build_requires = build_requires or {}
        if reference:
            reference = ConanFileReference.loads(
                "%s@%s/%s" % (reference, self.username, self.channel))
        reference = reference or self.reference
        self._builds.append(
            BuildConf(settings, options, env_vars, build_requires, reference))

    def run(self, base_profile_name=None):
        env_vars = self.auth_manager.env_vars()
        env_vars.update(self.remotes_manager.env_vars())
        with tools.environment_append(env_vars):
            self.printer.print_message("Running builds...")
            if self.ci_manager.skip_builds():
                print("Skipped builds due [skip ci] commit message")
                return 99
            if not self.skip_check_credentials and self._upload_enabled():
                self.remotes_manager.add_remotes_to_conan()
                self.auth_manager.login(
                    self.remotes_manager.upload_remote_name)
            if self.conan_pip_package and not self.use_docker:
                with self.printer.foldable_output("pip_update"):
                    self.runner(
                        '%s pip install %s' %
                        (self.sudo_pip_command, self.conan_pip_package))

            self.run_builds(base_profile_name=base_profile_name)

    def _upload_enabled(self):
        if not self.remotes_manager.upload_remote_name:
            return False

        if not self.auth_manager.credentials_ready(
                self.remotes_manager.upload_remote_name):
            return False

        st_channel = self.stable_channel or "stable"
        if self.upload_only_when_stable and self.channel != st_channel:
            self.printer.print_message("Skipping upload, not stable channel")
            return False

        if not os.getenv("CONAN_TEST_SUITE", False):
            if self.ci_manager.is_pull_request():
                # PENDING! can't found info for gitlab/bamboo
                self.printer.print_message(
                    "Skipping upload, this is a Pull Request")
                return False

        def raise_error(field):
            raise Exception("Upload not possible, '%s' is missing!" % field)

        if not self.channel:
            raise_error("channel")
        if not self.username:
            raise_error("username")

        return True

    def run_builds(self,
                   curpage=None,
                   total_pages=None,
                   base_profile_name=None):
        if len(self.named_builds) > 0 and len(self.items) > 0:
            raise Exception(
                "Both bulk and named builds are set. Only one is allowed.")

        self.builds_in_current_page = []
        if len(self.items) > 0:
            curpage = curpage or int(self.curpage)
            total_pages = total_pages or int(self.total_pages)
            for index, build in enumerate(self.items):
                if curpage is None or total_pages is None or (
                        index % total_pages) + 1 == curpage:
                    self.builds_in_current_page.append(build)
        elif len(self.named_builds) > 0:
            curpage = curpage or self.curpage
            if curpage not in self.named_builds:
                raise Exception("No builds set for page %s" % curpage)
            for build in self.named_builds[curpage]:
                self.builds_in_current_page.append(build)

        self.printer.print_current_page(curpage, total_pages)
        self.printer.print_jobs(self.builds_in_current_page)

        pulled_docker_images = defaultdict(lambda: False)

        # FIXME: Remove in Conan 1.3, https://github.com/conan-io/conan/issues/2787
        abs_folder = os.path.realpath(os.getcwd())
        for build in self.builds_in_current_page:
            base_profile_name = base_profile_name or os.getenv(
                "CONAN_BASE_PROFILE")
            if base_profile_name:
                self.printer.print_message(
                    "**************************************************")
                self.printer.print_message("Using specified default "
                                           "base profile: %s" %
                                           base_profile_name)
                self.printer.print_message(
                    "**************************************************")

            profile_text, base_profile_text = get_profiles(
                self.client_cache, build, base_profile_name)
            if not self.use_docker:
                profile_abs_path = save_profile_to_tmp(profile_text)
                r = CreateRunner(
                    profile_abs_path,
                    build.reference,
                    self.conan_api,
                    self.uploader,
                    args=self.args,
                    exclude_vcvars_precommand=self.exclude_vcvars_precommand,
                    build_policy=self.build_policy,
                    runner=self.runner,
                    abs_folder=abs_folder,
                    printer=self.printer,
                    upload=self._upload_enabled(),
                    test_folder=self.test_folder)
                r.run()
            else:
                docker_image = self._get_docker_image(build)
                r = DockerCreateRunner(
                    profile_text,
                    base_profile_text,
                    base_profile_name,
                    build.reference,
                    args=self.args,
                    conan_pip_package=self.conan_pip_package,
                    docker_image=docker_image,
                    sudo_docker_command=self.sudo_docker_command,
                    sudo_pip_command=self.sudo_pip_command,
                    docker_image_skip_update=self._docker_image_skip_update,
                    docker_image_skip_pull=self._docker_image_skip_pull,
                    build_policy=self.build_policy,
                    always_update_conan_in_docker=self._update_conan_in_docker,
                    upload=self._upload_enabled(),
                    runner=self.runner,
                    docker_shell=self.docker_shell,
                    docker_conan_home=self.docker_conan_home,
                    docker_platform_param=self.docker_platform_param,
                    lcow_user_workaround=self.lcow_user_workaround,
                    test_folder=self.test_folder)

                r.run(pull_image=not pulled_docker_images[docker_image],
                      docker_entry_script=self.docker_entry_script)
                pulled_docker_images[docker_image] = True

    def _get_docker_image(self, build):
        if self._docker_image:
            docker_image = self._docker_image
        else:
            compiler_name = build.settings.get("compiler", "")
            compiler_version = build.settings.get("compiler.version", "")
            docker_image = self._autodetect_docker_base_image(
                compiler_name, compiler_version)

        arch = build.settings.get("arch", "") or build.settings.get(
            "arch_build", "")
        if self.docker_32_images and arch == "x86":
            build.settings["arch_build"] = "x86"
            docker_arch_suffix = "x86"
        elif arch != "x86" and arch != "x86_64":
            docker_arch_suffix = arch
        else:
            docker_arch_suffix = None
        if docker_arch_suffix and "-" not in docker_image:
            docker_image = "%s-%s" % (docker_image, docker_arch_suffix)

        return docker_image

    @staticmethod
    def _autodetect_docker_base_image(compiler_name, compiler_version):
        if compiler_name not in ["clang", "gcc"]:
            raise Exception("Docker image cannot be autodetected for "
                            "the compiler %s" % compiler_name)

        if compiler_name == "gcc" and Version(compiler_version) > Version("5"):
            compiler_version = Version(compiler_version).major(fill=False)

        return "lasote/conan%s%s" % (compiler_name,
                                     compiler_version.replace(".", ""))

    def _get_channel(self, specified_channel, stable_channel):
        if self.stable_branch_pattern:
            stable_patterns = [self.stable_branch_pattern]
        else:
            stable_patterns = ["master$", "release*", "stable*"]

        branch = self.ci_manager.get_branch()
        self.printer.print_message("Branch detected", branch)

        for pattern in stable_patterns:
            prog = re.compile(pattern)

            if branch and prog.match(branch):
                self.printer.print_message(
                    "Info",
                    "Redefined channel by CI branch matching with '%s', "
                    "setting CONAN_CHANNEL to '%s'" %
                    (pattern, stable_channel))
                return stable_channel

        return specified_channel