diff --git a/conan/cps/cps.py b/conan/cps/cps.py index 67007d8570a..621a387ce27 100644 --- a/conan/cps/cps.py +++ b/conan/cps/cps.py @@ -85,7 +85,7 @@ def from_cpp_info(cpp_info, pkg_type, libname=None): cps_comp.type = CPSComponentType.INTERFACE return cps_comp - cpp_info.deduce_cps(pkg_type) + cpp_info.deduce_locations(pkg_type) cps_comp.type = CPSComponentType.from_conan(cpp_info.type) cps_comp.location = cpp_info.location cps_comp.link_location = cpp_info.link_location diff --git a/conan/tools/cmake/__init__.py b/conan/tools/cmake/__init__.py index d4123be7493..73c8a835b08 100644 --- a/conan/tools/cmake/__init__.py +++ b/conan/tools/cmake/__init__.py @@ -1,4 +1,14 @@ from conan.tools.cmake.toolchain.toolchain import CMakeToolchain from conan.tools.cmake.cmake import CMake -from conan.tools.cmake.cmakedeps.cmakedeps import CMakeDeps from conan.tools.cmake.layout import cmake_layout + + +def CMakeDeps(conanfile): # noqa + if conanfile.conf.get("tools.cmake.cmakedeps:new", choices=["will_break_next"]): + from conan.tools.cmake.cmakedeps2.cmakedeps import CMakeDeps2 + conanfile.output.warning("Using the new CMakeDeps generator, behind the " + "'tools.cmake.cmakedeps:new' gate conf. This conf will change" + "next release, breaking, so use it only for testing and dev") + return CMakeDeps2(conanfile) + from conan.tools.cmake.cmakedeps.cmakedeps import CMakeDeps as _CMakeDeps + return _CMakeDeps(conanfile) diff --git a/conan/tools/cmake/cmakedeps2/__init__.py b/conan/tools/cmake/cmakedeps2/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/conan/tools/cmake/cmakedeps2/cmakedeps.py b/conan/tools/cmake/cmakedeps2/cmakedeps.py new file mode 100644 index 00000000000..b92e8276ecb --- /dev/null +++ b/conan/tools/cmake/cmakedeps2/cmakedeps.py @@ -0,0 +1,211 @@ +import os +import re +import textwrap + +from jinja2 import Template + +from conan.internal import check_duplicated_generator +from conan.tools.cmake.cmakedeps2.config import ConfigTemplate2 +from conan.tools.cmake.cmakedeps2.config_version import ConfigVersionTemplate2 +from conan.tools.cmake.cmakedeps2.target_configuration import TargetConfigurationTemplate2 +from conan.tools.cmake.cmakedeps2.targets import TargetsTemplate2 +from conan.tools.files import save +from conan.errors import ConanException +from conans.model.dependencies import get_transitive_requires +from conans.util.files import load + +FIND_MODE_MODULE = "module" +FIND_MODE_CONFIG = "config" +FIND_MODE_NONE = "none" +FIND_MODE_BOTH = "both" + + +class CMakeDeps2: + + def __init__(self, conanfile): + self._conanfile = conanfile + self.configuration = str(self._conanfile.settings.build_type) + + # These are just for legacy compatibility, but not use at al + self.build_context_activated = [] + self.build_context_build_modules = [] + self.build_context_suffix = {} + # Enable/Disable checking if a component target exists or not + self.check_components_exist = False + + self._properties = {} + + def generate(self): + check_duplicated_generator(self, self._conanfile) + # Current directory is the generators_folder + generator_files = self._content() + for generator_file, content in generator_files.items(): + save(self._conanfile, generator_file, content) + _PathGenerator(self, self._conanfile).generate() + + def _content(self): + host_req = self._conanfile.dependencies.host + build_req = self._conanfile.dependencies.direct_build + test_req = self._conanfile.dependencies.test + + # Iterate all the transitive requires + ret = {} + for require, dep in list(host_req.items()) + list(build_req.items()) + list(test_req.items()): + cmake_find_mode = self.get_property("cmake_find_mode", dep) + cmake_find_mode = cmake_find_mode or FIND_MODE_CONFIG + cmake_find_mode = cmake_find_mode.lower() + if cmake_find_mode == FIND_MODE_NONE: + continue + + config = ConfigTemplate2(self, dep) + ret[config.filename] = config.content() + config_version = ConfigVersionTemplate2(self, dep) + ret[config_version.filename] = config_version.content() + + targets = TargetsTemplate2(self, dep) + ret[targets.filename] = targets.content() + target_configuration = TargetConfigurationTemplate2(self, dep, require) + ret[target_configuration.filename] = target_configuration.content() + return ret + + def set_property(self, dep, prop, value, build_context=False): + """ + Using this method you can overwrite the :ref:`property` values set by + the Conan recipes from the consumer. + + :param dep: Name of the dependency to set the :ref:`property`. For + components use the syntax: ``dep_name::component_name``. + :param prop: Name of the :ref:`property`. + :param value: Value of the property. Use ``None`` to invalidate any value set by the + upstream recipe. + :param build_context: Set to ``True`` if you want to set the property for a dependency that + belongs to the build context (``False`` by default). + """ + build_suffix = "&build" if build_context else "" + self._properties.setdefault(f"{dep}{build_suffix}", {}).update({prop: value}) + + def get_property(self, prop, dep, comp_name=None, check_type=None): + dep_name = dep.ref.name + build_suffix = "&build" if dep.context == "build" else "" + dep_comp = f"{str(dep_name)}::{comp_name}" if comp_name else f"{str(dep_name)}" + try: + value = self._properties[f"{dep_comp}{build_suffix}"][prop] + if check_type is not None and not isinstance(value, check_type): + raise ConanException(f'The expected type for {prop} is "{check_type.__name__}", ' + f'but "{type(value).__name__}" was found') + return value + except KeyError: + # Here we are not using the cpp_info = deduce_cpp_info(dep) because it is not + # necessary for the properties + return dep.cpp_info.get_property(prop, check_type=check_type) if not comp_name \ + else dep.cpp_info.components[comp_name].get_property(prop, check_type=check_type) + + def get_cmake_filename(self, dep, module_mode=None): + """Get the name of the file for the find_package(XXX)""" + # This is used by CMakeDeps to determine: + # - The filename to generate (XXX-config.cmake or FindXXX.cmake) + # - The name of the defined XXX_DIR variables + # - The name of transitive dependencies for calls to find_dependency + if module_mode and self._get_find_mode(dep) in [FIND_MODE_MODULE, FIND_MODE_BOTH]: + ret = self.get_property("cmake_module_file_name", dep) + if ret: + return ret + ret = self.get_property("cmake_file_name", dep) + return ret or dep.ref.name + + def _get_find_mode(self, dep): + """ + :param dep: requirement + :return: "none" or "config" or "module" or "both" or "config" when not set + """ + tmp = self.get_property("cmake_find_mode", dep) + if tmp is None: + return "config" + return tmp.lower() + + def get_transitive_requires(self, conanfile): + # Prepared to filter transitive tool-requires with visible=True + return get_transitive_requires(self._conanfile, conanfile) + + +class _PathGenerator: + _conan_cmakedeps_paths = "conan_cmakedeps_paths.cmake" + + def __init__(self, cmakedeps, conanfile): + self._conanfile = conanfile + self._cmakedeps = cmakedeps + + def generate(self): + template = textwrap.dedent("""\ + {% for pkg_name, folder in pkg_paths.items() %} + set({{pkg_name}}_DIR "{{folder}}") + {% endfor %} + {% if host_runtime_dirs %} + set(CONAN_RUNTIME_LIB_DIRS {{ host_runtime_dirs }} ) + {% endif %} + """) + + host_req = self._conanfile.dependencies.host + build_req = self._conanfile.dependencies.direct_build + test_req = self._conanfile.dependencies.test + + # gen_folder = self._conanfile.generators_folder.replace("\\", "/") + # if not, test_cmake_add_subdirectory test fails + # content.append('set(CMAKE_FIND_PACKAGE_PREFER_CONFIG ON)') + pkg_paths = {} + for req, dep in list(host_req.items()) + list(build_req.items()) + list(test_req.items()): + cmake_find_mode = self._cmakedeps.get_property("cmake_find_mode", dep) + cmake_find_mode = cmake_find_mode or FIND_MODE_CONFIG + cmake_find_mode = cmake_find_mode.lower() + + pkg_name = self._cmakedeps.get_cmake_filename(dep) + # https://cmake.org/cmake/help/v3.22/guide/using-dependencies/index.html + if cmake_find_mode == FIND_MODE_NONE: + try: + # This is irrespective of the components, it should be in the root cpp_info + # To define the location of the pkg-config.cmake file + build_dir = dep.cpp_info.builddirs[0] + except IndexError: + build_dir = dep.package_folder + pkg_folder = build_dir.replace("\\", "/") if build_dir else None + if pkg_folder: + config_file = ConfigTemplate2(self._cmakedeps, dep).filename + if os.path.isfile(os.path.join(pkg_folder, config_file)): + pkg_paths[pkg_name] = pkg_folder + continue + + # If CMakeDeps generated, the folder is this one + # content.append(f'set({pkg_name}_ROOT "{gen_folder}")') + pkg_paths[pkg_name] = "${CMAKE_CURRENT_LIST_DIR}" + + context = {"host_runtime_dirs": self._get_host_runtime_dirs(), + "pkg_paths": pkg_paths} + content = Template(template, trim_blocks=True, lstrip_blocks=True).render(context) + save(self._conanfile, self._conan_cmakedeps_paths, content) + + def _get_host_runtime_dirs(self): + host_runtime_dirs = {} + + # Get the previous configuration + if os.path.exists(self._conan_cmakedeps_paths): + existing_toolchain = load(self._conan_cmakedeps_paths) + pattern_lib_dirs = r"set\(CONAN_RUNTIME_LIB_DIRS ([^)]*)\)" + variable_match = re.search(pattern_lib_dirs, existing_toolchain) + if variable_match: + capture = variable_match.group(1) + matches = re.findall(r'"\$<\$:([^>]*)>"', capture) + for config, paths in matches: + host_runtime_dirs.setdefault(config, []).append(paths) + + is_win = self._conanfile.settings.get_safe("os") == "Windows" + for req in self._conanfile.dependencies.host.values(): + config = req.settings.get_safe("build_type", self._cmakedeps.configuration) + aggregated_cppinfo = req.cpp_info.aggregated_components() + runtime_dirs = aggregated_cppinfo.bindirs if is_win else aggregated_cppinfo.libdirs + for d in runtime_dirs: + d = d.replace("\\", "/") + existing = host_runtime_dirs.setdefault(config, []) + if d not in existing: + existing.append(d) + + return ' '.join(f'"$<$:{i}>"' for c, v in host_runtime_dirs.items() for i in v) diff --git a/conan/tools/cmake/cmakedeps2/config.py b/conan/tools/cmake/cmakedeps2/config.py new file mode 100644 index 00000000000..459d3b42171 --- /dev/null +++ b/conan/tools/cmake/cmakedeps2/config.py @@ -0,0 +1,61 @@ +import textwrap + +import jinja2 +from jinja2 import Template + + +class ConfigTemplate2: + """ + FooConfig.cmake + foo-config.cmake + """ + def __init__(self, cmakedeps, conanfile): + self._cmakedeps = cmakedeps + self._conanfile = conanfile + + def content(self): + t = Template(self._template, trim_blocks=True, lstrip_blocks=True, + undefined=jinja2.StrictUndefined) + return t.render(self._context) + + @property + def filename(self): + f = self._cmakedeps.get_cmake_filename(self._conanfile) + return f"{f}-config.cmake" if f == f.lower() else f"{f}Config.cmake" + + @property + def _context(self): + f = self._cmakedeps.get_cmake_filename(self._conanfile) + targets_include = f"{f}Targets.cmake" + pkg_name = self._conanfile.ref.name + build_modules_paths = self._cmakedeps.get_property("cmake_build_modules", self._conanfile, + check_type=list) or [] + # FIXME: Proper escaping of paths for CMake and relativization + # FIXME: build_module_paths coming from last config only + build_modules_paths = [f.replace("\\", "/") for f in build_modules_paths] + return {"pkg_name": pkg_name, + "targets_include_file": targets_include, + "build_modules_paths": build_modules_paths} + + @property + def _template(self): + return textwrap.dedent("""\ + # Requires CMake > 3.15 + if(${CMAKE_VERSION} VERSION_LESS "3.15") + message(FATAL_ERROR "The 'CMakeDeps' generator only works with CMake >= 3.15") + endif() + + include(${CMAKE_CURRENT_LIST_DIR}/{{ targets_include_file }}) + + get_property(isMultiConfig GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) + if(NOT isMultiConfig AND NOT CMAKE_BUILD_TYPE) + message(FATAL_ERROR "Please, set the CMAKE_BUILD_TYPE variable when calling to CMake " + "adding the '-DCMAKE_BUILD_TYPE=' argument.") + endif() + + # build_modules_paths comes from last configuration only + {% for build_module in build_modules_paths %} + message(STATUS "Conan: Including build module from '{{build_module}}'") + include("{{ build_module }}") + {% endfor %} + """) diff --git a/conan/tools/cmake/cmakedeps2/config_version.py b/conan/tools/cmake/cmakedeps2/config_version.py new file mode 100644 index 00000000000..4a3212ba21c --- /dev/null +++ b/conan/tools/cmake/cmakedeps2/config_version.py @@ -0,0 +1,102 @@ +import textwrap + +import jinja2 +from jinja2 import Template + +from conan.errors import ConanException + + +class ConfigVersionTemplate2: + """ + foo-config-version.cmake + """ + def __init__(self, cmakedeps, conanfile): + self._cmakedeps = cmakedeps + self._conanfile = conanfile + + def content(self): + t = Template(self._template, trim_blocks=True, lstrip_blocks=True, + undefined=jinja2.StrictUndefined) + return t.render(self._context) + + @property + def filename(self): + f = self._cmakedeps.get_cmake_filename(self._conanfile) + return f"{f}-config-version.cmake" if f == f.lower() else f"{f}ConfigVersion.cmake" + + @property + def _context(self): + policy = self._cmakedeps.get_property("cmake_config_version_compat", self._conanfile) + if policy is None: + policy = "SameMajorVersion" + if policy not in ("AnyNewerVersion", "SameMajorVersion", "SameMinorVersion", "ExactVersion"): + raise ConanException(f"Unknown cmake_config_version_compat={policy} in {self._conanfile}") + version = self._cmakedeps.get_property("system_package_version", self._conanfile) + version = version or self._conanfile.ref.version + return {"version": version, + "policy": policy} + + @property + def _template(self): + # https://gitlab.kitware.com/cmake/cmake/blob/master/Modules/BasicConfigVersion-SameMajorVersion.cmake.in + # This will be at XXX-config-version.cmake + # AnyNewerVersion|SameMajorVersion|SameMinorVersion|ExactVersion + ret = textwrap.dedent("""\ + set(PACKAGE_VERSION "{{ version }}") + + if(PACKAGE_VERSION VERSION_LESS PACKAGE_FIND_VERSION) + set(PACKAGE_VERSION_COMPATIBLE FALSE) + else() + {% if policy == "AnyNewerVersion" %} + set(PACKAGE_VERSION_COMPATIBLE TRUE) + {% elif policy == "SameMajorVersion" %} + if("{{ version }}" MATCHES "^([0-9]+)\\\\.") + set(CVF_VERSION_MAJOR {{ '${CMAKE_MATCH_1}' }}) + else() + set(CVF_VERSION_MAJOR "{{ version }}") + endif() + + if(PACKAGE_FIND_VERSION_MAJOR STREQUAL CVF_VERSION_MAJOR) + set(PACKAGE_VERSION_COMPATIBLE TRUE) + else() + set(PACKAGE_VERSION_COMPATIBLE FALSE) + endif() + {% elif policy == "SameMinorVersion" %} + if("{{ version }}" MATCHES "^([0-9]+)\\.([0-9]+)") + set(CVF_VERSION_MAJOR "${CMAKE_MATCH_1}") + set(CVF_VERSION_MINOR "${CMAKE_MATCH_2}") + else() + set(CVF_VERSION_MAJOR "{{ version }}") + set(CVF_VERSION_MINOR "") + endif() + if((PACKAGE_FIND_VERSION_MAJOR STREQUAL CVF_VERSION_MAJOR) AND + (PACKAGE_FIND_VERSION_MINOR STREQUAL CVF_VERSION_MINOR)) + set(PACKAGE_VERSION_COMPATIBLE TRUE) + else() + set(PACKAGE_VERSION_COMPATIBLE FALSE) + endif() + {% elif policy == "ExactVersion" %} + if("{{ version }}" MATCHES "^([0-9]+)\\.([0-9]+)\\.([0-9]+)") + set(CVF_VERSION_MAJOR "${CMAKE_MATCH_1}") + set(CVF_VERSION_MINOR "${CMAKE_MATCH_2}") + set(CVF_VERSION_MINOR "${CMAKE_MATCH_3}") + else() + set(CVF_VERSION_MAJOR "{{ version }}") + set(CVF_VERSION_MINOR "") + set(CVF_VERSION_PATCH "") + endif() + if((PACKAGE_FIND_VERSION_MAJOR STREQUAL CVF_VERSION_MAJOR) AND + (PACKAGE_FIND_VERSION_MINOR STREQUAL CVF_VERSION_MINOR) AND + (PACKAGE_FIND_VERSION_PATCH STREQUAL CVF_VERSION_PATCH)) + set(PACKAGE_VERSION_COMPATIBLE TRUE) + else() + set(PACKAGE_VERSION_COMPATIBLE FALSE) + endif() + {% endif %} + + if(PACKAGE_FIND_VERSION STREQUAL PACKAGE_VERSION) + set(PACKAGE_VERSION_EXACT TRUE) + endif() + endif() + """) + return ret diff --git a/conan/tools/cmake/cmakedeps2/target_configuration.py b/conan/tools/cmake/cmakedeps2/target_configuration.py new file mode 100644 index 00000000000..5d8c2150413 --- /dev/null +++ b/conan/tools/cmake/cmakedeps2/target_configuration.py @@ -0,0 +1,384 @@ +import os +import textwrap + +import jinja2 +from jinja2 import Template + +from conan.errors import ConanException +from conans.client.graph.graph import CONTEXT_BUILD, CONTEXT_HOST +from conans.model.pkg_type import PackageType + + +class TargetConfigurationTemplate2: + """ + FooTarget-release.cmake + """ + def __init__(self, cmakedeps, conanfile, require): + self._cmakedeps = cmakedeps + self._conanfile = conanfile # The dependency conanfile, not the consumer one + self._require = require + + def content(self): + t = Template(self._template, trim_blocks=True, lstrip_blocks=True, + undefined=jinja2.StrictUndefined) + return t.render(self._context) + + @property + def filename(self): + f = self._cmakedeps.get_cmake_filename(self._conanfile) + # Fallback to consumer configuration if it doesn't have build_type + config = self._conanfile.settings.get_safe("build_type", self._cmakedeps.configuration) + config = (config or "none").lower() + build = "Build" if self._conanfile.context == CONTEXT_BUILD else "" + return f"{f}-Targets{build}-{config}.cmake" + + def _requires(self, info, components): + result = [] + requires = info.parsed_requires() + pkg_name = self._conanfile.ref.name + transitive_reqs = self._cmakedeps.get_transitive_requires(self._conanfile) + if not requires and not components: # global cpp_info without components definition + # require the pkgname::pkgname base (user defined) or INTERFACE base target + return [f"{d.ref.name}::{d.ref.name}" for d in transitive_reqs.values()] + + for required_pkg, required_comp in requires: + if required_pkg is None: # Points to a component of same package + dep_comp = components.get(required_comp) + assert dep_comp, f"Component {required_comp} not found in {self._conanfile}" + dep_target = self._cmakedeps.get_property("cmake_target_name", self._conanfile, + required_comp) + dep_target = dep_target or f"{pkg_name}::{required_comp}" + result.append(dep_target) + else: # Different package + try: + dep = transitive_reqs[required_pkg] + except KeyError: # The transitive dep might have been skipped + pass + else: + # To check if the component exist, it is ok to use the standard cpp_info + # No need to use the cpp_info = deduce_cpp_info(dep) + dep_comp = dep.cpp_info.components.get(required_comp) + if dep_comp is None: + # It must be the interface pkgname::pkgname target + assert required_pkg == required_comp + comp = None + else: + comp = required_comp + dep_target = self._cmakedeps.get_property("cmake_target_name", dep, comp) + dep_target = dep_target or f"{required_pkg}::{required_comp}" + result.append(dep_target) + return result + + @property + def _context(self): + cpp_info = self._conanfile.cpp_info.deduce_full_cpp_info(self._conanfile) + pkg_name = self._conanfile.ref.name + # fallback to consumer configuration if it doesn't have build_type + config = self._conanfile.settings.get_safe("build_type", self._cmakedeps.configuration) + config = config.upper() if config else None + pkg_folder = self._conanfile.package_folder.replace("\\", "/") + config_folder = f"_{config}" if config else "" + build = "_BUILD" if self._conanfile.context == CONTEXT_BUILD else "" + pkg_folder_var = f"{pkg_name}_PACKAGE_FOLDER{config_folder}{build}" + + libs = {} + # The BUILD context does not generate libraries targets atm + if self._conanfile.context == CONTEXT_HOST: + libs = self._get_libs(cpp_info, pkg_name, pkg_folder, pkg_folder_var) + self._add_root_lib_target(libs, pkg_name, cpp_info) + exes = self._get_exes(cpp_info, pkg_name, pkg_folder, pkg_folder_var) + + prefixes = self._cmakedeps.get_property("cmake_additional_variables_prefixes", + self._conanfile, check_type=list) or [] + f = self._cmakedeps.get_cmake_filename(self._conanfile) + prefixes = [f] + prefixes + include_dirs = definitions = libraries = None + if not self._require.build: # To add global variables for try_compile and legacy + aggregated_cppinfo = cpp_info.aggregated_components() + # FIXME: Proper escaping of paths for CMake and relativization + include_dirs = ";".join(i.replace("\\", "/") for i in aggregated_cppinfo.includedirs) + definitions = "" + root_target_name = self._cmakedeps.get_property("cmake_target_name", self._conanfile) + libraries = root_target_name or f"{pkg_name}::{pkg_name}" + + # TODO: Missing find_modes + dependencies = self._get_dependencies() + return {"dependencies": dependencies, + "pkg_folder": pkg_folder, + "pkg_folder_var": pkg_folder_var, + "config": config, + "exes": exes, + "libs": libs, + "context": self._conanfile.context, + # Extra global variables + "additional_variables_prefixes": prefixes, + "version": self._conanfile.ref.version, + "include_dirs": include_dirs, + "definitions": definitions, + "libraries": libraries + } + + def _get_libs(self, cpp_info, pkg_name, pkg_folder, pkg_folder_var) -> dict: + libs = {} + if cpp_info.has_components: + for name, component in cpp_info.components.items(): + target_name = self._cmakedeps.get_property("cmake_target_name", self._conanfile, + name) + target_name = target_name or f"{pkg_name}::{name}" + target = self._get_cmake_lib(component, cpp_info.components, pkg_folder, + pkg_folder_var) + if target is not None: + libs[target_name] = target + else: + target_name = self._cmakedeps.get_property("cmake_target_name", self._conanfile) + target_name = target_name or f"{pkg_name}::{pkg_name}" + target = self._get_cmake_lib(cpp_info, None, pkg_folder, pkg_folder_var) + if target is not None: + libs[target_name] = target + return libs + + def _get_cmake_lib(self, info, components, pkg_folder, pkg_folder_var): + if info.exe or not (info.includedirs or info.libs): + return + + includedirs = ";".join(self._path(i, pkg_folder, pkg_folder_var) + for i in info.includedirs) if info.includedirs else "" + requires = " ".join(self._requires(info, components)) + defines = " ".join(info.defines) + # TODO: Missing escaping? + # TODO: Missing link language + # FIXME: Filter by lib traits!!!!! + if not self._require.headers: # If not depending on headers, paths and + includedirs = defines = None + system_libs = " ".join(info.system_libs) + target = {"type": "INTERFACE", + "includedirs": includedirs, + "defines": defines, + "requires": requires, + "cxxflags": " ".join(info.cxxflags), + "cflags": " ".join(info.cflags), + "sharedlinkflags": " ".join(info.sharedlinkflags), + "exelinkflags": " ".join(info.exelinkflags), + "system_libs": system_libs} + + if info.frameworks: + self._conanfile.output.warning("frameworks not supported yet in new CMakeDeps generator") + + if info.libs: + if len(info.libs) != 1: + raise ConanException(f"New CMakeDeps only allows 1 lib per component:\n" + f"{self._conanfile}: {info.libs}") + assert info.location, "info.location missing for .libs, it should have been deduced" + location = self._path(info.location, pkg_folder, pkg_folder_var) + link_location = self._path(info.link_location, pkg_folder, pkg_folder_var) \ + if info.link_location else None + lib_type = "SHARED" if info.type is PackageType.SHARED else \ + "STATIC" if info.type is PackageType.STATIC else None + assert lib_type, f"Unknown package type {info.type}" + target["type"] = lib_type + target["location"] = location + target["link_location"] = link_location + link_languages = info.languages or self._conanfile.languages or [] + link_languages = ["CXX" if c == "C++" else c for c in link_languages] + target["link_languages"] = link_languages + + return target + + def _add_root_lib_target(self, libs, pkg_name, cpp_info): + """ + Addd a new pkgname::pkgname INTERFACE target that depends on default_components or + on all other library targets (not exes) + It will not be added if there exists already a pkgname::pkgname target. + """ + root_target_name = self._cmakedeps.get_property("cmake_target_name", self._conanfile) + root_target_name = root_target_name or f"{pkg_name}::{pkg_name}" + if libs and root_target_name not in libs: + # Add a generic interface target for the package depending on the others + if cpp_info.default_components is not None: + all_requires = [] + for defaultc in cpp_info.default_components: + target_name = self._cmakedeps.get_property("cmake_target_name", self._conanfile, + defaultc) + comp_name = target_name or f"{pkg_name}::{defaultc}" + all_requires.append(comp_name) + all_requires = " ".join(all_requires) + else: + all_requires = " ".join(libs.keys()) + libs[root_target_name] = {"type": "INTERFACE", + "requires": all_requires} + + def _get_exes(self, cpp_info, pkg_name, pkg_folder, pkg_folder_var): + exes = {} + + if cpp_info.has_components: + assert not cpp_info.exe, "Package has components and exe" + assert not cpp_info.libs, "Package has components and libs" + for name, comp in cpp_info.components.items(): + if comp.exe or comp.type is PackageType.APP: + target_name = self._cmakedeps.get_property("cmake_target_name", self._conanfile, + name) + target = target_name or f"{pkg_name}::{name}" + exe_location = self._path(comp.location, pkg_folder, pkg_folder_var) + exes[target] = exe_location + else: + if cpp_info.exe: + assert not cpp_info.libs, "Package has exe and libs" + assert cpp_info.location, "Package has exe and no location" + target_name = self._cmakedeps.get_property("cmake_target_name", self._conanfile) + target = target_name or f"{pkg_name}::{pkg_name}" + exe_location = self._path(cpp_info.location, pkg_folder, pkg_folder_var) + exes[target] = exe_location + + return exes + + def _get_dependencies(self): + """ transitive dependencies Filenames for find_dependency() + """ + # Build requires are already filtered by the get_transitive_requires + transitive_reqs = self._cmakedeps.get_transitive_requires(self._conanfile) + # FIXME: Hardcoded CONFIG + ret = {self._cmakedeps.get_cmake_filename(r): "CONFIG" for r in transitive_reqs.values()} + return ret + + @staticmethod + def _path(p, pkg_folder, pkg_folder_var): + def escape(p_): + return p_.replace("$", "\\$").replace('"', '\\"') + + p = p.replace("\\", "/") + if os.path.isabs(p): + if p.startswith(pkg_folder): + rel = p[len(pkg_folder):].lstrip("/") + return f"${{{pkg_folder_var}}}/{escape(rel)}" + return escape(p) + return f"${{{pkg_folder_var}}}/{escape(p)}" + + @staticmethod + def _escape_cmake_string(values): + return " ".join(v.replace("\\", "\\\\").replace('$', '\\$').replace('"', '\\"') + for v in values) + + @property + def _template(self): + # TODO: Check why not set_property instead of target_link_libraries + return textwrap.dedent("""\ + {%- macro config_wrapper(config, value) -%} + {% if config -%} + $<$:{{value}}> + {%- else -%} + {{value}} + {%- endif %} + {%- endmacro -%} + set({{pkg_folder_var}} "{{pkg_folder}}") + + # Dependencies finding + include(CMakeFindDependencyMacro) + + {% for dep, dep_find_mode in dependencies.items() %} + if(NOT {{dep}}_FOUND) + find_dependency({{dep}} REQUIRED {{dep_find_mode}}) + endif() + {% endfor %} + + ################# Libs information ############## + {% for lib, lib_info in libs.items() %} + #################### {{lib}} #################### + if(NOT TARGET {{ lib }}) + message(STATUS "Conan: Target declared imported {{lib_info["type"]}} library '{{lib}}'") + add_library({{lib}} {{lib_info["type"]}} IMPORTED) + endif() + {% if lib_info.get("includedirs") %} + set_property(TARGET {{lib}} APPEND PROPERTY INTERFACE_INCLUDE_DIRECTORIES + {{config_wrapper(config, lib_info["includedirs"])}}) + {% endif %} + {% if lib_info.get("defines") %} + set_property(TARGET {{lib}} APPEND PROPERTY INTERFACE_COMPILE_DEFINITIONS + {{config_wrapper(config, lib_info["defines"])}}) + {% endif %} + {% if lib_info.get("cxxflags") %} + set_property(TARGET {{lib}} APPEND PROPERTY INTERFACE_COMPILE_OPTIONS + $<$:{{config_wrapper(config, lib_info["cxxflags"])}}>) + {% endif %} + {% if lib_info.get("cflags") %} + set_property(TARGET {{lib}} APPEND PROPERTY INTERFACE_COMPILE_OPTIONS + $<$:{{config_wrapper(config, lib_info["cflags"])}}>) + {% endif %} + {% if lib_info.get("sharedlinkflags") %} + {% set linkflags = config_wrapper(config, lib_info["sharedlinkflags"]) %} + set_property(TARGET {{lib}} APPEND PROPERTY INTERFACE_LINK_OPTIONS + "$<$,SHARED_LIBRARY>:{{linkflags}}>" + "$<$,MODULE_LIBRARY>:{{linkflags}}>") + {% endif %} + {% if lib_info.get("exelinkflags") %} + {% set exeflags = config_wrapper(config, lib_info["exelinkflags"]) %} + set_property(TARGET {{lib}} APPEND PROPERTY INTERFACE_LINK_OPTIONS + "$<$,EXECUTABLE>:{{exeflags}}>") + {% endif %} + + {% if lib_info.get("link_languages") %} + get_property(_languages GLOBAL PROPERTY ENABLED_LANGUAGES) + {% for lang in lib_info["link_languages"] %} + if(NOT "{{lang}}" IN_LIST _languages) + message(SEND_ERROR + "Target {{lib}} has {{lang}} linkage but {{lang}} not enabled in project()") + endif() + set_property(TARGET {{lib}} APPEND PROPERTY + IMPORTED_LINK_INTERFACE_LANGUAGES_{{config}} {{lang}}) + {% endfor %} + {% endif %} + {% if lib_info.get("location") %} + set_property(TARGET {{lib}} APPEND PROPERTY IMPORTED_CONFIGURATIONS {{config}}) + set_target_properties({{lib}} PROPERTIES IMPORTED_LOCATION_{{config}} + "{{lib_info["location"]}}") + {% endif %} + {% if lib_info.get("link_location") %} + set_target_properties({{lib}} PROPERTIES IMPORTED_IMPLIB_{{config}} + "{{lib_info["link_location"]}}") + {% endif %} + {% if lib_info.get("requires") %} + target_link_libraries({{lib}} INTERFACE {{lib_info["requires"]}}) + {% endif %} + {% if lib_info.get("system_libs") %} + target_link_libraries({{lib}} INTERFACE {{lib_info["system_libs"]}}) + {% endif %} + + {% endfor %} + + ################# Global variables for try compile and legacy ############## + {% for prefix in additional_variables_prefixes %} + set({{ prefix }}_VERSION_STRING "{{ version }}") + {% if include_dirs is not none %} + set({{ prefix }}_INCLUDE_DIRS "{{ include_dirs }}" ) + set({{ prefix }}_INCLUDE_DIR "{{ include_dirs }}" ) + {% endif %} + {% if libraries is not none %} + set({{ prefix }}_LIBRARIES {{ libraries }} ) + {% endif %} + {% if definitions is not none %} + set({{ prefix }}_DEFINITIONS {{ definitions}} ) + {% endif %} + {% endfor %} + + ################# Exes information ############## + {% for exe, location in exes.items() %} + #################### {{exe}} #################### + if(NOT TARGET {{ exe }}) + message(STATUS "Conan: Target declared imported executable '{{exe}}' {{context}}") + add_executable({{exe}} IMPORTED) + else() + get_property(_context TARGET {{exe}} PROPERTY CONAN_CONTEXT) + if(NOT $${_context} STREQUAL "{{context}}") + message(STATUS "Conan: Exe {{exe}} was already defined in ${_context}") + get_property(_configurations TARGET {{exe}} PROPERTY IMPORTED_CONFIGURATIONS) + message(STATUS "Conan: Exe {{exe}} defined configurations: ${_configurations}") + foreach(_config ${_configurations}) + set_property(TARGET {{exe}} PROPERTY IMPORTED_LOCATION_${_config}) + endforeach() + set_property(TARGET {{exe}} PROPERTY IMPORTED_CONFIGURATIONS) + endif() + endif() + set_property(TARGET {{exe}} APPEND PROPERTY IMPORTED_CONFIGURATIONS {{config}}) + set_target_properties({{exe}} PROPERTIES IMPORTED_LOCATION_{{config}} "{{location}}") + set_property(TARGET {{exe}} PROPERTY CONAN_CONTEXT "{{context}}") + {% endfor %} + """) diff --git a/conan/tools/cmake/cmakedeps2/targets.py b/conan/tools/cmake/cmakedeps2/targets.py new file mode 100644 index 00000000000..2b934f82502 --- /dev/null +++ b/conan/tools/cmake/cmakedeps2/targets.py @@ -0,0 +1,47 @@ +import textwrap + +import jinja2 +from jinja2 import Template + + +class TargetsTemplate2: + """ + FooTargets.cmake + """ + def __init__(self, cmakedeps, conanfile): + self._cmakedeps = cmakedeps + self._conanfile = conanfile + + def content(self): + t = Template(self._template, trim_blocks=True, lstrip_blocks=True, + undefined=jinja2.StrictUndefined) + return t.render(self._context) + + @property + def filename(self): + f = self._cmakedeps.get_cmake_filename(self._conanfile) + return f"{f}Targets.cmake" + + @property + def _context(self): + filename = self._cmakedeps.get_cmake_filename(self._conanfile) + ret = {"ref": str(self._conanfile.ref), + "filename": filename} + return ret + + @property + def _template(self): + return textwrap.dedent("""\ + message(STATUS "Configuring Targets for {{ ref }}") + + # Load information for each installed configuration. + file(GLOB _target_files "${CMAKE_CURRENT_LIST_DIR}/{{filename}}-Targets-*.cmake") + foreach(_target_file IN LISTS _target_files) + include("${_target_file}") + endforeach() + + file(GLOB _build_files "${CMAKE_CURRENT_LIST_DIR}/{{filename}}-TargetsBuild-*.cmake") + foreach(_build_file IN LISTS _build_files) + include("${_build_file}") + endforeach() + """) diff --git a/conan/tools/cmake/toolchain/blocks.py b/conan/tools/cmake/toolchain/blocks.py index ca8a85e383b..e19b113a90f 100644 --- a/conan/tools/cmake/toolchain/blocks.py +++ b/conan/tools/cmake/toolchain/blocks.py @@ -520,6 +520,10 @@ def to_apple_archs(conanfile): class FindFiles(Block): template = textwrap.dedent("""\ # Define paths to find packages, programs, libraries, etc. + if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/conan_cmakedeps_paths.cmake") + message(STATUS "Conan toolchain: Including CMakeDeps generated conan_find_paths.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/conan_cmakedeps_paths.cmake") + else() {% if find_package_prefer_config %} set(CMAKE_FIND_PACKAGE_PREFER_CONFIG {{ find_package_prefer_config }}) @@ -578,6 +582,7 @@ class FindFiles(Block): set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE "BOTH") endif() {% endif %} + endif() """) def _runtime_dirs_value(self, dirs): diff --git a/conan/tools/google/bazel.py b/conan/tools/google/bazel.py index e0a8cbb4d9d..55702d2db26 100644 --- a/conan/tools/google/bazel.py +++ b/conan/tools/google/bazel.py @@ -11,6 +11,10 @@ def __init__(self, conanfile): :param conanfile: ``< ConanFile object >`` The current recipe object. Always use ``self``. """ self._conanfile = conanfile + # Use BazelToolchain generated file if exists + self._conan_bazelrc = os.path.join(self._conanfile.generators_folder, BazelToolchain.bazelrc_name) + self._use_conan_config = os.path.exists(self._conan_bazelrc) + self._startup_opts = self._get_startup_command_options() def _safe_run_command(self, command): """ @@ -22,7 +26,18 @@ def _safe_run_command(self, command): self._conanfile.run(command) finally: if platform.system() == "Windows": - self._conanfile.run("bazel shutdown") + self._conanfile.run("bazel" + self._startup_opts + " shutdown") + + def _get_startup_command_options(self): + bazelrc_paths = [] + if self._use_conan_config: + bazelrc_paths.append(self._conan_bazelrc) + # User bazelrc paths have more prio than Conan one + # See more info in https://bazel.build/run/bazelrc + bazelrc_paths.extend(self._conanfile.conf.get("tools.google.bazel:bazelrc_path", default=[], + check_type=list)) + opts = " ".join(["--bazelrc=" + rc.replace("\\", "/") for rc in bazelrc_paths]) + return f" {opts}" if opts else "" def build(self, args=None, target="//...", clean=True): """ @@ -41,34 +56,21 @@ def build(self, args=None, target="//...", clean=True): :param clean: boolean that indicates to run a "bazel clean" before running the "bazel build". Notice that this is important to ensure a fresh bazel cache every """ - # Use BazelToolchain generated file if exists - conan_bazelrc = os.path.join(self._conanfile.generators_folder, BazelToolchain.bazelrc_name) - use_conan_config = os.path.exists(conan_bazelrc) - bazelrc_paths = [] - bazelrc_configs = [] - if use_conan_config: - bazelrc_paths.append(conan_bazelrc) - bazelrc_configs.append(BazelToolchain.bazelrc_config) - # User bazelrc paths have more prio than Conan one - # See more info in https://bazel.build/run/bazelrc - bazelrc_paths.extend(self._conanfile.conf.get("tools.google.bazel:bazelrc_path", default=[], - check_type=list)) # Note: In case of error like this: ... https://bcr.bazel.build/: PKIX path building failed # Check this comment: https://github.com/bazelbuild/bazel/issues/3915#issuecomment-1120894057 - command = "bazel" - for rc in bazelrc_paths: - rc = rc.replace("\\", "/") - command += f" --bazelrc={rc}" - command += " build" - bazelrc_configs.extend(self._conanfile.conf.get("tools.google.bazel:configs", default=[], + bazelrc_build_configs = [] + if self._use_conan_config: + bazelrc_build_configs.append(BazelToolchain.bazelrc_config) + command = "bazel" + self._startup_opts + " build" + bazelrc_build_configs.extend(self._conanfile.conf.get("tools.google.bazel:configs", default=[], check_type=list)) - for config in bazelrc_configs: + for config in bazelrc_build_configs: command += f" --config={config}" if args: command += " ".join(f" {arg}" for arg in args) command += f" {target}" if clean: - self._safe_run_command("bazel clean") + self._safe_run_command("bazel" + self._startup_opts + " clean") self._safe_run_command(command) def test(self, target=None): @@ -77,4 +79,4 @@ def test(self, target=None): """ if self._conanfile.conf.get("tools.build:skip_test", check_type=bool) or target is None: return - self._safe_run_command(f'bazel test {target}') + self._safe_run_command("bazel" + self._startup_opts + f" test {target}") diff --git a/conan/tools/google/bazeldeps.py b/conan/tools/google/bazeldeps.py index 44a948a61b1..3aac16e3149 100644 --- a/conan/tools/google/bazeldeps.py +++ b/conan/tools/google/bazeldeps.py @@ -72,13 +72,12 @@ def _get_requirements(conanfile, build_context_activated): yield require, dep -def _get_libs(dep, cpp_info=None, reference_name=None) -> list: +def _get_libs(dep, cpp_info=None) -> list: """ Get the static/shared library paths :param dep: normally a :param cpp_info: of the component. - :param reference_name: Package/Component's reference name. ``None`` by default. :return: list of tuples per static/shared library -> [(name, is_shared, lib_path, import_lib_path)] Note: ``library_path`` could be both static and shared ones in case of UNIX systems. @@ -119,7 +118,7 @@ def _save_lib_path(file_name, file_path): shared_windows_libs[libs[0]] = formatted_path else: # let's cross the fingers... This is the last chance. for lib in libs: - if ref_name in name and ref_name in lib and lib not in shared_windows_libs: + if lib in name and lib not in shared_windows_libs: shared_windows_libs[lib] = formatted_path break elif lib_name is not None: @@ -132,7 +131,6 @@ def _save_lib_path(file_name, file_path): is_shared = _is_shared() libdirs = cpp_info.libdirs bindirs = cpp_info.bindirs if is_shared else [] # just want to get shared libraries - ref_name = reference_name or dep.ref.name if hasattr(cpp_info, "aggregated_components"): # Global cpp_info total_libs_number = len(cpp_info.aggregated_components().libs) diff --git a/conan/tools/meson/meson.py b/conan/tools/meson/meson.py index 1f99ade0efc..17fa5924100 100644 --- a/conan/tools/meson/meson.py +++ b/conan/tools/meson/meson.py @@ -46,8 +46,7 @@ def configure(self, reconfigure=False): else: # extra native file for cross-building scenarios cmd += f' --native-file "{native}"' cmd += ' "{}" "{}"'.format(build_folder, source_folder) - # Issue related: https://github.com/mesonbuild/meson/issues/12880 - cmd += ' --prefix=/' # this must be an absolute path, otherwise, meson complains + cmd += f" --prefix={self._prefix}" self._conanfile.output.info("Meson configure cmd: {}".format(cmd)) self._conanfile.run(cmd) @@ -112,3 +111,30 @@ def _install_verbosity(self): # so it's a bit backwards verbosity = self._conanfile.conf.get("tools.build:verbosity", choices=("quiet", "verbose")) return "--quiet" if verbosity else "" + + @property + def _prefix(self): + """Generate a valid ``--prefix`` argument value for meson. + For conan, the prefix must be similar to the Unix root directory ``/``. + + The result of this function should be passed to + ``meson setup --prefix={self._prefix} ...`` + + Python 3.13 changed the semantics of ``/`` on the Windows ntpath module, + it is now special-cased as a relative directory. + Thus, ``os.path.isabs("/")`` is true on Linux but false on Windows. + So for Windows, an equivalent path is ``C:\\``. However, this can be + parsed wrongly in meson in specific circumstances due to the trailing + backslash. Hence, we also use forward slashes for Windows, leaving us + with ``C:/`` or similar paths. + + See also + -------- + * The meson issue discussing the need to set ``--prefix`` to ``/``: + `mesonbuild/meson#12880 `_ + * The cpython PR introducing the ``/`` behavior change: + `python/cpython#113829 `_ + * The issue detailing the erroneous parsing of ``\\``: + `conan-io/conan#14213 `_ + """ + return os.path.abspath("/").replace("\\", "/") diff --git a/conans/model/build_info.py b/conans/model/build_info.py index 4505075e6a1..4b4ef9f334b 100644 --- a/conans/model/build_info.py +++ b/conans/model/build_info.py @@ -79,6 +79,8 @@ def __init__(self, set_defaults=False): self._sharedlinkflags = None # linker flags self._exelinkflags = None # linker flags self._objects = None # linker flags + self._exe = None # application executable, only 1 allowed, following CPS + self._languages = None self._sysroot = None self._requires = None @@ -119,9 +121,11 @@ def serialize(self): "sysroot": self._sysroot, "requires": self._requires, "properties": self._properties, + "exe": self._exe, # single exe, incompatible with libs "type": self._type, "location": self._location, - "link_location": self._link_location + "link_location": self._link_location, + "languages": self._languages } @staticmethod @@ -131,6 +135,14 @@ def deserialize(contents): setattr(result, f"_{field}", value) return result + def clone(self): + # Necessary below for exploding a cpp_info.libs = [lib1, lib2] into components + result = _Component() + for k, v in vars(self).items(): + if k.startswith("_"): + setattr(result, k, copy.copy(v)) + return result + @property def includedirs(self): if self._includedirs is None: @@ -258,6 +270,14 @@ def libs(self): def libs(self, value): self._libs = value + @property + def exe(self): + return self._exe + + @exe.setter + def exe(self, value): + self._exe = value + @property def type(self): return self._type @@ -282,6 +302,14 @@ def link_location(self): def link_location(self, value): self._link_location = value + @property + def languages(self): + return self._languages + + @languages.setter + def languages(self, value): + self._languages = value + @property def defines(self): if self._defines is None: @@ -453,7 +481,9 @@ def relocate(el): def parsed_requires(self): return [r.split("::", 1) if "::" in r else (None, r) for r in self.requires] - def deduce_cps(self, pkg_type): + def deduce_locations(self, pkg_type): + if self._exe: # exe is a new field, it should have the correct location + return if self._location or self._link_location: if self._type is None or self._type is PackageType.HEADER: raise ConanException("Incorrect cpp_info defining location without type or header") @@ -461,13 +491,16 @@ def deduce_cps(self, pkg_type): if self._type not in [None, PackageType.SHARED, PackageType.STATIC, PackageType.APP]: return - # Recipe didn't specify things, need to auto deduce - libdirs = [x.replace("\\", "/") for x in self.libdirs] - bindirs = [x.replace("\\", "/") for x in self.bindirs] + if len(self.libs) == 0: + return if len(self.libs) != 1: raise ConanException("More than 1 library defined in cpp_info.libs, cannot deduce CPS") + # Recipe didn't specify things, need to auto deduce + libdirs = [x.replace("\\", "/") for x in self.libdirs] + bindirs = [x.replace("\\", "/") for x in self.bindirs] + # TODO: Do a better handling of pre-defined type libname = self.libs[0] static_patterns = [f"{libname}.lib", f"{libname}.a", f"lib{libname}.a"] @@ -519,6 +552,7 @@ class CppInfo: def __init__(self, set_defaults=False): self.components = defaultdict(lambda: _Component(set_defaults)) + self.default_components = None self._package = _Component(set_defaults) def __getattr__(self, attr): @@ -526,19 +560,22 @@ def __getattr__(self, attr): return getattr(self._package, attr) def __setattr__(self, attr, value): - if attr in ("components", "_package", "_aggregated"): + if attr in ("components", "default_components", "_package", "_aggregated"): super(CppInfo, self).__setattr__(attr, value) else: setattr(self._package, attr, value) def serialize(self): ret = {"root": self._package.serialize()} + if self.default_components: + ret["default_components"] = self.default_components for component_name, info in self.components.items(): ret[component_name] = info.serialize() return ret def deserialize(self, content): self._package = _Component.deserialize(content.pop("root")) + self.default_components = content.get("default_components") for component_name, info in content.items(): self.components[component_name] = _Component.deserialize(info) return self @@ -678,3 +715,37 @@ def required_components(self): # Then split the names ret = [r.split("::") if "::" in r else (None, r) for r in ret] return ret + + def deduce_full_cpp_info(self, conanfile): + pkg_type = conanfile.package_type + + result = CppInfo() # clone it + + if self.libs and len(self.libs) > 1: # expand in multiple components + ConanOutput().warning(f"{conanfile}: The 'cpp_info.libs' contain more than 1 library. " + "Define 'cpp_info.components' instead.") + assert not self.components, f"{conanfile} cpp_info shouldn't have .libs and .components" + for lib in self.libs: + c = _Component() # Do not do a full clone, we don't need the properties + c.type = self.type # This should be a string + c.includedirs = self.includedirs + c.libdirs = self.libdirs + c.libs = [lib] + result.components[f"_{lib}"] = c + + common = self._package.clone() + common.libs = [] + common.type = str(PackageType.HEADER) # the type of components is a string! + common.requires = list(result.components.keys()) + (self.requires or []) + + result.components["_common"] = common + else: + result._package = self._package.clone() + result.default_components = self.default_components + result.components = {k: v.clone() for k, v in self.components.items()} + + result._package.deduce_locations(pkg_type) + for comp in result.components.values(): + comp.deduce_locations(pkg_type) + + return result diff --git a/conans/model/conf.py b/conans/model/conf.py index cdbf0cb55cc..ebc41bc5885 100644 --- a/conans/model/conf.py +++ b/conans/model/conf.py @@ -80,6 +80,7 @@ "tools.cmake.cmake_layout:build_folder": "(Experimental) Allow configuring the base folder of the build for local builds", "tools.cmake.cmake_layout:test_folder": "(Experimental) Allow configuring the base folder of the build for test_package", "tools.cmake:cmake_program": "Path to CMake executable", + "tools.cmake.cmakedeps:new": "Use the new CMakeDeps generator", "tools.cmake:install_strip": "Add --strip to cmake.install()", "tools.deployer:symlinks": "Set to False to disable deployers copying symlinks", "tools.files.download:retry": "Number of retries in case of failure when downloading", diff --git a/conans/model/pkg_type.py b/conans/model/pkg_type.py index 29c207952c5..d850d03d4ba 100644 --- a/conans/model/pkg_type.py +++ b/conans/model/pkg_type.py @@ -58,6 +58,11 @@ def deduce_from_options(): if conanfile_type is PackageType.UNKNOWN: raise ConanException(f"{conanfile}: Package type is 'library'," " but no 'shared' option declared") + elif any(option in conanfile.options for option in ["shared", "header_only"]): + conanfile.output.warning(f"{conanfile}: package_type '{conanfile_type}' is defined, " + "but 'shared' and/or 'header_only' options are present. " + "The package_type will have precedence over the options " + "regardless of their value.") conanfile.package_type = conanfile_type else: # automatic default detection with option shared/header-only conanfile.package_type = deduce_from_options() diff --git a/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_new.py b/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_new.py new file mode 100644 index 00000000000..e2c3a84bea9 --- /dev/null +++ b/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_new.py @@ -0,0 +1,1104 @@ +import os +import platform +import re +import textwrap + +import pytest + +from conan.test.assets.genconanfile import GenConanfile +from conan.test.assets.sources import gen_function_h, gen_function_cpp +from conan.test.utils.tools import TestClient + + +new_value = "will_break_next" + + +@pytest.mark.tool("cmake") +class TestExes: + @pytest.mark.parametrize("tool_requires", [False, True]) + def test_exe(self, tool_requires): + conanfile = textwrap.dedent(r""" + import os + from conan import ConanFile + from conan.tools.cmake import CMake + + class Test(ConanFile): + name = "mytool" + version = "0.1" + package_type = "application" + settings = "os", "arch", "compiler", "build_type" + generators = "CMakeToolchain" + exports_sources = "*" + + def build(self): + cmake = CMake(self) + cmake.configure() + cmake.build() + + def package(self): + cmake = CMake(self) + cmake.install() + + def package_info(self): + self.cpp_info.exe = "mytool" + self.cpp_info.set_property("cmake_target_name", "MyTool::myexe") + self.cpp_info.location = os.path.join("bin", "mytool") + """) + main = textwrap.dedent(""" + #include + #include + + int main() { + std::cout << "Mytool generating out.c!!!!!" << std::endl; + std::ofstream f("out.c"); + } + """) + c = TestClient() + c.run("new cmake_exe -d name=mytool -d version=0.1") + c.save({"conanfile.py": conanfile, + "src/main.cpp": main}) + c.run("create .") + + requires = "tool_requires" if tool_requires else "requires" + consumer = textwrap.dedent(f""" + from conan import ConanFile + from conan.tools.cmake import CMakeDeps, CMakeToolchain, CMake, cmake_layout + class Consumer(ConanFile): + settings = "os", "compiler", "arch", "build_type" + {requires} = "mytool/0.1" + + def generate(self): + deps = CMakeDeps(self) + deps.generate() + tc = CMakeToolchain(self) + tc.generate() + + def layout(self): + cmake_layout(self) + def build(self): + cmake = CMake(self) + cmake.configure() + cmake.build() + """) + cmake = textwrap.dedent(""" + cmake_minimum_required(VERSION 3.15) + project(consumer C) + + find_package(mytool) + add_custom_command(OUTPUT out.c COMMAND MyTool::myexe) + add_library(myLib out.c) + """) + c.save({"conanfile.py": consumer, + "CMakeLists.txt": cmake}, clean_first=True) + c.run(f"build . -c tools.cmake.cmakedeps:new={new_value}") + assert "Conan: Target declared imported executable 'MyTool::myexe'" in c.out + assert "Mytool generating out.c!!!!!" in c.out + + def test_exe_components(self): + conanfile = textwrap.dedent(r""" + import os + from conan import ConanFile + from conan.tools.cmake import CMake + + class Test(ConanFile): + name = "mytool" + version = "0.1" + package_type = "application" + settings = "os", "arch", "compiler", "build_type" + generators = "CMakeToolchain" + exports_sources = "*" + + def build(self): + cmake = CMake(self) + cmake.configure() + cmake.build() + + def package(self): + cmake = CMake(self) + cmake.install() + + def package_info(self): + self.cpp_info.components["my1exe"].exe = "mytool1" + self.cpp_info.components["my1exe"].set_property("cmake_target_name", "MyTool::my1exe") + self.cpp_info.components["my1exe"].location = os.path.join("bin", "mytool1") + self.cpp_info.components["my2exe"].exe = "mytool2" + self.cpp_info.components["my2exe"].set_property("cmake_target_name", "MyTool::my2exe") + self.cpp_info.components["my2exe"].location = os.path.join("bin", "mytool2") + """) + main = textwrap.dedent(""" + #include + #include + + int main() {{ + std::cout << "Mytool{number} generating out{number}.c!!!!!" << std::endl; + std::ofstream f("out{number}.c"); + }} + """) + cmake = textwrap.dedent(""" + set(CMAKE_CXX_COMPILER_WORKS 1) + set(CMAKE_CXX_ABI_COMPILED 1) + cmake_minimum_required(VERSION 3.15) + project(proj CXX) + + add_executable(mytool1 src/main1.cpp) + add_executable(mytool2 src/main2.cpp) + + install(TARGETS mytool1 DESTINATION "." RUNTIME DESTINATION bin) + install(TARGETS mytool2 DESTINATION "." RUNTIME DESTINATION bin) + """) + c = TestClient() + c.save({"conanfile.py": conanfile, + "CMakeLists.txt": cmake, + "src/main1.cpp": main.format(number=1), + "src/main2.cpp": main.format(number=2) + }) + c.run("create .") + + consumer = textwrap.dedent(""" + from conan import ConanFile + from conan.tools.cmake import CMakeDeps, CMakeToolchain, CMake, cmake_layout + class Consumer(ConanFile): + settings = "os", "compiler", "arch", "build_type" + tool_requires = "mytool/0.1" + + def generate(self): + deps = CMakeDeps(self) + deps.generate() + tc = CMakeToolchain(self) + tc.generate() + def layout(self): + cmake_layout(self) + def build(self): + cmake = CMake(self) + cmake.configure() + cmake.build() + """) + cmake = textwrap.dedent(""" + cmake_minimum_required(VERSION 3.15) + project(consumer C) + + find_package(mytool) + add_custom_command(OUTPUT out1.c COMMAND MyTool::my1exe) + add_custom_command(OUTPUT out2.c COMMAND MyTool::my2exe) + add_library(myLib out1.c out2.c) + """) + c.save({"conanfile.py": consumer, + "CMakeLists.txt": cmake}, clean_first=True) + c.run(f"build . -c tools.cmake.cmakedeps:new={new_value}") + assert "Conan: Target declared imported executable 'MyTool::my1exe'" in c.out + assert "Mytool1 generating out1.c!!!!!" in c.out + assert "Conan: Target declared imported executable 'MyTool::my2exe'" in c.out + assert "Mytool2 generating out2.c!!!!!" in c.out + + +@pytest.mark.tool("cmake") +class TestLibs: + def test_libs(self, matrix_client): + c = matrix_client + c.run("new cmake_lib -d name=app -d version=0.1 -d requires=matrix/1.0") + c.run(f"build . -c tools.cmake.cmakedeps:new={new_value}") + assert "Conan: Target declared imported STATIC library 'matrix::matrix'" in c.out + + @pytest.mark.parametrize("shared", [False, True]) + def test_libs_transitive(self, transitive_libraries, shared): + c = transitive_libraries + c.run("new cmake_lib -d name=app -d version=0.1 -d requires=engine/1.0") + shared = "-o engine/*:shared=True" if shared else "" + c.run(f"build . {shared} -c tools.cmake.cmakedeps:new={new_value}") + if shared: + assert "matrix::matrix" not in c.out # It is hidden as static behind the engine + assert "Conan: Target declared imported SHARED library 'engine::engine'" in c.out + else: + assert "Conan: Target declared imported STATIC library 'matrix::matrix'" in c.out + assert "Conan: Target declared imported STATIC library 'engine::engine'" in c.out + + def test_multilevel_shared(self): + # TODO: make this shared fixtures in conftest for multi-level shared testing + c = TestClient(default_server_user=True) + c.run("new cmake_lib -d name=matrix -d version=0.1") + c.run(f"create . -o *:shared=True -c tools.cmake.cmakedeps:new={new_value}") + + c.save({}, clean_first=True) + c.run("new cmake_lib -d name=engine -d version=0.1 -d requires=matrix/0.1") + c.run(f"create . -o *:shared=True -c tools.cmake.cmakedeps:new={new_value}") + + c.save({}, clean_first=True) + c.run("new cmake_lib -d name=gamelib -d version=0.1 -d requires=engine/0.1") + c.run(f"create . -o *:shared=True -c tools.cmake.cmakedeps:new={new_value}") + + c.save({}, clean_first=True) + c.run("new cmake_exe -d name=game -d version=0.1 -d requires=gamelib/0.1") + c.run(f"create . -o *:shared=True -c tools.cmake.cmakedeps:new={new_value}") + + assert "matrix/0.1: Hello World Release!" + assert "engine/0.1: Hello World Release!" + assert "gamelib/0.1: Hello World Release!" + assert "game/0.1: Hello World Release!" + + # Make sure it works downloading to another cache + c.run("upload * -r=default -c") + c.run("remove * -c") + + c2 = TestClient(servers=c.servers) + c2.run("new cmake_exe -d name=game -d version=0.1 -d requires=gamelib/0.1") + c2.run(f"create . -o *:shared=True -c tools.cmake.cmakedeps:new={new_value}") + + assert "matrix/0.1: Hello World Release!" + assert "engine/0.1: Hello World Release!" + assert "gamelib/0.1: Hello World Release!" + assert "game/0.1: Hello World Release!" + + +class TestLibsLinkageTraits: + def test_linkage_shared_static(self): + """ + the static library is skipped + """ + c = TestClient() + c.run("new cmake_lib -d name=matrix -d version=0.1") + c.run(f"create . -c tools.cmake.cmakedeps:new={new_value} -tf=") + + c.save({}, clean_first=True) + c.run("new cmake_lib -d name=engine -d version=0.1 -d requires=matrix/0.1") + c.run(f"create . -o engine/*:shared=True -c tools.cmake.cmakedeps:new={new_value} -tf=") + + c.save({}, clean_first=True) + c.run("new cmake_exe -d name=game -d version=0.1 -d requires=engine/0.1") + c.run(f"create . -o engine/*:shared=True -c tools.cmake.cmakedeps:new={new_value} " + "-c tools.compilation:verbosity=verbose") + assert re.search(r"Skipped binaries(\s*)matrix/0.1", c.out) + assert "matrix/0.1: Hello World Release!" + assert "engine/0.1: Hello World Release!" + assert "game/0.1: Hello World Release!" + + +@pytest.mark.tool("cmake") +class TestLibsComponents: + def test_libs_components(self, matrix_client_components): + """ + explicit usage of components + """ + c = matrix_client_components + + # TODO: Check that find_package(.. COMPONENTS nonexisting) fails + c.run("new cmake_exe -d name=app -d version=0.1 -d requires=matrix/1.0") + cmake = textwrap.dedent(""" + cmake_minimum_required(VERSION 3.15) + project(app CXX) + + find_package(matrix CONFIG REQUIRED) + add_executable(app src/app.cpp) + # standard one, and a custom cmake_target_name one + target_link_libraries(app PRIVATE matrix::module MatrixHeaders) + """) + app_cpp = textwrap.dedent(""" + #include "module.h" + #include "headers.h" + int main() { module(); headers();} + """) + c.save({"CMakeLists.txt": cmake, + "src/app.cpp": app_cpp}) + c.run(f"build . -c tools.cmake.cmakedeps:new={new_value}") + assert "Conan: Target declared imported STATIC library 'matrix::vector'" in c.out + assert "Conan: Target declared imported STATIC library 'matrix::module'" in c.out + if platform.system() == "Windows": + c.run_command(r".\build\Release\app.exe") + assert "Matrix headers __cplusplus: __cplusplus2014" in c.out + + def test_libs_components_default(self, matrix_client_components): + """ + Test that the default components are used when no component is specified + """ + c = matrix_client_components + + c.run("new cmake_exe -d name=app -d version=0.1 -d requires=matrix/1.0") + + app_cpp = textwrap.dedent(""" + #include "module.h" + int main() { module();} + """) + cmake = textwrap.dedent(""" + cmake_minimum_required(VERSION 3.15) + project(app CXX) + + find_package(matrix CONFIG REQUIRED) + add_executable(app src/app.cpp) + target_link_libraries(app PRIVATE matrix::matrix) + """) + c.save({"src/app.cpp": app_cpp, + "CMakeLists.txt": cmake}) + c.run(f"build . -c tools.cmake.cmakedeps:new={new_value}") + assert "Conan: Target declared imported STATIC library 'matrix::vector'" in c.out + assert "Conan: Target declared imported STATIC library 'matrix::module'" in c.out + assert "Conan: Target declared imported INTERFACE library 'MatrixHeaders'" in c.out + + def test_libs_components_default_error(self, matrix_client_components): + """ + Same as above, but it fails, because headers is not in the default components + """ + c = matrix_client_components + + c.run("new cmake_exe -d name=app -d version=0.1 -d requires=matrix/1.0") + + app_cpp = textwrap.dedent(""" + #include "module.h" + #include "headers.h" + int main() { module();} + """) + cmake = textwrap.dedent(""" + cmake_minimum_required(VERSION 3.15) + project(app CXX) + + find_package(matrix CONFIG REQUIRED) + add_executable(app src/app.cpp) + target_link_libraries(app PRIVATE matrix::matrix) + """) + c.save({"src/app.cpp": app_cpp, + "CMakeLists.txt": cmake}) + c.run(f"build . -c tools.cmake.cmakedeps:new={new_value}", assert_error=True) + assert "Error in build() method, line 35" in c.out + assert "Conan: Target declared imported STATIC library 'matrix::vector'" in c.out + assert "Conan: Target declared imported STATIC library 'matrix::module'" in c.out + cmake = textwrap.dedent(""" + cmake_minimum_required(VERSION 3.15) + project(app CXX) + + find_package(matrix CONFIG REQUIRED) + add_executable(app src/app.cpp) + target_link_libraries(app PRIVATE matrix::matrix MatrixHeaders) + """) + c.save({"CMakeLists.txt": cmake}) + c.run(f"build . -c tools.cmake.cmakedeps:new={new_value}") + assert "Running CMake.build()" in c.out # Now it doesn't fail + + def test_libs_components_transitive(self, matrix_client_components): + """ + explicit usage of components + matrix::module -> matrix::vector + + engine::bots -> engine::physix + engine::physix -> matrix::vector + engine::world -> engine::physix, matrix::module + """ + c = matrix_client_components + + from conan.test.assets.sources import gen_function_h + bots_h = gen_function_h(name="bots") + from conan.test.assets.sources import gen_function_cpp + bots_cpp = gen_function_cpp(name="bots", includes=["bots", "physix"], calls=["physix"]) + physix_h = gen_function_h(name="physix") + physix_cpp = gen_function_cpp(name="physix", includes=["physix", "vector"], calls=["vector"]) + world_h = gen_function_h(name="world") + world_cpp = gen_function_cpp(name="world", includes=["world", "physix", "module"], + calls=["physix", "module"]) + + conanfile = textwrap.dedent(""" + from conan import ConanFile + from conan.tools.cmake import CMake + from conan.tools.files import copy + + class Engine(ConanFile): + name = "engine" + version = "1.0" + settings = "os", "compiler", "build_type", "arch" + generators = "CMakeToolchain" + exports_sources = "src/*", "CMakeLists.txt" + + requires = "matrix/1.0" + generators = "CMakeDeps", "CMakeToolchain" + + def build(self): + cmake = CMake(self) + cmake.configure() + cmake.build() + + def package(self): + cmake = CMake(self) + cmake.install() + + def package_info(self): + self.cpp_info.components["bots"].libs = ["bots"] + self.cpp_info.components["bots"].includedirs = ["include"] + self.cpp_info.components["bots"].libdirs = ["lib"] + self.cpp_info.components["bots"].requires = ["physix"] + + self.cpp_info.components["physix"].libs = ["physix"] + self.cpp_info.components["physix"].includedirs = ["include"] + self.cpp_info.components["physix"].libdirs = ["lib"] + self.cpp_info.components["physix"].requires = ["matrix::vector"] + + self.cpp_info.components["world"].libs = ["world"] + self.cpp_info.components["world"].includedirs = ["include"] + self.cpp_info.components["world"].libdirs = ["lib"] + self.cpp_info.components["world"].requires = ["physix", "matrix::module"] + """) + + cmakelists = textwrap.dedent(""" + set(CMAKE_CXX_COMPILER_WORKS 1) + set(CMAKE_CXX_ABI_COMPILED 1) + cmake_minimum_required(VERSION 3.15) + project(matrix CXX) + + find_package(matrix CONFIG REQUIRED) + + add_library(physix src/physix.cpp) + add_library(bots src/bots.cpp) + add_library(world src/world.cpp) + + target_link_libraries(physix PRIVATE matrix::vector) + target_link_libraries(bots PRIVATE physix) + target_link_libraries(world PRIVATE physix matrix::module) + + set_target_properties(bots PROPERTIES PUBLIC_HEADER "src/bots.h") + set_target_properties(physix PROPERTIES PUBLIC_HEADER "src/physix.h") + set_target_properties(world PROPERTIES PUBLIC_HEADER "src/world.h") + install(TARGETS physix bots world) + """) + c.save({"src/physix.h": physix_h, + "src/physix.cpp": physix_cpp, + "src/bots.h": bots_h, + "src/bots.cpp": bots_cpp, + "src/world.h": world_h, + "src/world.cpp": world_cpp, + "CMakeLists.txt": cmakelists, + "conanfile.py": conanfile}) + c.run("create .") + + c.save({}, clean_first=True) + c.run("new cmake_exe -d name=app -d version=0.1 -d requires=engine/1.0") + cmake = textwrap.dedent(""" + cmake_minimum_required(VERSION 3.15) + project(app CXX) + + find_package(engine CONFIG REQUIRED) + + add_executable(app src/app.cpp) + target_link_libraries(app PRIVATE engine::bots) + + install(TARGETS app) + """) + app_cpp = textwrap.dedent(""" + #include "bots.h" + int main() { bots();} + """) + c.save({"CMakeLists.txt": cmake, + "src/app.cpp": app_cpp}) + c.run(f"create . -c tools.cmake.cmakedeps:new={new_value}") + assert "Conan: Target declared imported STATIC library 'matrix::vector'" in c.out + assert "Conan: Target declared imported STATIC library 'matrix::module'" in c.out + assert "Conan: Target declared imported INTERFACE library 'matrix::matrix'" in c.out + assert "Conan: Target declared imported STATIC library 'engine::bots'" in c.out + assert "Conan: Target declared imported STATIC library 'engine::physix'" in c.out + assert "Conan: Target declared imported STATIC library 'engine::world'" in c.out + assert "Conan: Target declared imported INTERFACE library 'engine::engine'" in c.out + + assert "bots: Release!" in c.out + assert "physix: Release!" in c.out + assert "vector: Release!" in c.out + + def test_libs_components_multilib(self): + """ + cpp_info.libs = ["lib1", "lib2"] + """ + c = TestClient() + + from conan.test.assets.sources import gen_function_h + vector_h = gen_function_h(name="vector") + from conan.test.assets.sources import gen_function_cpp + vector_cpp = gen_function_cpp(name="vector", includes=["vector"]) + module_h = gen_function_h(name="module") + module_cpp = gen_function_cpp(name="module", includes=["module"]) + + conanfile = textwrap.dedent(""" + from conan import ConanFile + from conan.tools.cmake import CMake + + class Matrix(ConanFile): + name = "matrix" + version = "1.0" + settings = "os", "compiler", "build_type", "arch" + generators = "CMakeToolchain" + exports_sources = "src/*", "CMakeLists.txt" + + generators = "CMakeDeps", "CMakeToolchain" + + def build(self): + cmake = CMake(self) + cmake.configure() + cmake.build() + + def package(self): + cmake = CMake(self) + cmake.install() + + def package_info(self): + self.cpp_info.set_property("cmake_target_name", "MyMatrix::MyMatrix") + self.cpp_info.libs = ["module", "vector"] + """) + + cmakelists = textwrap.dedent(""" + set(CMAKE_CXX_COMPILER_WORKS 1) + set(CMAKE_CXX_ABI_COMPILED 1) + cmake_minimum_required(VERSION 3.15) + project(matrix CXX) + + add_library(module src/module.cpp) + add_library(vector src/vector.cpp) + + set_target_properties(vector PROPERTIES PUBLIC_HEADER "src/vector.h") + set_target_properties(module PROPERTIES PUBLIC_HEADER "src/module.h") + install(TARGETS module vector) + """) + c.save({"src/module.h": module_h, + "src/module.cpp": module_cpp, + "src/vector.h": vector_h, + "src/vector.cpp": vector_cpp, + "CMakeLists.txt": cmakelists, + "conanfile.py": conanfile}) + c.run("create .") + + c.save({}, clean_first=True) + c.run("new cmake_exe -d name=app -d version=0.1 -d requires=matrix/1.0") + cmake = textwrap.dedent(""" + cmake_minimum_required(VERSION 3.15) + project(app CXX) + + find_package(matrix CONFIG REQUIRED) + + add_executable(app src/app.cpp) + target_link_libraries(app PRIVATE MyMatrix::MyMatrix) + + install(TARGETS app) + """) + app_cpp = textwrap.dedent(""" + #include "vector.h" + #include "module.h" + int main() { vector();module();} + """) + c.save({"CMakeLists.txt": cmake, + "src/app.cpp": app_cpp}) + c.run(f"create . -c tools.cmake.cmakedeps:new={new_value}") + assert "Conan: Target declared imported STATIC library 'matrix::_vector'" in c.out + assert "Conan: Target declared imported STATIC library 'matrix::_module'" in c.out + assert "Conan: Target declared imported INTERFACE library 'MyMatrix::MyMatrix'" in c.out + assert "matrix::matrix" not in c.out + + assert "vector: Release!" in c.out + assert "module: Release!" in c.out + assert "vector: Release!" in c.out + + +@pytest.mark.tool("cmake") +class TestHeaders: + def test_header_lib(self, matrix_client): + c = matrix_client + conanfile = textwrap.dedent(""" + from conan import ConanFile + from conan.tools.files import copy + class EngineHeader(ConanFile): + name = "engine" + version = "1.0" + requires = "matrix/1.0" + exports_sources = "*.h" + settings = "compiler" + def package(self): + copy(self, "*.h", src=self.source_folder, dst=self.package_folder) + def package_id(self): + self.info.clear() + def package_info(self): + self.cpp_info.defines = ["MY_MATRIX_HEADERS_DEFINE=1", + "MY_MATRIX_HEADERS_DEFINE2=1"] + # Few flags to cover that CMakeDeps doesn't crash with them + if self.settings.compiler == "msvc": + self.cpp_info.cxxflags = ["/Zc:__cplusplus"] + self.cpp_info.cflags = ["/Zc:__cplusplus"] + self.cpp_info.system_libs = ["ws2_32"] + else: + self.cpp_info.system_libs = ["m", "dl"] + # Just to verify CMake don't break + if self.settings.compiler == "gcc": + self.cpp_info.sharedlinkflags = ["-z now", "-z relro"] + self.cpp_info.exelinkflags = ["-z now", "-z relro"] + """) + engine_h = textwrap.dedent(""" + #pragma once + #include + #include "matrix.h" + #ifndef MY_MATRIX_HEADERS_DEFINE + #error "Fatal error MY_MATRIX_HEADERS_DEFINE not defined" + #endif + #ifndef MY_MATRIX_HEADERS_DEFINE2 + #error "Fatal error MY_MATRIX_HEADERS_DEFINE2 not defined" + #endif + void engine(){ std::cout << "Engine!" < + + #ifndef MY_MATRIX_HEADERS_{version}_DEFINE + #error "Fatal error MY_MATRIX_HEADERS_{version}_DEFINE not defined" + #endif + void engine(){{ std::cout << "Engine {version}!" < + #include + #include "protobuf.h" + + int main() { + protobuf(); + #ifdef NDEBUG + std::cout << "Protoc RELEASE generating out.c!!!!!" << std::endl; + #else + std::cout << "Protoc DEBUG generating out.c!!!!!" << std::endl; + #endif + std::ofstream f("out.c"); + } + """) + cmake = textwrap.dedent(""" + cmake_minimum_required(VERSION 3.15) + project(protobuf CXX) + + add_library(protobuf src/protobuf.cpp) + add_executable(protoc src/main.cpp) + target_link_libraries(protoc PRIVATE protobuf) + set_target_properties(protobuf PROPERTIES PUBLIC_HEADER "src/protobuf.h") + + install(TARGETS protoc protobuf) + """) + c = TestClient() + c.save({"conanfile.py": conanfile, + "CMakeLists.txt": cmake, + "src/protobuf.h": gen_function_h(name="protobuf"), + "src/protobuf.cpp": gen_function_cpp(name="protobuf", includes=["protobuf"]), + "src/main.cpp": main}) + c.run("export .") + + consumer = textwrap.dedent(""" + import os + from conan import ConanFile + from conan.tools.cmake import CMake, cmake_layout + class Consumer(ConanFile): + settings = "os", "compiler", "arch", "build_type" + requires = "protobuf/0.1" + generators = "CMakeToolchain", "CMakeDeps" + + def layout(self): + cmake_layout(self) + def build(self): + cmake = CMake(self) + cmake.configure() + cmake.build() + self.run(os.path.join(self.cpp.build.bindir, "myapp")) + """) + cmake = textwrap.dedent(""" + cmake_minimum_required(VERSION 3.15) + project(consumer CXX) + + find_package(MyProtobuf CONFIG REQUIRED) + add_custom_command(OUTPUT out.c COMMAND Protobuf::Protocompile) + add_executable(myapp myapp.cpp out.c) + target_link_libraries(myapp PRIVATE protobuf::protobuf) + get_target_property(imported_configs Protobuf::Protocompile IMPORTED_CONFIGURATIONS) + message(STATUS "Protoc imported configurations: ${imported_configs}!!!") + """) + myapp = textwrap.dedent(""" + #include + #include "protobuf.h" + + int main() { + protobuf(); + std::cout << "MyApp" << std::endl; + } + """) + c.save({"conanfile.py": consumer, + "CMakeLists.txt": cmake, + "myapp.cpp": myapp}, clean_first=True) + return c + + def test_requires(self, protobuf): + c = protobuf + c.run(f"build . --build=missing -c tools.cmake.cmakedeps:new={new_value}") + assert "Conan: Target declared imported STATIC library 'protobuf::protobuf'" in c.out + assert "Conan: Target declared imported executable 'Protobuf::Protocompile'" in c.out + assert "Protoc RELEASE generating out.c!!!!!" in c.out + assert 'Protoc imported configurations: RELEASE!!!' in c.out + + def test_both(self, protobuf): + consumer = textwrap.dedent(""" + import os + from conan import ConanFile + from conan.tools.cmake import CMake, cmake_layout + class Consumer(ConanFile): + settings = "os", "compiler", "arch", "build_type" + requires = "protobuf/0.1" + tool_requires = "protobuf/0.1" + generators = "CMakeToolchain", "CMakeDeps" + + def layout(self): + cmake_layout(self) + def build(self): + cmake = CMake(self) + cmake.configure() + cmake.build() + """) + c = protobuf + c.save({"conanfile.py": consumer}) + c.run("build . -s:h build_type=Debug --build=missing " + f"-c tools.cmake.cmakedeps:new={new_value}") + + assert "Conan: Target declared imported STATIC library 'protobuf::protobuf'" in c.out + assert "Conan: Target declared imported executable 'Protobuf::Protocompile'" in c.out + assert "Protoc RELEASE generating out.c!!!!!" in c.out + assert "protobuf: Release!" in c.out + assert "protobuf: Debug!" not in c.out + assert 'Protoc imported configurations: RELEASE!!!' in c.out + + cmd = "./build/Debug/myapp" if platform.system() != "Windows" else r"build\Debug\myapp" + c.run_command(cmd) + assert "protobuf: Debug!" in c.out + assert "protobuf: Release!" not in c.out + + c.run("build . --build=missing " + f"-c tools.cmake.cmakedeps:new={new_value}") + + assert "Conan: Target declared imported STATIC library 'protobuf::protobuf'" in c.out + assert "Conan: Target declared imported executable 'Protobuf::Protocompile'" in c.out + assert "Protoc RELEASE generating out.c!!!!!" in c.out + assert "protobuf: Release!" in c.out + assert "protobuf: Debug!" not in c.out + assert 'Protoc imported configurations: RELEASE!!!' in c.out + + cmd = "./build/Release/myapp" if platform.system() != "Windows" else r"build\Release\myapp" + c.run_command(cmd) + assert "protobuf: Debug!" not in c.out + assert "protobuf: Release!" in c.out + + +@pytest.mark.tool("cmake", "3.23") +class TestConfigs: + @pytest.mark.skipif(platform.system() != "Windows", reason="Only MSVC multi-conf") + def test_multi_config(self, matrix_client): + c = matrix_client + c.run("new cmake_exe -d name=app -d version=0.1 -d requires=matrix/1.0") + c.run(f"install . -c tools.cmake.cmakedeps:new={new_value}") + c.run("install . -s build_type=Debug --build=missing " + f"-c tools.cmake.cmakedeps:new={new_value}") + + c.run_command("cmake --preset conan-default") + c.run_command("cmake --build --preset conan-release") + c.run_command("cmake --build --preset conan-debug") + + c.run_command("build\\Release\\app") + assert "matrix/1.0: Hello World Release!" in c.out + assert "app/0.1: Hello World Release!" in c.out + c.run_command("build\\Debug\\app") + assert "matrix/1.0: Hello World Debug!" in c.out + assert "app/0.1: Hello World Debug!" in c.out + + def test_cross_config(self, matrix_client): + # Release dependencies, but compiling app in Debug + c = matrix_client + c.run("new cmake_exe -d name=app -d version=0.1 -d requires=matrix/1.0") + c.run(f"install . -s &:build_type=Debug -c tools.cmake.cmakedeps:new={new_value}") + + # With modern CMake > 3.26 not necessary set(CMAKE_MAP_IMPORTED_CONFIG_DEBUG Release) + cmake = textwrap.dedent(""" + cmake_minimum_required(VERSION 3.15) + project(app CXX) + set(CMAKE_MAP_IMPORTED_CONFIG_DEBUG Release) + find_package(matrix CONFIG REQUIRED) + + add_executable(app src/app.cpp src/main.cpp) + target_link_libraries(app PRIVATE matrix::matrix) + """) + c.save({"CMakeLists.txt": cmake}) + + preset = "conan-default" if platform.system() == "Windows" else "conan-debug" + c.run_command(f"cmake --preset {preset}") + c.run_command("cmake --build --preset conan-debug") + + c.run_command(os.path.join("build", "Debug", "app")) + assert "matrix/1.0: Hello World Release!" in c.out + assert "app/0.1: Hello World Debug!" in c.out + + @pytest.mark.skipif(platform.system() == "Windows", reason="This doesn't work in MSVC") + def test_cross_config_implicit(self, matrix_client): + # Release dependencies, but compiling app in Debug, without specifying it + c = matrix_client + c.run("new cmake_exe -d name=app -d version=0.1 -d requires=matrix/1.0") + c.run(f"install . -c tools.cmake.cmakedeps:new={new_value}") + + # With modern CMake > 3.26 not necessary set(CMAKE_MAP_IMPORTED_CONFIG_DEBUG Release) + cmake = textwrap.dedent(""" + cmake_minimum_required(VERSION 3.15) + project(app CXX) + set(CMAKE_MAP_IMPORTED_CONFIG_DEBUG Release) + find_package(matrix CONFIG REQUIRED) + + add_executable(app src/app.cpp src/main.cpp) + target_link_libraries(app PRIVATE matrix::matrix) + """) + + c.save({"CMakeLists.txt": cmake}) + # Now we can force the Debug build, even if dependencies are Release + c.run_command("cmake . -DCMAKE_BUILD_TYPE=Debug -B build " + "-DCMAKE_PREFIX_PATH=build/Release/generators") + c.run_command("cmake --build build") + c.run_command("./build/app") + assert "matrix/1.0: Hello World Release!" in c.out + assert "app/0.1: Hello World Debug!" in c.out + + +@pytest.mark.tool("cmake", "3.23") +class TestCMakeTry: + + def test_check_c_source_compiles(self, matrix_client): + """ + https://github.com/conan-io/conan/issues/12012 + """ + c = matrix_client # it brings the "matrix" package dependency pre-built + + consumer = textwrap.dedent(""" + from conan import ConanFile + from conan.tools.cmake import CMakeDeps + class PkgConan(ConanFile): + settings = "os", "arch", "compiler", "build_type" + requires = "matrix/1.0" + generators = "CMakeToolchain", + def generate(self): + deps = CMakeDeps(self) + deps.set_property("matrix", "cmake_additional_variables_prefixes", ["MyMatrix"]) + deps.generate() + """) + + cmakelist = textwrap.dedent("""\ + cmake_minimum_required(VERSION 3.15) + project(Hello LANGUAGES CXX) + + find_package(matrix CONFIG REQUIRED) + include(CheckCXXSourceCompiles) + + set(CMAKE_REQUIRED_INCLUDES ${MyMatrix_INCLUDE_DIRS}) + set(CMAKE_REQUIRED_LIBRARIES ${MyMatrix_LIBRARIES}) + check_cxx_source_compiles("#include + int main(void) { matrix();return 0; }" IT_COMPILES) + """) + + c.save({"conanfile.py": consumer, + "CMakeLists.txt": cmakelist}, clean_first=True) + c.run(f"install . -c tools.cmake.cmakedeps:new={new_value}") + + preset = "conan-default" if platform.system() == "Windows" else "conan-release" + c.run_command(f"cmake --preset {preset} ") + assert "Performing Test IT_COMPILES - Success" in c.out diff --git a/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_new_cpp_linkage.py b/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_new_cpp_linkage.py new file mode 100644 index 00000000000..0932a77fa35 --- /dev/null +++ b/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_new_cpp_linkage.py @@ -0,0 +1,54 @@ +import platform +import textwrap +import pytest + + +new_value = "will_break_next" + + +@pytest.mark.skipif(platform.system() == "Windows", reason="Windows doesn't fail to link") +def test_auto_cppstd(matrix_c_interface_client): + c = matrix_c_interface_client + # IMPORTANT: This must be a C and CXX CMake project!! + consumer = textwrap.dedent(""" + cmake_minimum_required(VERSION 3.15) + project(myapp C CXX) + + find_package(matrix REQUIRED) + + add_executable(app app.c) + target_link_libraries(app PRIVATE matrix::matrix) + """) + + conanfile = textwrap.dedent("""\ + import os + from conan import ConanFile + from conan.tools.cmake import CMake, cmake_layout + + class Recipe(ConanFile): + settings = "os", "compiler", "build_type", "arch" + package_type = "application" + generators = "CMakeToolchain", "CMakeDeps" + requires = "matrix/0.1" + + def layout(self): + cmake_layout(self) + + def build(self): + cmake = CMake(self) + cmake.configure() + cmake.build() + self.run(os.path.join(self.cpp.build.bindir, "app"), env="conanrun") + """) + app = textwrap.dedent(""" + #include "matrix.h" + int main(){ + matrix(); + return 0; + } + """) + c.save({"conanfile.py": conanfile, + "CMakeLists.txt": consumer, + "app.c": app}, clean_first=True) + c.run(f"build . -c tools.cmake.cmakedeps:new={new_value}") + assert "Hello Matrix!" in c.out diff --git a/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_new_paths.py b/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_new_paths.py new file mode 100644 index 00000000000..f3c04e165f3 --- /dev/null +++ b/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_new_paths.py @@ -0,0 +1,97 @@ +import re +import textwrap + +import pytest + +from conan.test.assets.genconanfile import GenConanfile +from conan.test.utils.tools import TestClient + +new_value = "will_break_next" + + +@pytest.fixture +def client(): + c = TestClient() + pkg = textwrap.dedent(""" + from conan import ConanFile + from conan.tools.cmake import CMake, cmake_layout + import os + class Pkg(ConanFile): + settings = "build_type", "os", "arch", "compiler" + requires = "dep/0.1" + generators = "CMakeDeps", "CMakeToolchain" + def layout(self): # Necessary to force config files in another location + cmake_layout(self) + def build(self): + cmake = CMake(self) + cmake.configure(variables={"CMAKE_FIND_DEBUG_MODE": "ON"}) + """) + cmake = textwrap.dedent(""" + cmake_minimum_required(VERSION 3.15) + project(pkgb LANGUAGES NONE) + find_package(dep CONFIG REQUIRED) + """) + c.save({"dep/conanfile.py": GenConanfile("dep", "0.1"), + "pkg/conanfile.py": pkg, + "pkg/CMakeLists.txt": cmake}) + return c + + +@pytest.mark.tool("cmake") +def test_cmake_generated(client): + c = client + c.run("create dep") + c.run(f"build pkg -c tools.cmake.cmakedeps:new={new_value}") + assert "Conan toolchain: Including CMakeDeps generated conan_find_paths.cmake" in c.out + assert "Conan: Target declared imported INTERFACE library 'dep::dep'" in c.out + + +@pytest.mark.tool("cmake") +def test_cmake_in_package(client): + c = client + # same, but in-package + dep = textwrap.dedent(""" + import os + from conan import ConanFile + from conan.tools.files import save + class Pkg(ConanFile): + name = "dep" + version = "0.1" + + def package(self): + content = 'message(STATUS "Hello from dep dep-Config.cmake!!!!!")' + save(self, os.path.join(self.package_folder, "cmake", "dep-config.cmake"), content) + def package_info(self): + self.cpp_info.set_property("cmake_find_mode", "none") + self.cpp_info.builddirs = ["cmake"] + """) + + c.save({"dep/conanfile.py": dep}) + c.run("create dep") + c.run(f"build pkg -c tools.cmake.cmakedeps:new={new_value}") + assert "Conan toolchain: Including CMakeDeps generated conan_find_paths.cmake" in c.out + assert "Hello from dep dep-Config.cmake!!!!!" in c.out + + +class TestRuntimeDirs: + + def test_runtime_lib_dirs_multiconf(self): + client = TestClient() + app = GenConanfile().with_requires("dep/1.0").with_generator("CMakeDeps")\ + .with_settings("build_type") + client.save({"lib/conanfile.py": GenConanfile(), + "dep/conanfile.py": GenConanfile("dep").with_requires("onelib/1.0", + "twolib/1.0"), + "app/conanfile.py": app}) + client.run("create lib --name=onelib --version=1.0") + client.run("create lib --name=twolib --version=1.0") + client.run("create dep --version=1.0") + + client.run(f'install app -s build_type=Release -c tools.cmake.cmakedeps:new={new_value}') + client.run(f'install app -s build_type=Debug -c tools.cmake.cmakedeps:new={new_value}') + + contents = client.load("app/conan_cmakedeps_paths.cmake") + pattern_lib_dirs = r"set\(CONAN_RUNTIME_LIB_DIRS ([^)]*)\)" + runtime_lib_dirs = re.search(pattern_lib_dirs, contents).group(1) + assert "" in runtime_lib_dirs + assert "" in runtime_lib_dirs diff --git a/test/functional/toolchains/conftest.py b/test/functional/toolchains/conftest.py index e1f294b62a6..b81671e870f 100644 --- a/test/functional/toolchains/conftest.py +++ b/test/functional/toolchains/conftest.py @@ -1,8 +1,10 @@ import os import shutil +import textwrap import pytest +from conan.test.assets.sources import gen_function_h, gen_function_cpp from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import TestClient @@ -48,3 +50,188 @@ def transitive_libraries(_transitive_libraries): c.cache_folder = os.path.join(temp_folder(), ".conan2") shutil.copytree(_transitive_libraries.cache_folder, c.cache_folder) return c + + +@pytest.fixture(scope="session") +def _matrix_client_components(): + """ + 2 components, different than the package name + """ + c = TestClient() + headers_h = textwrap.dedent(""" + #include + #ifndef MY_MATRIX_HEADERS_DEFINE + #error "Fatal error MY_MATRIX_HEADERS_DEFINE not defined" + #endif + void headers(){ std::cout << "Matrix headers: Release!" << std::endl; + #if __cplusplus + std::cout << " Matrix headers __cplusplus: __cplusplus" << __cplusplus << std::endl; + #endif + } + """) + vector_h = gen_function_h(name="vector") + vector_cpp = gen_function_cpp(name="vector", includes=["vector"]) + module_h = gen_function_h(name="module") + module_cpp = gen_function_cpp(name="module", includes=["module", "vector"], calls=["vector"]) + + conanfile = textwrap.dedent(""" + from conan import ConanFile + from conan.tools.cmake import CMake + + class Matrix(ConanFile): + name = "matrix" + version = "1.0" + settings = "os", "compiler", "build_type", "arch" + generators = "CMakeToolchain" + exports_sources = "src/*", "CMakeLists.txt" + + def build(self): + cmake = CMake(self) + cmake.configure() + cmake.build() + + def package(self): + cmake = CMake(self) + cmake.install() + + def package_info(self): + self.cpp_info.default_components = ["vector", "module"] + + self.cpp_info.components["headers"].includedirs = ["include/headers"] + self.cpp_info.components["headers"].set_property("cmake_target_name", "MatrixHeaders") + self.cpp_info.components["headers"].defines = ["MY_MATRIX_HEADERS_DEFINE=1"] + # Few flags to cover that CMakeDeps doesn't crash with them + if self.settings.compiler == "msvc": + self.cpp_info.components["headers"].cxxflags = ["/Zc:__cplusplus"] + self.cpp_info.components["headers"].cflags = ["/Zc:__cplusplus"] + self.cpp_info.components["headers"].system_libs = ["ws2_32"] + else: + self.cpp_info.components["headers"].system_libs = ["m"] + # Just to verify CMake don't break + self.cpp_info.sharedlinkflags = ["-z now", "-z relro"] + self.cpp_info.exelinkflags = ["-z now", "-z relro"] + + self.cpp_info.components["vector"].libs = ["vector"] + self.cpp_info.components["vector"].includedirs = ["include"] + self.cpp_info.components["vector"].libdirs = ["lib"] + + self.cpp_info.components["module"].libs = ["module"] + self.cpp_info.components["module"].includedirs = ["include"] + self.cpp_info.components["module"].libdirs = ["lib"] + self.cpp_info.components["module"].requires = ["vector"] + """) + + cmakelists = textwrap.dedent(""" + set(CMAKE_CXX_COMPILER_WORKS 1) + set(CMAKE_CXX_ABI_COMPILED 1) + cmake_minimum_required(VERSION 3.15) + project(matrix CXX) + + add_library(vector src/vector.cpp) + add_library(module src/module.cpp) + add_library(headers INTERFACE) + target_link_libraries(module PRIVATE vector) + + set_target_properties(headers PROPERTIES PUBLIC_HEADER "src/headers.h") + set_target_properties(module PROPERTIES PUBLIC_HEADER "src/module.h") + set_target_properties(vector PROPERTIES PUBLIC_HEADER "src/vector.h") + install(TARGETS vector module) + install(TARGETS headers PUBLIC_HEADER DESTINATION include/headers) + """) + c.save({"src/headers.h": headers_h, + "src/vector.h": vector_h, + "src/vector.cpp": vector_cpp, + "src/module.h": module_h, + "src/module.cpp": module_cpp, + "CMakeLists.txt": cmakelists, + "conanfile.py": conanfile}) + c.run("create .") + return c + + +@pytest.fixture() +def matrix_client_components(_matrix_client_components): + c = TestClient() + c.cache_folder = os.path.join(temp_folder(), ".conan2") + shutil.copytree(_matrix_client_components.cache_folder, c.cache_folder) + return c + + +@pytest.fixture(scope="session") +def _matrix_c_interface_client(): + c = TestClient() + matrix_h = textwrap.dedent("""\ + #pragma once + #ifdef __cplusplus + extern "C" { + #endif + void matrix(); + #ifdef __cplusplus + } + #endif + """) + matrix_cpp = textwrap.dedent("""\ + #include "matrix.h" + #include + #include + void matrix(){ + std::cout<< std::string("Hello Matrix!") < + $ + ) + set_target_properties(matrix PROPERTIES PUBLIC_HEADER "include/matrix.h") + install(TARGETS matrix EXPORT matrixConfig) + export(TARGETS matrix + NAMESPACE matrix:: + FILE "${CMAKE_CURRENT_BINARY_DIR}/matrixConfig.cmake" + ) + install(EXPORT matrixConfig + DESTINATION "${CMAKE_INSTALL_PREFIX}/matrix/cmake" + NAMESPACE matrix:: + ) + """) + conanfile = textwrap.dedent("""\ + from conan import ConanFile + from conan.tools.cmake import CMake, cmake_layout + class Recipe(ConanFile): + name = "matrix" + version = "0.1" + settings = "os", "compiler", "build_type", "arch" + package_type = "static-library" + generators = "CMakeToolchain" + exports_sources = "CMakeLists.txt", "src/*", "include/*" + languages = "C++" + def build(self): + cmake = CMake(self) + cmake.configure() + cmake.build() + def layout(self): + cmake_layout(self) + def package(self): + cmake = CMake(self) + cmake.install() + def package_info(self): + self.cpp_info.libs = ["matrix"] + """) + c.save({"include/matrix.h": matrix_h, + "src/matrix.cpp": matrix_cpp, + "conanfile.py": conanfile, + "CMakeLists.txt": cmake}) + c.run("create .") + return c + + +@pytest.fixture() +def matrix_c_interface_client(_matrix_c_interface_client): + c = TestClient() + c.cache_folder = os.path.join(temp_folder(), ".conan2") + shutil.copytree(_matrix_c_interface_client.cache_folder, c.cache_folder) + return c diff --git a/test/integration/graph/core/test_auto_package_type.py b/test/integration/graph/core/test_auto_package_type.py index 01db28fe8fd..9e12c11ccfa 100644 --- a/test/integration/graph/core/test_auto_package_type.py +++ b/test/integration/graph/core/test_auto_package_type.py @@ -1,3 +1,5 @@ +import textwrap + import pytest from conan.test.utils.tools import TestClient @@ -36,6 +38,7 @@ def test_auto_package_type(conanfile): c.run("graph info . --filter package_type") assert "package_type: static-library" in c.out c.run("graph info . --filter package_type -o shared=True") + assert "The package_type will have precedence over the options" not in c.out assert "package_type: shared-library" in c.out c.run("graph info . --filter package_type -o shared=True -o header_only=False") assert "package_type: shared-library" in c.out @@ -43,3 +46,21 @@ def test_auto_package_type(conanfile): assert "package_type: header-library" in c.out c.run("graph info . --filter package_type -o header_only=True -o shared=False") assert "package_type: header-library" in c.out + +def test_package_type_and_header_library(): + """ Show that forcing a package_type and header_only=True does not change the package_type""" + tc = TestClient(light=True) + tc.save({"conanfile.py": textwrap.dedent(""" + from conan import ConanFile + + class Pkg(ConanFile): + package_type = "static-library" + options = {"header_only": [True, False]} + + """)}) + tc.run("graph info . --filter package_type -o &:header_only=False") + assert "package_type: static-library" in tc.out + assert "The package_type will have precedence over the options" in tc.out + tc.run("graph info . --filter package_type -o &:header_only=True") + assert "package_type: static-library" in tc.out + assert "The package_type will have precedence over the options" in tc.out diff --git a/test/integration/py_requires/python_requires_test.py b/test/integration/py_requires/python_requires_test.py index 007ed086dfb..ac032298a42 100644 --- a/test/integration/py_requires/python_requires_test.py +++ b/test/integration/py_requires/python_requires_test.py @@ -832,6 +832,49 @@ def generate(self): assert "OptionBASE: True" in c.out assert "OptionDERIVED: False" in c.out + def test_options_default_update(self): + c = TestClient(light=True) + base = textwrap.dedent(""" + from conan import ConanFile + class BaseConan: + options = {"base": [True, False]} + default_options = {"base": True} + + class PyReq(ConanFile): + name = "base" + version = "1.0.0" + package_type = "python-require" + """) + derived = textwrap.dedent(""" + import conan + + class DerivedConan(conan.ConanFile): + name = "derived" + python_requires = "base/1.0.0" + python_requires_extend = "base.BaseConan" + options = {"derived": [True, False]} + default_options = {"derived": False} + + def init(self): + base = self.python_requires["base"].module.BaseConan + self.options.update(base.options, base.default_options) + self.options.base = False + + def generate(self): + self.output.info(f"OptionBASE: {self.options.base}") + self.output.info(f"OptionDERIVED: {self.options.derived}") + """) + c.save({"base/conanfile.py": base, + "derived/conanfile.py": derived}) + c.run("create base") + c.run("install derived") + assert "OptionBASE: False" in c.out + assert "OptionDERIVED: False" in c.out + + c.run("install derived -o=&:base=True") + assert "OptionBASE: True" in c.out + assert "OptionDERIVED: False" in c.out + def test_transitive_python_requires(): # https://github.com/conan-io/conan/issues/8546 diff --git a/test/integration/toolchains/google/test_bazeldeps.py b/test/integration/toolchains/google/test_bazeldeps.py index f0d03a48249..a9a0b3d784c 100644 --- a/test/integration/toolchains/google/test_bazeldeps.py +++ b/test/integration/toolchains/google/test_bazeldeps.py @@ -1039,6 +1039,25 @@ def package(self): def package_info(self): self.cpp_info.libs = ["zdll"] """) + libiconv = textwrap.dedent(""" + import os + from conan import ConanFile + from conan.tools.files import save + class Example(ConanFile): + name = "libiconv" + version = "1.0" + options = {"shared": [True, False]} + default_options = {"shared": False} + def package(self): + bindirs = os.path.join(self.package_folder, "bin") + libdirs = os.path.join(self.package_folder, "lib") + save(self, os.path.join(bindirs, "charset-1.dll"), "") + save(self, os.path.join(bindirs, "iconv-2.dll"), "") + save(self, os.path.join(libdirs, "charset.lib"), "") + save(self, os.path.join(libdirs, "iconv.lib"), "") + def package_info(self): + self.cpp_info.libs = ["iconv", "charset"] + """) openssl = textwrap.dedent(""" import os from conan import ConanFile @@ -1081,20 +1100,25 @@ def package_info(self): zlib/1.0 openssl/1.0 libcurl/1.0 + libiconv/1.0 [options] *:shared=True """) c.save({"conanfile.txt": consumer, "zlib/conanfile.py": zlib, "openssl/conanfile.py": openssl, - "libcurl/conanfile.py": libcurl}) + "libcurl/conanfile.py": libcurl, + "libiconv/conanfile.py": libiconv, + }) c.run("export-pkg zlib -o:a shared=True") c.run("export-pkg openssl -o:a shared=True") c.run("export-pkg libcurl -o:a shared=True") + c.run("export-pkg libiconv -o:a shared=True") c.run("install . -g BazelDeps") libcurl_bazel_build = load(None, os.path.join(c.current_folder, "libcurl", "BUILD.bazel")) zlib_bazel_build = load(None, os.path.join(c.current_folder, "zlib", "BUILD.bazel")) openssl_bazel_build = load(None, os.path.join(c.current_folder, "openssl", "BUILD.bazel")) + libiconv_bazel_build = load(None, os.path.join(c.current_folder, "libiconv", "BUILD.bazel")) libcurl_expected = textwrap.dedent("""\ # Components precompiled libs cc_import( @@ -1118,14 +1142,27 @@ def package_info(self): ) """) zlib_expected = textwrap.dedent("""\ - # Components precompiled libs - # Root package precompiled libs cc_import( name = "zdll_precompiled", shared_library = "bin/zlib1.dll", interface_library = "lib/zdll.lib", ) """) + iconv_expected = textwrap.dedent("""\ + cc_import( + name = "iconv_precompiled", + shared_library = "bin/iconv-2.dll", + interface_library = "lib/iconv.lib", + ) + """) + charset_expected = textwrap.dedent("""\ + cc_import( + name = "charset_precompiled", + shared_library = "bin/charset-1.dll", + interface_library = "lib/charset.lib", + ) + """) assert libcurl_expected in libcurl_bazel_build assert zlib_expected in zlib_bazel_build assert openssl_expected in openssl_bazel_build + assert iconv_expected in libiconv_bazel_build and charset_expected in libiconv_bazel_build diff --git a/test/unittests/tools/google/test_bazel.py b/test/unittests/tools/google/test_bazel.py index 5c5aa583693..3aa21d364d5 100644 --- a/test/unittests/tools/google/test_bazel.py +++ b/test/unittests/tools/google/test_bazel.py @@ -54,8 +54,10 @@ def test_bazel_command_with_config_values(): conanfile.conf.define("tools.google.bazel:bazelrc_path", ["/path/to/bazelrc"]) bazel = Bazel(conanfile) bazel.build(target='//test:label') + commands = conanfile.commands assert "bazel --bazelrc=/path/to/bazelrc build " \ - "--config=config --config=config2 //test:label" in conanfile.commands + "--config=config --config=config2 //test:label" in commands + assert "bazel --bazelrc=/path/to/bazelrc clean" in commands @pytest.mark.parametrize("path, pattern, expected", [