|
| 1 | +import json |
| 2 | +import os |
| 3 | +import re |
| 4 | +from conans import ConanFile, CMake, tools |
| 5 | + |
| 6 | +required_conan_version = ">=1.33.0" |
| 7 | + |
| 8 | +class NmosCppConan(ConanFile): |
| 9 | + name = "nmos-cpp" |
| 10 | + description = "An NMOS C++ Implementation" |
| 11 | + license = "Apache-2.0" |
| 12 | + url = "https://github.com/conan-io/conan-center-index" |
| 13 | + homepage = "https://github.com/sony/nmos-cpp" |
| 14 | + topics = ("amwa", "nmos", "is-04", "is-05", "is-07", "is-08", "is-09", "broadcasting", "network", "media") |
| 15 | + |
| 16 | + settings = "os", "compiler", "build_type", "arch" |
| 17 | + # for now, no "shared" option support |
| 18 | + options = { |
| 19 | + "fPIC": [True, False], |
| 20 | + "with_dnssd": ["mdnsresponder", "avahi"], |
| 21 | + } |
| 22 | + # "fPIC" is handled automatically by Conan, injecting CMAKE_POSITION_INDEPENDENT_CODE |
| 23 | + default_options = { |
| 24 | + "fPIC": True, |
| 25 | + "with_dnssd": "mdnsresponder", |
| 26 | + } |
| 27 | + |
| 28 | + # wrapper CMakeLists.txt to call conan_basic_setup() |
| 29 | + exports_sources = ["CMakeLists.txt"] |
| 30 | + # use cmake_find_package_multi and prefer config-file packages |
| 31 | + generators = "cmake", "cmake_find_package_multi" |
| 32 | + |
| 33 | + short_paths = True |
| 34 | + |
| 35 | + _cmake = None |
| 36 | + |
| 37 | + # for out-of-source build, cf. wrapper CMakeLists.txt |
| 38 | + @property |
| 39 | + def _source_subfolder(self): |
| 40 | + return "source_subfolder" |
| 41 | + |
| 42 | + @property |
| 43 | + def _build_subfolder(self): |
| 44 | + return "build_subfolder" |
| 45 | + |
| 46 | + def config_options(self): |
| 47 | + if self.settings.os == "Windows": |
| 48 | + del self.options.fPIC |
| 49 | + |
| 50 | + if self.settings.os == "Macos": |
| 51 | + del self.options.with_dnssd |
| 52 | + elif self.settings.os == "Linux": |
| 53 | + self.options.with_dnssd = "avahi" |
| 54 | + elif self.settings.os == "Windows": |
| 55 | + self.options.with_dnssd = "mdnsresponder" |
| 56 | + |
| 57 | + def requirements(self): |
| 58 | + # for now, consistent with project's conanfile.txt |
| 59 | + self.requires("boost/1.76.0") |
| 60 | + self.requires("cpprestsdk/2.10.18") |
| 61 | + self.requires("websocketpp/0.8.2") |
| 62 | + self.requires("openssl/1.1.1l") |
| 63 | + self.requires("json-schema-validator/2.1.0") |
| 64 | + |
| 65 | + if self.options.get_safe("with_dnssd") == "mdnsresponder": |
| 66 | + self.requires("mdnsresponder/878.200.35") |
| 67 | + # The option mdnsresponder:with_opt_patches=True is recommended in order to permit the |
| 68 | + # over-long service type _nmos-registration._tcp used in IS-04 v1.2, and also to enable |
| 69 | + # support for unicast DNS-SD on Linux, since NMOS recommends this in preference to mDNS. |
| 70 | + # See https://specs.amwa.tv/is-04/releases/v1.3.1/docs/3.1._Discovery_-_Registered_Operation.html#dns-sd-advertisement |
| 71 | + elif self.options.get_safe("with_dnssd") == "avahi": |
| 72 | + self.requires("avahi/0.8") |
| 73 | + |
| 74 | + def build_requirements(self): |
| 75 | + # nmos-cpp needs CMake 3.17 or higher but CCI doesn't allow version ranges |
| 76 | + self.build_requires("cmake/3.21.2") |
| 77 | + |
| 78 | + def validate(self): |
| 79 | + if self.settings.compiler.get_safe("cppstd"): |
| 80 | + tools.check_min_cppstd(self, 11) |
| 81 | + |
| 82 | + def source(self): |
| 83 | + tools.get(**self.conan_data["sources"][self.version], |
| 84 | + destination=self._source_subfolder, strip_root=True) |
| 85 | + |
| 86 | + def _configure_cmake(self): |
| 87 | + if self._cmake: |
| 88 | + return self._cmake |
| 89 | + self._cmake = CMake(self) |
| 90 | + # prefer config-file packages created by cmake_find_package_multi |
| 91 | + # over any system-installed find-module packages |
| 92 | + self._cmake.definitions["CMAKE_FIND_PACKAGE_PREFER_CONFIG"] = True |
| 93 | + # (on Linux) select Avahi or mDNSResponder |
| 94 | + self._cmake.definitions["NMOS_CPP_USE_AVAHI"] = self.options.get_safe("with_dnssd") == "avahi" |
| 95 | + # (on Windows) use the Conan package for DNSSD (mdnsresponder), not the project's own DLL stub library |
| 96 | + self._cmake.definitions["NMOS_CPP_USE_BONJOUR_SDK"] = True |
| 97 | + # no need to build unit tests |
| 98 | + self._cmake.definitions["NMOS_CPP_BUILD_TESTS"] = False |
| 99 | + # the examples (nmos-cpp-registry and nmos-cpp-node) are useful utilities for users |
| 100 | + self._cmake.definitions["NMOS_CPP_BUILD_EXAMPLES"] = True |
| 101 | + # out-of-source build |
| 102 | + self._cmake.configure(build_folder=self._build_subfolder) |
| 103 | + return self._cmake |
| 104 | + |
| 105 | + def build(self): |
| 106 | + cmake = self._configure_cmake() |
| 107 | + cmake.build() |
| 108 | + |
| 109 | + def package(self): |
| 110 | + self.copy("LICENSE", dst="licenses", src=self._source_subfolder) |
| 111 | + cmake = self._configure_cmake() |
| 112 | + cmake.install() |
| 113 | + cmake_folder = os.path.join(self.package_folder, "lib", "cmake") |
| 114 | + self._create_components_file_from_cmake_target_file(os.path.join(cmake_folder, "nmos-cpp", "nmos-cpp-targets.cmake")) |
| 115 | + # remove the project's own generated config-file package |
| 116 | + tools.rmdir(cmake_folder) |
| 117 | + |
| 118 | + # based on abseil recipe |
| 119 | + # see https://github.com/conan-io/conan-center-index/blob/master/recipes/abseil/all/conanfile.py |
| 120 | + def _create_components_file_from_cmake_target_file(self, target_file_path): |
| 121 | + components = {} |
| 122 | + |
| 123 | + target_content = tools.load(target_file_path) |
| 124 | + |
| 125 | + cmake_functions = re.findall(r"(?P<func>add_library|set_target_properties)[\n|\s]*\([\n|\s]*(?P<args>[^)]*)\)", target_content) |
| 126 | + for (cmake_function_name, cmake_function_args) in cmake_functions: |
| 127 | + cmake_function_args = re.split(r"[\s|\n]+", cmake_function_args, maxsplit=2) |
| 128 | + |
| 129 | + cmake_imported_target_name = cmake_function_args[0] |
| 130 | + cmake_target_nonamespace = cmake_imported_target_name.replace("nmos-cpp::", "") |
| 131 | + component_name = cmake_target_nonamespace.lower() |
| 132 | + # Conan component name cannot be the same as the package name |
| 133 | + if component_name == "nmos-cpp": |
| 134 | + component_name = "nmos-cpp-lib" |
| 135 | + |
| 136 | + components.setdefault(component_name, {"cmake_target": cmake_target_nonamespace}) |
| 137 | + |
| 138 | + if cmake_function_name == "add_library": |
| 139 | + cmake_imported_target_type = cmake_function_args[1] |
| 140 | + if cmake_imported_target_type in ["STATIC", "SHARED"]: |
| 141 | + # library filenames are based on the target name by default |
| 142 | + lib_name = cmake_target_nonamespace |
| 143 | + # the filename may be changed by a straightforward command: |
| 144 | + # set_property(TARGET Bonjour PROPERTY OUTPUT_NAME dnssd) |
| 145 | + # but we'd have to read the nmos-cpp-targets-<config>.cmake files |
| 146 | + # and parse the IMPORTED_LOCATION_<CONFIG> values |
| 147 | + if lib_name == "Bonjour": |
| 148 | + lib_name = "dnssd" |
| 149 | + components[component_name]["libs"] = [lib_name] |
| 150 | + elif cmake_function_name == "set_target_properties": |
| 151 | + target_properties = re.findall(r"(?P<property>INTERFACE_[A-Z_]+)[\n|\s]+\"(?P<values>.+)\"", cmake_function_args[2]) |
| 152 | + for target_property in target_properties: |
| 153 | + property_type = target_property[0] |
| 154 | + # '\', '$' and '"' are escaped; '$' especially is important here |
| 155 | + # see https://github.com/conan-io/conan/blob/release/1.39/conans/client/generators/cmake_common.py#L43-L48 |
| 156 | + property_values = re.sub(r"\\(.)", r"\1", target_property[1]).split(";") |
| 157 | + if property_type == "INTERFACE_LINK_LIBRARIES": |
| 158 | + for dependency in property_values: |
| 159 | + match_private = re.fullmatch(r"\$<LINK_ONLY:(.+)>", dependency) |
| 160 | + if match_private: |
| 161 | + dependency = match_private.group(1) |
| 162 | + if "::" in dependency: |
| 163 | + dependency = dependency.replace("nmos-cpp::", "") |
| 164 | + # Conan component name cannot be the same as the package name |
| 165 | + if dependency == "nmos-cpp": |
| 166 | + dependency = "nmos-cpp-lib" |
| 167 | + # Conan packages for Boost, cpprestsdk, websocketpp, OpenSSL and Avahi have component names that (except for being lowercase) match the CMake targets |
| 168 | + # json-schema-validator overrides cmake_find_package[_multi] names |
| 169 | + elif dependency == "nlohmann_json_schema_validator::nlohmann_json_schema_validator": |
| 170 | + dependency = "json-schema-validator::json-schema-validator" |
| 171 | + # mdnsresponder overrides cmake_find_package[_multi] names |
| 172 | + elif dependency == "DNSSD::DNSSD": |
| 173 | + dependency = "mdnsresponder::mdnsresponder" |
| 174 | + components[component_name].setdefault("requires" if not match_private else "requires_private", []).append(dependency.lower()) |
| 175 | + elif "${_IMPORT_PREFIX}/lib/" in dependency: |
| 176 | + self.output.warn("{} recipe does not handle {} {} (yet)".format(self.name, property_type, dependency)) |
| 177 | + else: |
| 178 | + components[component_name].setdefault("system_libs", []).append(dependency) |
| 179 | + elif property_type == "INTERFACE_COMPILE_DEFINITIONS": |
| 180 | + for property_value in property_values: |
| 181 | + components[component_name].setdefault("defines", []).append(property_value) |
| 182 | + elif property_type == "INTERFACE_COMPILE_FEATURES": |
| 183 | + for property_value in property_values: |
| 184 | + if property_value not in ["cxx_std_11"]: |
| 185 | + self.output.warn("{} recipe does not handle {} {} (yet)".format(self.name, property_type, property_value)) |
| 186 | + elif property_type == "INTERFACE_COMPILE_OPTIONS": |
| 187 | + for property_value in property_values: |
| 188 | + # handle forced include (Visual Studio /FI, gcc -include) by relying on includedirs containing "include" |
| 189 | + property_value = property_value.replace("${_IMPORT_PREFIX}/include/", "") |
| 190 | + components[component_name].setdefault("cxxflags", []).append(property_value) |
| 191 | + elif property_type == "INTERFACE_INCLUDE_DIRECTORIES": |
| 192 | + for property_value in property_values: |
| 193 | + if property_value not in ["${_IMPORT_PREFIX}/include"]: |
| 194 | + self.output.warn("{} recipe does not handle {} {} (yet)".format(self.name, property_type, property_value)) |
| 195 | + elif property_type == "INTERFACE_LINK_OPTIONS": |
| 196 | + for property_value in property_values: |
| 197 | + # workaround required because otherwise "/ignore:4099" gets converted to "\ignore:4099.obj" |
| 198 | + # thankfully the MSVC linker accepts both '/' and '-' for the option specifier and Visual Studio |
| 199 | + # handles link options appearing in Link/AdditionalDependencies rather than Link/AdditionalOptions |
| 200 | + # because the CMake generators put them in INTERFACE_LINK_LIBRARIES rather than INTERFACE_LINK_OPTIONS |
| 201 | + # see https://github.com/conan-io/conan/pull/8812 |
| 202 | + # and https://docs.microsoft.com/en-us/cpp/build/reference/linking?view=msvc-160#command-line |
| 203 | + property_value = re.sub(r"^/", r"-", property_value) |
| 204 | + components[component_name].setdefault("linkflags", []).append(property_value) |
| 205 | + else: |
| 206 | + self.output.warn("{} recipe does not handle {} (yet)".format(self.name, property_type)) |
| 207 | + |
| 208 | + # Save components informations in json file |
| 209 | + with open(self._components_helper_filepath, "w") as json_file: |
| 210 | + json.dump(components, json_file, indent=4) |
| 211 | + |
| 212 | + @property |
| 213 | + def _components_helper_filepath(self): |
| 214 | + return os.path.join(self.package_folder, "lib", "components.json") |
| 215 | + |
| 216 | + def package_info(self): |
| 217 | + bindir = "bin" |
| 218 | + libdir = "lib" |
| 219 | + # on Windows, cmake_install() puts the binaries in a config-specific sub-folder |
| 220 | + if self.settings.os == "Windows": |
| 221 | + config_install_dir = "Debug" if self.settings.build_type == "Debug" else "Release" |
| 222 | + bindir = os.path.join(bindir, config_install_dir) |
| 223 | + libdir = os.path.join(libdir, config_install_dir) |
| 224 | + |
| 225 | + def _register_components(): |
| 226 | + components_json_file = tools.load(self._components_helper_filepath) |
| 227 | + components = json.loads(components_json_file) |
| 228 | + for component_name, values in components.items(): |
| 229 | + cmake_target = values["cmake_target"] |
| 230 | + self.cpp_info.components[component_name].names["cmake_find_package"] = cmake_target |
| 231 | + self.cpp_info.components[component_name].names["cmake_find_package_multi"] = cmake_target |
| 232 | + self.cpp_info.components[component_name].libs = values.get("libs", []) |
| 233 | + self.cpp_info.components[component_name].libdirs = [libdir] |
| 234 | + self.cpp_info.components[component_name].defines = values.get("defines", []) |
| 235 | + self.cpp_info.components[component_name].cxxflags = values.get("cxxflags", []) |
| 236 | + linkflags = values.get("linkflags", []) |
| 237 | + self.cpp_info.components[component_name].sharedlinkflags = linkflags |
| 238 | + self.cpp_info.components[component_name].exelinkflags = linkflags |
| 239 | + self.cpp_info.components[component_name].system_libs = values.get("system_libs", []) |
| 240 | + self.cpp_info.components[component_name].frameworks = values.get("frameworks", []) |
| 241 | + self.cpp_info.components[component_name].requires = values.get("requires", []) |
| 242 | + # hmm, how should private requirements be indicated? this results in a string format error... |
| 243 | + #self.cpp_info.components[component_name].requires.extend([(r, "private") for r in values.get("requires_private", [])]) |
| 244 | + self.cpp_info.components[component_name].requires.extend(values.get("requires_private", [])) |
| 245 | + _register_components() |
| 246 | + |
| 247 | + # add nmos-cpp-registry and nmos-cpp-node to the path |
| 248 | + bin_path = os.path.join(self.package_folder, bindir) |
| 249 | + self.output.info("Appending PATH environment variable: {}".format(bin_path)) |
| 250 | + self.env_info.PATH.append(bin_path) |
0 commit comments