# The MIT License (MIT) # # Copyright (c) 2018 Mateusz Pusz # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import os import re from conan import ConanFile from conan.errors import ConanInvalidConfiguration from conan.tools.build import can_run, default_cppstd, valid_min_cppstd from conan.tools.cmake import CMake, CMakeDeps, CMakeToolchain, cmake_layout from conan.tools.files import copy, load, rmdir from conan.tools.scm import Version required_conan_version = ">=2.0.15" class MPUnitsConan(ConanFile): name = "mp-units" homepage = "https://github.com/mpusz/mp-units" description = "The quantities and units library for C++" topics = ( "units", "dimensions", "quantities", "dimensional-analysis", "physical-quantities", "physical-units", "system-of-units", "system-of-quantities", "isq", "si", "library", "quantity-manipulation", ) license = "MIT" url = "https://github.com/mpusz/mp-units" settings = "os", "arch", "compiler", "build_type" options = { "cxx_modules": [True, False], "import_std": [True, False], "std_format": [True, False], "no_crtp": [True, False], "contracts": ["none", "gsl-lite", "ms-gsl"], "freestanding": [True, False], } default_options = { # "cxx_modules" default set in config_options() # "import_std" default set in config_options() # "std_format" default set in config_options() # "no_crtp" default set in config_options() "contracts": "gsl-lite", "freestanding": False, } implements = ["auto_header_only"] exports = "LICENSE.md" exports_sources = ( "docs/*", "src/*", "test/*", "cmake/*", "example/*", "CMakeLists.txt", ) no_copy_source = True @property def _feature_compatibility(self): return { "minimum_support": { "min_cppstd": "20", "compiler": { "gcc": "12", "clang": "16", "apple-clang": "15", "msvc": "194", }, }, "std_format": { "min_cppstd": "20", "compiler": { "gcc": "13", "clang": "17", "apple-clang": "16", "msvc": "194", }, }, "cxx_modules": { "min_cppstd": "20", "compiler": {"gcc": "", "clang": "17", "apple-clang": "", "msvc": ""}, }, "import_std": { "min_cppstd": "23", "compiler": {"gcc": "", "clang": "18", "apple-clang": "", "msvc": ""}, }, "explicit_this": { "min_cppstd": "23", "compiler": { "gcc": "14", "clang": "18", "apple-clang": "", "msvc": "", }, }, } @property def _option_feature_map(self): return { "std_format": "std_format", "cxx_modules": "cxx_modules", "import_std": "import_std", "no_crtp": "explicit_this", } def _set_default_option(self, name): compiler = self.settings.compiler feature_name = self._option_feature_map[name] feature = self._feature_compatibility[feature_name] min_version = feature["compiler"].get(str(compiler)) setattr( self.options, name, bool( min_version and Version(compiler.version) >= min_version and valid_min_cppstd(self, feature["min_cppstd"]) ), ) def _check_feature_supported(self, name, feature_name=name): compiler = self.settings.compiler cppstd = compiler.get_safe("cppstd", default_cppstd(self)) feature = self._feature_compatibility[feature_name] # check C++ version if not valid_min_cppstd(self, feature["min_cppstd"]): raise ConanInvalidConfiguration( f"'{name}' requires at least cppstd={feature['min_cppstd']} ({cppstd} in use)", ) # check compiler version min_version = feature["compiler"].get(str(compiler)) if min_version == None: # not tested compiler being used - use at your own risk return if min_version == "": raise ConanInvalidConfiguration( f"'{name}' is not yet supported by any known {compiler} version" ) if Version(compiler.version) < min_version: raise ConanInvalidConfiguration( f"'{name}' requires at least {compiler}-{min_version} ({compiler}-{compiler.version} in use)" ) @property def _build_all(self): return bool(self.conf.get("user.mp-units.build:all", default=False)) @property def _skip_la(self): # broken until https://github.com/BobSteagall/wg21/issues/77 is fixed return bool(self.conf.get("user.mp-units.build:skip_la", default=True)) @property def _run_clang_tidy(self): return bool(self.conf.get("user.mp-units.analyze:clang-tidy", default=False)) def set_version(self): content = load(self, os.path.join(self.recipe_folder, "src/CMakeLists.txt")) version = re.search( r"project\([^\)]+VERSION (\d+\.\d+\.\d+)[^\)]*\)", content ).group(1) self.version = version.strip() def config_options(self): for key in self._option_feature_map.keys(): self._set_default_option(key) # TODO mixing of `import std;` and regular header files includes does not work for now if self.options.import_std: self.options.contracts = "none" def configure(self): if self.options.cxx_modules: self.package_type = "static-library" else: self.package_type = "header-library" if self.options.freestanding: self.options.rm_safe("std_format") def requirements(self): if not self.options.freestanding: if self.options.contracts == "gsl-lite": self.requires("gsl-lite/0.41.0", transitive_headers=True) elif self.options.contracts == "ms-gsl": self.requires("ms-gsl/4.0.0", transitive_headers=True) if not self.options.std_format: self.requires("fmt/11.0.1", transitive_headers=True) def build_requirements(self): self.tool_requires("cmake/[>=3.31 <4]") if self._build_all: if not self.options.freestanding: self.test_requires("catch2/3.7.0") if not self._skip_la: self.test_requires("wg21-linear_algebra/0.7.3") def validate(self): self._check_feature_supported("mp-units", "minimum_support") for key, value in self._option_feature_map.items(): if self.options.get_safe(key) == True: self._check_feature_supported(key, value) if self.options.freestanding and self.options.contracts != "none": raise ConanInvalidConfiguration( "'contracts' should be set to 'none' for a freestanding build" ) # TODO mixing of `import std;` and regular header files includes does not work for now if self.options.import_std: if self.options.contracts != "none": raise ConanInvalidConfiguration( "'contracts' should be set to 'none' to use `import std;`" ) if not self.options.get_safe("std_format", default=True): raise ConanInvalidConfiguration( "'std_format' should be enabled to use `import std;`" ) def layout(self): cmake_layout(self) def generate(self): opt = self.options tc = CMakeToolchain(self) tc.absolute_paths = True # only needed for CMake CI if self._build_all: tc.cache_variables["CMAKE_EXPORT_COMPILE_COMMANDS"] = True tc.cache_variables["CMAKE_VERIFY_INTERFACE_HEADER_SETS"] = ( not opt.import_std ) tc.cache_variables["MP_UNITS_DEV_BUILD_LA"] = not self._skip_la if self._run_clang_tidy: tc.cache_variables["MP_UNITS_DEV_CLANG_TIDY"] = True if opt.cxx_modules: tc.cache_variables["CMAKE_CXX_SCAN_FOR_MODULES"] = True tc.cache_variables["MP_UNITS_BUILD_CXX_MODULES"] = True if opt.import_std: tc.cache_variables["CMAKE_CXX_MODULE_STD"] = True # Current experimental support according to `Help/dev/experimental.rst` tc.cache_variables["CMAKE_EXPERIMENTAL_CXX_IMPORT_STD"] = ( "0e5b6991-d74f-4b3d-a41c-cf096e0b2508" ) # TODO remove the below when Conan will learn to handle C++ modules if opt.freestanding: tc.cache_variables["MP_UNITS_API_FREESTANDING"] = True else: tc.cache_variables["MP_UNITS_API_STD_FORMAT"] = opt.std_format tc.cache_variables["MP_UNITS_API_NO_CRTP"] = opt.no_crtp tc.cache_variables["MP_UNITS_API_CONTRACTS"] = str(opt.contracts).upper() tc.generate() deps = CMakeDeps(self) deps.generate() def build(self): cmake = CMake(self) cmake.configure(build_script_folder=None if self._build_all else "src") if self._build_all or self.options.cxx_modules: cmake.build() if self._build_all: if not self.options.import_std: cmake.build(target="all_verify_interface_header_sets") if can_run(self): cmake.ctest(cli_args=["--output-on-failure"]) def package(self): copy( self, "LICENSE.md", self.source_folder, os.path.join(self.package_folder, "licenses"), ) cmake = CMake(self) cmake.install() # TODO remove the below when Conan will learn to handle C++ modules if not self.options.cxx_modules: # We have to preserve those files for C++ modules build as Conan # can't generate such CMake targets for now rmdir(self, os.path.join(self.package_folder, "lib", "cmake")) def package_info(self): compiler = self.settings.compiler # TODO remove the branch when Conan will learn to handle C++ modules if self.options.cxx_modules: # CMakeDeps does not generate C++ modules definitions for now # Skip the Conan-generated files and use the mp-unitsConfig.cmake bundled with mp-units self.cpp_info.set_property("cmake_find_mode", "none") self.cpp_info.builddirs = ["."] else: # handle contracts if self.options.contracts == "none": self.cpp_info.components["core"].defines.append( "MP_UNITS_API_CONTRACTS=0" ) elif self.options.contracts == "gsl-lite": self.cpp_info.components["core"].requires.append("gsl-lite::gsl-lite") self.cpp_info.components["core"].defines.append( "MP_UNITS_API_CONTRACTS=2" ) elif self.options.contracts == "ms-gsl": self.cpp_info.components["core"].requires.append("ms-gsl::ms-gsl") self.cpp_info.components["core"].defines.append( "MP_UNITS_API_CONTRACTS=3" ) # handle API options self.cpp_info.components["core"].defines.append( "MP_UNITS_API_NO_CRTP=" + str(int(self.options.no_crtp == True)) ) self.cpp_info.components["core"].defines.append( "MP_UNITS_API_STD_FORMAT=" + str(int(self.options.std_format == True)) ) if not self.options.std_format: self.cpp_info.components["core"].requires.append("fmt::fmt") # handle hosted configuration if not self.options.freestanding: self.cpp_info.components["core"].defines.append("MP_UNITS_HOSTED=1") # handle import std if self.options.import_std: self.cpp_info.components["core"].defines.append("MP_UNITS_IMPORT_STD") if compiler == "clang" and Version(compiler.version) < 19: self.cpp_info.components["core"].cxxflags.append( "-Wno-deprecated-declarations" ) if compiler == "msvc": self.cpp_info.components["core"].cxxflags.append("/utf-8") self.cpp_info.components["systems"].requires = ["core"]