############################################################################# ## ## Copyright (C) 2022 The Qt Company Ltd. ## Contact: https://www.qt.io/licensing/ ## ## This file is part of the release tools of the Qt Toolkit. ## ## $QT_BEGIN_LICENSE:GPL-EXCEPT$ ## Commercial License Usage ## Licensees holding valid commercial Qt licenses may use this file in ## accordance with the commercial license agreement provided with the ## Software or, alternatively, in accordance with the terms contained in ## a written agreement between you and The Qt Company. For licensing terms ## and conditions see https://www.qt.io/terms-conditions. For further ## information use the contact form at https://www.qt.io/contact-us. ## ## GNU General Public License Usage ## Alternatively, this file may be used under the terms of the GNU ## General Public License version 3 as published by the Free Software ## Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT ## included in the packaging of this file. Please review the following ## information to ensure the GNU General Public License requirements will ## be met: https://www.gnu.org/licenses/gpl-3.0.html. ## ## $QT_END_LICENSE$ ## ############################################################################# import os import re import shlex import shutil import subprocess from abc import ABCMeta, abstractmethod from configparser import ConfigParser, ExtendedInterpolation from functools import lru_cache from pathlib import Path from typing import Any, Callable, Dict, List, Optional, Tuple, Union import yaml import json from conans import ConanFile, Options, tools class QtConanError(Exception): pass class QtConfigureOption(object): """A class to represent a single Qt configure(.bat) option It provides means to convert the Qt configure option to an option accepted by Conan Options class wrapper. """ def __init__(self, name: str, type: str, values: List[Any], default: Any): self.name = name self.type = type self._conan_option_name = self.convert_to_conan_option_name(name) if type == "enum" and set(values) == {"yes", "no"}: self._binary_option = True # matches to Conan option True|False values = [] self._prefix = "-" self._value_delim = "" elif "string" in type.lower() or type in ["enum", "cxxstd", "coverage", "sanitize"]: # these options have a value, e.g. # --zlib=qt (enum type) # --c++std=c++17 (cxxstd type) # --prefix=/foo self._binary_option = False self._prefix = "--" self._value_delim = "=" # exception to the rule if name == "qt-host-path": self._prefix = "-" self._value_delim = " " else: # e.g. -debug (void type) self._binary_option = True self._prefix = "-" self._value_delim = "" if not self._binary_option and not values: self.possible_values = ["ANY"] elif type == "addString": # -make=libs -make=examples <-> -o make="libs;examples" i.e. the possible values # can be randomly selected values in semicolon separated list -> "ANY" self.possible_values = ["ANY"] else: self.possible_values = values if self._binary_option and self.possible_values: raise QtConanError( "A binary option: '{0}' can not contain values: {1}".format( name, self.possible_values ) ) self.default = default @property def binary_option(self) -> bool: return self._binary_option @property def incremental_option(self) -> bool: return self.type == "addString" @property def prefix(self) -> str: return self._prefix @property def value_delim(self) -> str: return self._value_delim @property def conan_option_name(self) -> str: return self._conan_option_name def is_feature(self) -> bool: return self.name.startswith("feature") def convert_to_conan_option_name(self, qt_configure_option: str) -> str: # e.g. '-c++std' -> '-cxxstd' or '-cmake-generator' -> 'cmake_generator' return qt_configure_option.lstrip("-").replace("-", "_").replace("+", "x") def get_conan_option_values(self) -> Any: # The 'None' is added as a possible value. For Conan this means it is not mandatory to pass # this option for the build. if self._binary_option: return [True, False, None] if self.possible_values == ["ANY"]: # For 'ANY' value it can not be a List type for Conan return "ANY" return self.possible_values + [None] # type: ignore def get_default_conan_option_value(self) -> Any: return self.default class QtOptionParser: def __init__(self, recipe_folder: Path) -> None: self.options: List[QtConfigureOption] = [] self.load_configure_options(recipe_folder) self.extra_options: Dict[str, Any] = { "cmake_args_qtbase": "ANY", "android_sdk_version": "ANY", "android_ndk_version": "ANY", } self.extra_options_default_values = { "cmake_args_qtbase": None, "android_sdk_version": None, "android_ndk_version": None, } def load_configure_options(self, recipe_folder: Path) -> None: """Read the configure options and features dynamically via configure(.bat). There are two contexts where the ConanFile is initialized: - 'conan export' i.e. when the conan package is being created from sources (.git) - inside conan's cache when invoking: 'conan install, conan info, conan inspect, ..' """ print("QtOptionParser: load configure options ..") configure_options = recipe_folder / "configure_options.json" configure_features = recipe_folder / "configure_features.txt" if not configure_options.exists() or not configure_features.exists(): # This is when the 'conan export' is called script = Path("configure.bat") if tools.os_info.is_windows else Path("configure") root_path = recipe_folder configure = root_path.joinpath(script).resolve() if not configure.exists(): root_path = root_path.joinpath("..").joinpath("export_source").resolve() if root_path.exists(): configure = root_path.joinpath(script).resolve(strict=True) else: raise QtConanError( "Unable to locate 'configure(.bat)' " "from current context: {0}".format(recipe_folder) ) self.write_configure_options(configure, output_file=configure_options) self.write_configure_features(configure, output_file=configure_features) opt = self.read_configure_options(configure_options) self.set_configure_options(opt["options"]) features = self.read_configure_features(configure_features) self.set_features(feature_name_prefix="feature-", features=features) def write_configure_options(self, configure: Path, output_file: Path) -> None: print("QtOptionParser: writing Qt configure options to: {0}".format(output_file)) cmd = [str(configure), "-write-options-for-conan", str(output_file)] subprocess.run(cmd, check=True, timeout=60 * 2) def read_configure_options(self, input_file: Path) -> Dict[str, Any]: print("QtOptionParser: reading Qt configure options from: {0}".format(input_file)) with open(str(input_file)) as f: return json.load(f) def write_configure_features(self, configure: Path, output_file: Path) -> None: print("QtOptionParser: writing Qt configure features to: {0}".format(output_file)) cmd = [str(configure), "-list-features"] with open(output_file, "w") as f: subprocess.run( cmd, encoding="utf-8", check=True, timeout=60 * 2, stderr=subprocess.STDOUT, stdout=f, ) def read_configure_features(self, input_file: Path) -> List[str]: print("QtOptionParser: reading Qt configure features from: {0}".format(input_file)) with open(str(input_file)) as f: return f.readlines() def set_configure_options(self, configure_options: Dict[str, Any]) -> None: for option_name, field in configure_options.items(): option_type = field.get("type") values: List[str] = field.get("values", []) # For the moment all Options will get 'None' as the default value default = None if not option_type: raise QtConanError( "Qt 'configure(.bat) -write-options-for-conan' produced output " "that is missing 'type'. Unable to set options dynamically. " "Item: {0}".format(option_name) ) if not isinstance(values, list): raise QtConanError("The 'values' field is not a list: {0}".format(option_name)) if option_type == "enum" and not values: raise QtConanError("The enum values are missing for: {0}".format(option_name)) opt = QtConfigureOption( name=option_name, type=option_type, values=values, default=default ) self.options.append(opt) def set_features(self, feature_name_prefix: str, features: List[str]) -> None: for line in features: feature_name = self.parse_feature(line) if feature_name: opt = QtConfigureOption( name=feature_name_prefix + feature_name, type="void", values=[], default=None ) self.options.append(opt) def parse_feature(self, feature_line: str) -> Optional[str]: parts = feature_line.split() # e.g. 'itemmodel ................ ItemViews: Provides the item model for item views' if not len(parts) >= 3: return None if not parts[1].startswith("."): return None return parts[0] def get_qt_conan_options(self) -> Dict[str, Any]: # obtain all the possible configure(.bat) options and map those to # Conan options for the recipe opt: Dict = {} for qt_option in self.options: opt[qt_option.conan_option_name] = qt_option.get_conan_option_values() opt.update(self.extra_options) return opt def get_default_qt_conan_options(self) -> Dict[str, Any]: # set the default option values for each option in case the user or CI does not pass them opt: Dict = {} for qt_option in self.options: opt[qt_option.conan_option_name] = qt_option.get_default_conan_option_value() opt.update(self.extra_options_default_values) return opt def find_qt_option(self, conan_option_name: str) -> QtConfigureOption: for qt_opt in self.options: if conan_option_name == qt_opt.conan_option_name: return qt_opt raise QtConanError("No matching Qt configure option for: {0}".format(conan_option_name)) def is_excluded_from_configure(self, conan_option_name: str) -> bool: # extra options are not Qt configure(.bat) options but those exist as # conan recipe options which are treated outside Qt's configure(.bat) return conan_option_name in self.extra_options.keys() def convert_conan_option_to_qt_option( self, qt_option: QtConfigureOption, conan_option_value: Any ) -> str: ret: str = "" if qt_option.incremental_option: # e.g. -make=libs -make=examples <-> -o make=libs;examples;foo;bar _opt = qt_option.prefix + qt_option.name + qt_option.value_delim ret = " ".join(_opt + item.strip() for item in conan_option_value.split(";") if item.strip()) else: ret = qt_option.prefix + qt_option.name if not qt_option.binary_option: ret += qt_option.value_delim + conan_option_value return ret def convert_conan_options_to_qt_options(self, conan_options: Options) -> List[str]: qt_options: List[str] = [] def _option_enabled(opt: str) -> bool: return bool(conan_options.get_safe(opt)) def _option_disabled(opt: str) -> bool: return not bool(conan_options.get_safe(opt)) def _filter_overlapping_options() -> None: if _option_enabled("shared") or _option_disabled("static"): delattr(conan_options, "static") # should result only into "-shared" if _option_enabled("static") or _option_disabled("shared"): delattr(conan_options, "shared") # should result only into "-static" _filter_overlapping_options() self.sanity_check_android_options(conan_options) for conan_opt_name, conan_opt_value in conan_options.items(): if self.is_excluded_from_configure(conan_opt_name): continue if not is_used_option(conan_opt_name, conan_opt_value): continue qt_opt = self.find_qt_option(conan_opt_name) if qt_opt.is_feature(): ret = conan_option_to_qt_feature(option=conan_opt_name, value=conan_opt_value) else: ret = self.convert_conan_option_to_qt_option(qt_opt, conan_opt_value) qt_options.append(ret) return qt_options def sanity_check_android_options(self, conan_options: Options) -> None: check_list = [ ("android_sdk", "android_sdk_version"), ("android_ndk", "android_ndk_version"), ] for opt, opt_pair in check_list: opt_val = conan_options.get_safe(opt) opt_pair_val = conan_options.get_safe(opt_pair) if is_used_option(opt, opt_val) and not is_used_option(opt_pair, opt_pair_val): raise QtConanError("'{0}' must be set when using '{1}'".format(opt_pair, opt)) def get_cmake_args_for_configure(self, conan_options: Options) -> List[Optional[str]]: ret: List[Optional[str]] = [] for option_name, option_value in conan_options.items(): if option_name == "cmake_args_qtbase" and is_used_option( option_name, option_value ): ret = [ret for ret in option_value.strip(r" '\"").split()] return ret def is_used_option(conan_option_name: str, conan_option_value: Any) -> bool: if conan_option_name.startswith("feature"): # A Qt feature is used even if "False" is the value as it maps to '-no-feature-FOO' return conan_option_value not in [None, "None"] else: # A Qt configure option is not used if the value is "False" or [None, "None"] # conan install ... -o release=False -> configure(.bat) # conan install ... -> configure(.bat) # conan install ... -o release=True -> configure(.bat) -release # conan install ... -o prefix=/foo/bar -> configure(.bat) -prefix /foo/bar # conan install ... -o libjpeg=no -> configure(.bat) --libjpeg=no # conan install ... -o libjpeg=qt -> configure(.bat) --libjpeg=qt return False if conan_option_value in ["None", None, "False", False, ""] else True def parse_qt_leaf_module_options(conan_file: ConanFile) -> Tuple[List[str], List[str]]: qt_features: List[str] = [] cmake_args: List[str] = [] for option_name, option_value in conan_file.options.items(): if not conan_file.is_qt_module_feature(option_name): # this option is not intended to be passed to 'qt-configure-module.bat' continue elif not is_used_option(conan_option_name=option_name, conan_option_value=option_value): # this option was not being used at all continue elif option_name == "cmake_args_leaf_module": # replace multiple white space with one, strip ' and " characters cmake_args = (" ".join(option_value.split())).strip("'\"").split(" ") else: qt_features.append(conan_option_to_qt_feature(option_name, option_value)) return qt_features, cmake_args def convert_qt_features_to_conan_options(features: List[str]) -> Dict[str, List[Any]]: """Convert the given Qt leaf module features to Conan recipe options. The feature can be explicitly enabled or disabled by the user (True, False). We must also allow not passing the option at all hence the 'None'.""" return {qt_feature_to_conan_option(f): [True, False, None] for f in features} def convert_qt_features_to_default_conan_options(features: List[str]) -> Dict[str, Any]: """Get the default value for Qt leaf module features""" return {qt_feature_to_conan_option(f): None for f in features} def qt_feature_to_conan_option(feature_name: str) -> str: return "feature_" + feature_name.replace("-", "_") def conan_option_to_qt_feature(option: str, value: Union[str, bool]) -> str: """Convert 'a binary feature' from Conan recipe to Qt configure feature.""" if not is_used_option(conan_option_name=option, conan_option_value=value): raise QtConanError(f"Can not convert unused option to Qt feature: {option}={value}") if value in [False, "False"]: # e.g. '-o feature_foo=False' -> -no-feature-foo return "-no-" + option.replace("_", "-") else: # e.g. '-o feature_foo=True' -> -feature-foo return "-" + option.replace("_", "-") def build_leaf_qt_module(conan_file: ConanFile): run_qt_configure_module_with_additional_packages_prefix( conan_file, build_func=run_qt_configure_module ) def run_qt_configure_module_with_additional_packages_prefix( conan_file: ConanFile, build_func: Callable ): prefixes = "".join( [conan_file.deps_cpp_info[d].rootpath + ";" for d in conan_file.deps_cpp_info.deps] ) conan_file.output.info("Using QT_ADDITIONAL_PACKAGES_PREFIX_PATH: {0}".format(prefixes)) with tools.environment_append({"QT_ADDITIONAL_PACKAGES_PREFIX_PATH": prefixes}): build_env_wrap(conan_file, build_func) def build_env_wrap(conan_file: ConanFile, build_func: Callable): if conan_file.settings.os == "Windows" and conan_file.settings.compiler == "msvc": vcvars = tools.vcvars_dict(conan_file) # If user has CMake installed prepend it into vcvars PATH. # This could be replaced by adding CMake as conan dependency # when the packages are readily available. exe = shutil.which('cmake') if exe: vcvars["PATH"].insert(0, os.path.dirname(exe)) with tools.environment_append(vcvars): build_func(conan_file) else: build_func(conan_file) def run_qt_configure_module(conan_file: ConanFile): # We use the Qt's 'bin/qt-configure-module' directly script = ( Path("qt-configure-module.bat") if tools.os_info.is_windows else Path("qt-configure-module") ) if os.environ.get("QT_CONFIGURE_MODULE"): qt_configure_module = Path(os.environ.get("QT_CONFIGURE_MODULE"), resolve=True) else: qt_configure_module = Path(conan_file.deps_cpp_info["qtbase"].rootpath) / "bin" / script qt_module_features, cmake_args = parse_qt_leaf_module_options(conan_file) cmd = " ".join( [ str(qt_configure_module), conan_file.build_folder, " ".join(qt_module_features) if qt_module_features else "", "--", "-DQT_BUILD_SINGLE_REPO_TARGET_SET={0}".format(conan_file.name), "-DCMAKE_INSTALL_PREFIX={0}".format(conan_file.package_folder), "{0}".format(" ".join(cmake_args)) if cmake_args else "", ] ) conan_file.output.info("Calling: {0}".format(cmd)) conan_file.run(cmd, run_environment=True) # Qt qt-configure-module would direct the install to qtbase's -prefix which we do not want, # we need to direct the install to this packages '/package' directory Path(conan_file.package_folder).mkdir(parents=True) cmd = " ".join(["cmake", "--build", "."]) conan_file.output.info("Calling: {0}".format(cmd)) conan_file.run(cmd, run_environment=True) def append_cmake_arg(cmake_args_str, cmake_arg_name: str, cmake_arg_value: str) -> str: """Append the given CMake argument to the cmake args string and return the formatted version.""" formatted_cmake_args: List[str] = [] cmake_arg_found = False for item in shlex.split(cmake_args_str.strip("\"' ")): if cmake_arg_name in item: item = item + ";" + cmake_arg_value cmake_arg_found = True formatted_cmake_args.append(item) if not cmake_arg_found: formatted_cmake_args.append("-D{0}={1}".format(cmake_arg_name, cmake_arg_value)) return '"' + " ".join(formatted_cmake_args) + '"' def package_info(conan_file: ConanFile): conan_file.cpp_info.libs = tools.collect_libs(conan_file) def _add_qt_package_import_paths(qt_import_name: str, folder_names: List[str]) -> None: package_folder = Path(conan_file.package_folder) for subfolder in folder_names: search_folder = package_folder / subfolder if search_folder.exists(): info = getattr(conan_file.env_info, qt_import_name) info.append(str(search_folder)) # For virtualenv generator: build and run environment for qmake, CMake conan_file.env_info.CMAKE_PREFIX_PATH.append(conan_file.package_folder) conan_file.env_info.PATH.append(os.path.join(conan_file.package_folder, "bin")) conan_file.env_info.QMAKEPATH.append(conan_file.package_folder) conan_file.env_info.QT_ADDITIONAL_PACKAGES_PREFIX_PATH.append(conan_file.package_folder) _add_qt_package_import_paths(qt_import_name="QML2_IMPORT_PATH", folder_names=["qml"]) _add_qt_package_import_paths(qt_import_name="QT_PLUGIN_PATH", folder_names=["plugins"]) for item in Path(Path(conan_file.package_folder) / "lib" / "cmake").resolve().glob("Qt6*"): if item.is_dir(): setattr(conan_file.env_info, str(item.name) + "_DIR", str(item)) lib_path = Path(conan_file.package_folder) / "lib" if lib_path.exists(): if tools.os_info.is_linux: conan_file.env_info.LD_LIBRARY_PATH.append(str(lib_path)) elif tools.os_info.is_macos: conan_file.env_info.DYLD_LIBRARY_PATH.append(str(lib_path)) conan_file.env_info.DYLD_FRAMEWORK_PATH.append(str(lib_path)) # e.g. 'QTWEBENGINEPROCESS_PATH' = '/foo/bar/QtWebEngineProcess.exe' if callable(getattr(conan_file, "package_env_info", None)): for env_name, value in conan_file.package_env_info().items(): info = getattr(conan_file.env_info, env_name) info.append(str(value)) @lru_cache(maxsize=8) def parse_qt_version_by_key(source_folder: Path, key: str) -> str: pattern = fr'{key} .*"(.*)"' return parse_version_by_key(source_folder / ".cmake.conf", pattern) def parse_version_by_key(filename: Path, pattern: str) -> str: with open(filename) as f: m = re.search(pattern, f.read()) return m.group(1).strip() if m else "" def parse_module_dependencies(source_folder: Path) -> List[str]: with open(source_folder / "dependencies.yaml") as f: dep_list = yaml.load(f, Loader=yaml.SafeLoader) return [d.split("/")[-1] for d in dep_list.get("dependencies", {}).keys()] def reject_empty_args(f): def func(*args, **kwargs): if any(not arg for arg in args): raise ValueError( "The function '{0}' does not accept empty arguments".format(f.__name__) ) for kw_arg, kw_value in kwargs.items(): if not kw_value: raise ValueError( "The function '{0}' does not accept empty keywor arguments: {1}".format( f.__name__, kw_arg ) ) return f(*args, **kwargs) return func def simple_version_check(version: str) -> str: error_msg = "The given value doesn't seem to be a valid version: {0}".format(version) try: for item in version.split("."): int(item) except ValueError: print(error_msg) raise except AttributeError as a: print(error_msg) raise ValueError from a return version def qt_sw_versions_config_folder() -> Path: # This is usually used during "conan export qtbase/.." phase return Path.home() def qt_sw_versions_config_name() -> str: return "conan_sw_package_versions.ini" @reject_empty_args def parse_qt_sw_pkg_dependency( config_folder: Path, package_name: str, target_os: str ) -> Optional[str]: # always use the fallback folder for folder in [config_folder, qt_sw_versions_config_folder()]: section_name = "{0}-{1}".format(package_name, target_os.lower()) try: sw_versions_config = folder / qt_sw_versions_config_name() print("Reading sw dependencies from: {0}".format(sw_versions_config)) settings = ConfigParser(interpolation=ExtendedInterpolation()) settings.read(str(sw_versions_config.resolve(strict=True))) return simple_version_check(settings[section_name]["version"]) except FileNotFoundError: print("Warning: file not found: {0}".format(str(sw_versions_config))) except KeyError: print("Warning: 'version' not specified for: {0}".format(section_name)) return None def filter_cmake_args_for_package_id(cmake_args: Optional[str]) -> Optional[str]: if not cmake_args: return None excludes = [ r"CMAKE_CXX_COMPILER_LAUNCHER=", r"CMAKE_C_COMPILER_LAUNCHER=", r"QT_BUILD_EXAMPLES=", r"WARNINGS_ARE_ERRORS=", r"FEATURE_headersclean=", r"PostgreSQL_ROOT=", r"OPENSSL_ROOT_DIR=", r"LLVM_INSTALL_DIR=", r"ODBC_ROOT=", r"QT_HOST_PATH=", ] included_items: List[str] = [] exclude_next_part = False for item in shlex.split(cmake_args.strip("\"' ")): if exclude_next_part: exclude_next_part = False continue if item.startswith(("-I", "-L")): exclude_next_part = True continue for pattern in excludes: if re.search(pattern, item): print("Filtered out cmake argument from package_id: {0}".format(item)) break else: included_items.append(item) ret = " ".join(included_items) return f'"{ret}"' if included_items else None def call_install(conanfile: ConanFile) -> None: """Call 'cmake -DQT_BUILD_DIR={} -P {}/qt-cmake-private-install.cmake for the Qt leaf module' If 'QT_HOST_PATH' can be read from env (e.g. Boot2Qt) then use that for the location of qtbase's 'bin/' folder. Otherwise attempt to locate qbase's 'bin/' folder from the conanfile's bin_paths provided by Conan. """ _hp = os.environ.get("QT_HOST_PATH") if _hp is not None: print("Using 'QT_HOST_PATH={0}' for installing: {1}".format(_hp, conanfile.name)) hp = Path(os.path.expandvars(str(_hp))).expanduser().resolve(strict=True) qtbase_bin_path = hp / "bin" elif conanfile.name == "qtbase": qtbase_bin_path = Path("{0}/bin".format(conanfile.build_folder)) else: num_bin_paths = len(conanfile.deps_cpp_info["qtbase"].bin_paths) if num_bin_paths != 1: raise QtConanError("expected 1 bin path for qtbase received %s." % num_bin_paths) qtbase_bin_path = Path(conanfile.deps_cpp_info["qtbase"].bin_paths[0]) qtbase_bin_path.resolve(strict=True) cmd = ["cmake", "-DQT_BUILD_DIR={0}".format(conanfile.build_folder), "-P", "{0}/qt-cmake-private-install.cmake".format(str(qtbase_bin_path))] conanfile.run(" ".join(cmd)) @reject_empty_args def parse_android_sdk_version(cmake_args_qtbase: str) -> Optional[str]: # If -DQT_ANDROID_API_VERSION is defined then prefer that value for package_id m = re.search(r"QT_ANDROID_API_VERSION=(\S*)", cmake_args_qtbase) return m.group(1).strip() if m else None @reject_empty_args def parse_android_ndk_version(search_path: Path) -> str: f = locate_file(search_path, "source.properties") pattern = r"Pkg.Revision.*=(.*)" return parse_version_by_key(f, pattern) @reject_empty_args def locate_file(search_path: Path, fn: str) -> Path: matches = [m for m in search_path.rglob(fn)] if len(matches) != 1: raise QtConanError("Expected to find only one '{0}' from: {1}".format(fn, search_path)) return matches.pop() class QtLeafModule(metaclass=ABCMeta): """ A base class for leaf Qt module Conan recipes. As most of the leaf modules are identical what comes to build steps those are implemented here and the leaf classes can be extended with this. """ short_paths = True revision_mode = "scm" _shared = None def init(self) -> None: # The following python_requires only works when inheriting the QtLeafModule class how # Conan intends the usage. When inheriting class is instantiated by Conan and init() is # being called the "python_requires" below is resolved by then in the inheriting class. self._shared = self.python_requires["qt-conan-common"].module base_options: Dict[str, Any] = {"cmake_args_leaf_module": "ANY"} # ANY allows None leaf_options = self.get_qt_leaf_module_options() self.options = {**base_options, **leaf_options} bsae_default_options: Dict[str, Any] = {"cmake_args_leaf_module": None} leaf_default_options = self.get_qt_leaf_module_default_options() self.default_options = {**bsae_default_options, **leaf_default_options} def set_version(self): src_dir = Path(self.recipe_folder).resolve() ver = parse_qt_version_by_key(src_dir, "QT_REPO_MODULE_VERSION") prerelease = parse_qt_version_by_key(src_dir, "QT_REPO_MODULE_PRERELEASE_VERSION_SEGMENT") self.version = ver + "-" + prerelease if prerelease else ver def requirements(self): src_dir = Path(self.recipe_folder).resolve() ver = parse_qt_version_by_key(src_dir, "QT_REPO_MODULE_VERSION") # check if the inheriting leaf module recipe wants to override requirements # in 'dependencies.yaml' requirements = self.override_qt_requirements() if not requirements: # if not then parse the requirements from the 'dependencies.yaml' requirements = parse_module_dependencies(src_dir) # all the Qt Conan packages follow the same versioning schema for dep in requirements: # will match latest prerelase of final major.minor.patch self.requires( "{0}/{1}@{2}/{3}".format( dep, ver, self.user, self.channel ) ) @abstractmethod def get_qt_leaf_module_options(self) -> Dict[str, Any]: """Implement this in inheriting class if the Qt module declares Conan options""" return {} @abstractmethod def get_qt_leaf_module_default_options(self) -> Dict[str, Any]: """Implement this in inheriting class if the Qt module declares Conan options""" return {} @abstractmethod def override_qt_requirements(self) -> List[str]: """ If the Qt leaf module requirements (dependencies) does not match the Qt's .git repository naming then implement this method in the leaf module recipe class. E.g. . Example: qtscxml.git -> qtscxml/@qt/everywhere, qtscxmlqml/, qtstatemachine/, qtstatemachineqml/ qtfoo.git: dependencies.yaml: qtscxml # Note! Here the dep is using repo name granularity, # not Conan package name! If the qtfoo requires (depends) qtstatemachine Conan package in reality (produced from qtscxml.git) it would not get that dependency but it would get the qtscxml/@qt/everywhere instead, which is wrong. So the qtfoo recipe needs to implement this method and return ['qtbase', 'qtscxml']. """ return [] @abstractmethod def is_qt_module_feature(self, option_name: str) -> bool: """ If the Qt leaf module recipe defines an option which should not be passed to 'qt-configure-module(.bat)' then this method should be implemented and return False for such option.""" return True def build(self): build_leaf_qt_module(self) def package(self): call_install(self) def package_info(self): package_info(self) @abstractmethod def package_env_info(self) -> Dict[str, Any]: """ If the Qt module should append module specific attributes into Conan's 'env_info' the inheriting class can implement this and return a dictionary containing the key-pair for environment variables.""" return {} def package_id(self): if hasattr(self.info.options, "cmake_args_leaf_module"): self.info.options.cmake_args_leaf_module = filter_cmake_args_for_package_id( self.info.options.cmake_args_leaf_module ) # https://docs.conan.io/en/latest/creating_packages/define_abi_compatibility.html # The package_revision_mode() is too strict for Qt CI. This mode includes artifacts # checksum in package_id which is problematic in Qt CI re-runs (re-run flaky # build) which contain different build timestamps (cmake) which end up in library # files -> different package_id. self.info.requires.recipe_revision_mode() # Enable 'qt-conan-common' updates on client side with $conan install .. --update self.info.python_requires.recipe_revision_mode() def deploy(self): self.copy("*") # copy from current package if not os.environ.get("QT_CONAN_INSTALL_SKIP_DEPS"): self.copy_deps("*") # copy from dependencies class QtConanCommon(ConanFile): name = "qt-conan-common" license = "LGPL-3.0+, GPL-2.0+, Commercial Qt License Agreement" url = "https://www.qt.io" description = "Shared functionality for Qt Conan package recipes" revision_mode = "scm" short_paths = True exports = ".cmake.conf" def set_version(self): path = Path(__file__).parent.resolve() full_version = parse_qt_version_by_key(path, "QT_REPO_MODULE_VERSION") self.version = ".".join(full_version.split(".")[:2])