diff options
author | Iikka Eklund <iikka.eklund@qt.io> | 2022-03-23 12:43:28 +0200 |
---|---|---|
committer | Iikka Eklund <iikka.eklund@qt.io> | 2022-04-12 10:18:00 +0000 |
commit | 0e009457dd7f6dc7eff0f132a8aa1b8b4f07f2d3 (patch) | |
tree | fb63d5a1976d969849b9e19b23c22cfa1cda1399 | |
parent | dac9130748e6d8591e220871a97b3f182eb1e9e8 (diff) |
Move QtConfigureOption and QtOptionParser to qt-conan-common from qtbase
It is easier to maintain these classes in qt-conan-common. These classes
can also re-use other exising functionality from this module.
Also unit tests can be added for the functionality of these classses
in the existing framework.
Task-number: QTIFW-2585
Change-Id: I61227f0dc80e3f0948f46a864ba12d9fbecf0e2e
Reviewed-by: Toni Saario <toni.saario@qt.io>
-rw-r--r-- | conanfile.py | 297 |
1 files changed, 295 insertions, 2 deletions
diff --git a/conanfile.py b/conanfile.py index 0083ddd..2324d73 100644 --- a/conanfile.py +++ b/conanfile.py @@ -1,6 +1,6 @@ ############################################################################# ## -## Copyright (C) 2021 The Qt Company Ltd. +## 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. @@ -30,6 +30,7 @@ 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 @@ -37,13 +38,305 @@ from pathlib import Path from typing import Any, Callable, Dict, List, Optional, Tuple import yaml -from conans import ConanFile, tools +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 "yes"|"no" + 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 + + 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"} + self.extra_options_default_values = {"cmake_args_qtbase": 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 is_used_option(self, conan_options: Options, option_name: str) -> bool: + # conan install ... -o release=False -> configure(.bat) + # conan install ... -> configure(.bat) + # conan install ... -o release=True -> configure(.bat) -release + return bool(conan_options.get_safe(option_name)) + + def convert_conan_option_to_qt_option( + self, conan_options: Options, name: str, value: Any + ) -> str: + ret: str = "" + + def _find_qt_option(conan_option_name: str) -> QtConfigureOption: + for qt_opt in self.options: + if conan_option_name == qt_opt.conan_option_name: + return qt_opt + else: + raise QtConanError( + "Could not find a matching Qt configure option for: {0}".format( + conan_option_name + ) + ) + + def _is_excluded_from_configure() -> bool: + # extra options are not Qt configure(.bat) options but those exist as + # conan recipe options which are treated outside Qt's configure(.bat) + if name in self.extra_options.keys(): + return True + return False + + if self.is_used_option(conan_options, name) and not _is_excluded_from_configure(): + qt_option = _find_qt_option(name) + 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 value.split(";") if item.strip()) + else: + ret = qt_option.prefix + qt_option.name + if not qt_option.binary_option: + ret += qt_option.value_delim + 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() + + for option_name, option_value in conan_options.items(): + qt_option = self.convert_conan_option_to_qt_option( + conan_options=conan_options, name=option_name, value=option_value + ) + if not qt_option: + continue + qt_options.append(qt_option) + return qt_options + + 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 self.is_used_option( + conan_options, option_name + ): + ret = [ret for ret in option_value.strip(r" '\"").split()] + return ret + + def build_leaf_qt_module(conan_file: ConanFile): run_qt_configure_module_with_additional_packages_prefix( conan_file, build_func=run_qt_configure_module |