summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorIikka Eklund <iikka.eklund@qt.io>2022-03-23 12:43:28 +0200
committerIikka Eklund <iikka.eklund@qt.io>2022-04-12 10:18:00 +0000
commit0e009457dd7f6dc7eff0f132a8aa1b8b4f07f2d3 (patch)
treefb63d5a1976d969849b9e19b23c22cfa1cda1399
parentdac9130748e6d8591e220871a97b3f182eb1e9e8 (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.py297
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