diff --git a/conan/internal/model/pkg_type.py b/conan/internal/model/pkg_type.py index d850d03d4ba..06d9f50d20f 100644 --- a/conan/internal/model/pkg_type.py +++ b/conan/internal/model/pkg_type.py @@ -13,6 +13,7 @@ class PackageType(Enum): PYTHON = "python-require" CONF = "configuration" UNKNOWN = "unknown" + MODULE = "module" def __str__(self): return self.value diff --git a/conan/internal/model/requires.py b/conan/internal/model/requires.py index 2c3d034514c..d1a6e5f3986 100644 --- a/conan/internal/model/requires.py +++ b/conan/internal/model/requires.py @@ -210,6 +210,11 @@ def set_if_none(field, value): set_if_none("_libs", False) set_if_none("_headers", False) set_if_none("_visible", False) # Conflicts might be allowed for this kind of package + elif pkg_type is PackageType.MODULE: + set_if_none("_run", True) + set_if_none("_libs", False) + set_if_none("_headers", False) + set_if_none("_visible", False) src_pkg_type = src_node.conanfile.package_type if src_pkg_type is PackageType.HEADER: @@ -294,7 +299,7 @@ def transform_downstream(self, pkg_type, require, dep_pkg_type): # Regular and test requires if dep_pkg_type is PackageType.SHARED or dep_pkg_type is PackageType.STATIC: - if pkg_type is PackageType.SHARED: + if pkg_type is PackageType.SHARED or pkg_type is PackageType.MODULE: downstream_require = Requirement(require.ref, headers=False, libs=False, run=require.run) elif pkg_type is PackageType.STATIC: downstream_require = Requirement(require.ref, headers=False, libs=require.libs, run=require.run) diff --git a/conan/tools/cmake/cmakedeps2/target_configuration.py b/conan/tools/cmake/cmakedeps2/target_configuration.py index 0cb24590de9..381688b8820 100644 --- a/conan/tools/cmake/cmakedeps2/target_configuration.py +++ b/conan/tools/cmake/cmakedeps2/target_configuration.py @@ -187,6 +187,10 @@ def _get_cmake_lib(self, info, components, pkg_folder, pkg_folder_var): link_languages = ["CXX" if c == "C++" else c for c in link_languages] target["link_languages"] = link_languages + if self._conanfile.package_type == PackageType.MODULE: + target = {"type": "module"} + + return target def _add_root_lib_target(self, libs, pkg_name, cpp_info): diff --git a/test/functional/module/__init__.py b/test/functional/module/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/functional/module/test_cmake_module.py b/test/functional/module/test_cmake_module.py new file mode 100644 index 00000000000..55cd89a66f5 --- /dev/null +++ b/test/functional/module/test_cmake_module.py @@ -0,0 +1,268 @@ +import pytest +import textwrap + +from conan.test.assets.genconanfile import GenConanfile +from conan.test.utils.tools import TestClient +from conan.test.assets.sources import gen_function_cpp, gen_function_h + + +def test_module_project(): + # app ---> module1 ---> mylib + + client = TestClient() + + # -- Generate MyLib + cmakelists = textwrap.dedent(""" + cmake_minimum_required(VERSION 3.15) + project(MyLib CXX) + + add_library(MyLib mylib.h mylib.cpp) + install(FILES mylib.h DESTINATION include) + install(TARGETS MyLib DESTINATION "." + RUNTIME DESTINATION bin + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib + ) + """) + + conanfile = textwrap.dedent(""" + from conan import ConanFile + from conan.tools.cmake import CMakeToolchain, CMake, cmake_layout + + + class pkgRecipe(ConanFile): + name = "mylib" + package_type = "library" + + settings = "os", "compiler", "build_type", "arch" + options = { + "shared": [True, False], + "fPIC": [True, False], + } + default_options = { + "shared": True, + "fPIC": True, + } + implements = ["auto_shared_fpic"] + + exports_sources = "CMakeLists.txt", "mylib.h", "mylib.cpp" + + def layout(self): + cmake_layout(self) + + def generate(self): + tc = CMakeToolchain(self) + tc.generate() + + 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.libs = ["MyLib"] + """) + + client.save({"mylib/conanfile.py": conanfile, + "mylib/mylib.h": gen_function_h(name="mylib"), + "mylib/mylib.cpp": gen_function_cpp(name="mylib"), + "mylib/CMakeLists.txt": cmakelists}, clean_first=True) + client.run("create mylib --version=0.1 -o='*/*:shared=True'") + + # -- Generate MyModule + cmakelists = textwrap.dedent(""" + cmake_minimum_required(VERSION 3.15) + project(MyModule CXX) + + find_package(mylib REQUIRED) + + add_library(MyModule MODULE main.cpp) + target_link_libraries(MyModule mylib::mylib) + + install(TARGETS MyModule DESTINATION "." + RUNTIME DESTINATION bin + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib + ) + """) + + conanfile = textwrap.dedent(""" + from conan import ConanFile + from conan.tools.cmake import CMakeToolchain, CMake, cmake_layout, CMakeDeps + + + class pkgRecipe(ConanFile): + name = "mymodule" + package_type = "module" + + settings = "os", "compiler", "build_type", "arch" + options = { + "shared": [True, False], + "fPIC": [True, False], + } + default_options = { + "shared": False, + "fPIC": True, + } + implements = ["auto_shared_fpic"] + + exports_sources = "CMakeLists.txt", "main.cpp" + + def layout(self): + cmake_layout(self) + + def generate(self): + deps = CMakeDeps(self) + deps.generate() + tc = CMakeToolchain(self) + tc.generate() + + def requirements(self): + self.requires("mylib/0.1") + + 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.bindirs.append("lib") + """) + cpp = textwrap.dedent(""" + + #include + #include "mylib.h" + + #ifdef _WIN32 + #define DLL_EXPORT __declspec(dllexport) + #else + #define DLL_EXPORT + #endif + + extern "C" DLL_EXPORT int moduleFunction(){ + mylib(); + return 0; + } + """) + + client.save({"mymodule/conanfile.py": conanfile, + "mymodule/main.cpp": cpp, + "mymodule/CMakeLists.txt": cmakelists}, clean_first=True) + client.run("create mymodule --version=0.1 -v -c tools.cmake.cmakedeps:new='will_break_next'") + + + # -- Generate Main app + cmake = textwrap.dedent(""" + cmake_minimum_required(VERSION 3.15) + project(MainApp CXX) + + add_executable(MainApp main.cpp) + install(TARGETS MainApp DESTINATION "." + RUNTIME DESTINATION bin + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib + ) + """) + app_cpp = textwrap.dedent(""" + #include + #ifdef _WIN32 + #include + #else + #include + #endif + + int main() { + std::cout << "Hello from App" << std::endl; + + + #ifdef _WIN32 + + typedef int (__cdecl *MYPROC)(); + HINSTANCE hinstLib = LoadLibrary(TEXT("MyModule.dll")); + if (!hinstLib){ + std::cerr << "Error loading plugin: " << __FILE__ << __LINE__ << std::endl; + return 1; + } + MYPROC ProcAdd = (MYPROC) GetProcAddress(hinstLib, "moduleFunction"); + if (!ProcAdd) { + std::cerr << "Error finding function 'moduleFunction': " << __FILE__ << __LINE__ << std::endl; + FreeLibrary(hinstLib); + return 1; + } + (ProcAdd)(); + FreeLibrary(hinstLib); + + #else + void* handle = dlopen("libMyModule.so", RTLD_LAZY); + if (!handle) { + std::cerr << "Error loading plugin: " << dlerror() << std::endl; + return 1; + } + + typedef int (*moduleFunction)(void); + moduleFunction customFunction; + *(void**)(&customFunction) = dlsym(handle, "moduleFunction"); + + if (!customFunction) { + std::cerr << "Error finding function 'moduleFunction': " << dlerror() << std::endl; + dlclose(handle); + return 1; + } + + customFunction(); + dlclose(handle); + #endif + + return 0; + } + """) + conanfile = textwrap.dedent(""" + from conan import ConanFile + from conan.tools.cmake import CMakeToolchain, CMake, cmake_layout, CMakeDeps + import os + + + class pkgRecipe(ConanFile): + name = "mainapp" + package_type = "application" + + settings = "os", "compiler", "build_type", "arch" + + exports_sources = "CMakeLists.txt", "main.cpp" + + def layout(self): + cmake_layout(self) + + def generate(self): + deps = CMakeDeps(self) + deps.generate() + tc = CMakeToolchain(self) + tc.generate() + + def requirements(self): + self.requires("mymodule/0.1") + + def build(self): + cmake = CMake(self) + cmake.configure() + cmake.build() + if self.settings.os == "Windows": + my_path = self.cpp.build.bindir + self.run(os.path.join(my_path, "MainApp"), env="conanrun") + else: + self.run(f"'{os.path.join(self.build_folder, 'MainApp')}'", env="conanrun") + """) + client.save( + {"mainapp/conanfile.py": conanfile, + "mainapp/main.cpp": app_cpp, + "mainapp/CMakeLists.txt": cmake}, clean_first=True) + client.run("create mainapp --version=0.1 -c tools.cmake.cmakedeps:new='will_break_next'") + assert "mylib: Release!" in client.out diff --git a/test/functional/toolchains/cmake/test_cmakedeps2_modules.py b/test/functional/toolchains/cmake/test_cmakedeps2_modules.py new file mode 100644 index 00000000000..5684205a68b --- /dev/null +++ b/test/functional/toolchains/cmake/test_cmakedeps2_modules.py @@ -0,0 +1,38 @@ +from conan.test.utils.tools import TestClient, GenConanfile + + +def test_create_consume_module(): + + # main -> mod -> dep/1.0 + # -> dep/2.0 + tc = TestClient() + tc.save({"conanfile.py": GenConanfile("dep", "1.0").with_generator("CMakeDeps").with_settings("build_type").with_package_type("shared-library")}) + tc.run('create -c tools.cmake.cmakedeps:new="will_break_next" .') + tc.save({"conanfile.py": GenConanfile("dep", "2.0").with_generator("CMakeDeps").with_settings("build_type").with_package_type("shared-library")}) + tc.run('create -c tools.cmake.cmakedeps:new="will_break_next" .') + tc.save({"conanfile.py": GenConanfile("mod", "1.0").with_requirement("dep/1.0").with_package_type("module").with_generator("CMakeDeps").with_settings("build_type")}) + tc.run('create -c tools.cmake.cmakedeps:new="will_break_next" .') + conanfile = GenConanfile("main", "1.0").with_requirement("dep/2.0").with_requirement("mod/1.0").with_generator("CMakeDeps").with_settings("build_type") + tc.save({"conanfile.py": conanfile}) + tc.run('create -c tools.cmake.cmakedeps:new="will_break_next" .', assert_error=True) #Not valid + +def test_help(): + a = """ +from conan import ConanFile +class MyModule(ConanFile): + name = 'mod' + version = '1.0' + package_type = 'module' + generators = "CMakeDeps" + + def requirements(self): + self.requires("dep/1.0", visible=False) + settings = "build_type" + """ + tc = TestClient() + tc.save({"conanfile.py": GenConanfile("dep", "1.0").with_generator("CMakeDeps").with_settings("build_type")}) + tc.run('create -c tools.cmake.cmakedeps:new="will_break_next" .') + tc.save({"conanfile.py": a}) + tc.run('create -c tools.cmake.cmakedeps:new="will_break_next" .') + assert True +