conan-io / conan

Conan - The open-source C and C++ package manager
https://conan.io
MIT License
7.96k stars 952 forks source link

[question] When testing a package from a test_package folder how can I ensure that the macro PKG_API_STATIC_DEFINE is defined in the test project when --option:all=pkg/*:shared=False is used? #16588

Closed marvin-the-mathematician closed 1 day ago

marvin-the-mathematician commented 2 days ago

What is your question?

For a Conan library package defined by the conanfile.py something like:

"""Module containing a library package."""

from conan.tools.build import check_min_cppstd
from conan.tools.cmake import CMake, CMakeDeps, CMakeToolchain, cmake_layout
from conan.tools.scm import Git

from conan import ConanFile

class PackageRecipe(ConanFile):
    """Conan recipe for Five SDL ScenarioQueryEngine package."""

    name = "pkg"

    # Optional metadata...
    author = "Marvin Jones marvin.a.jones@mac.com"
    description = "A library package."
    package_type = "library"

    # Build configuration...
    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",
        "Copyright.txt",
        "README.md",
        "cmake/*",
        "include/*",
        "src/*",
        "test/*",
    )
    revision_mode = "scm"

    def set_version(self):
        """Set package version."""
        git = Git(self, self.recipe_folder)
        self.version = git.run("describe --tags")

    def config_options(self):
        """Configure package options."""
        if self.settings.os == "Windows":
            del self.options.fPIC

    def validate(self):
        """Check package constraints."""
        check_min_cppstd(self, "20")

    def requirements(self):
        """Declare package requirements."""
        self.requires("protobuf/3.21.12")

    def build_requirements(self):
        """Declare tool and test package requirements."""
        self.tool_requires("protobuf/<host_version>")
        self.test_requires("gtest/1.14.0")

    def layout(self):
        """Define package layout."""
        cmake_layout(self)

    def generate(self):
        """Prepare package for configuration."""
        deps = CMakeDeps(self)
        deps.generate()
        toolchain = CMakeToolchain(self)
        toolchain.generate()

    def build(self):
        """Configure, build and test package."""
        cmake = CMake(self)
        cmake.configure()
        cmake.build()

    def package(self):
        """Make the package."""
        cmake = CMake(self)
        cmake.install()

    def package_info(self):
        """Add package metadata."""
        self.cpp_info.libs = ["pkg"]

that includes an export header, say api.h, containing a PKG_API_STATIC_DEFINE preprocessor macro in something like the following way:

// Copyright Company 2024. All rights reserved.
#pragma once

#if defined(macintosh) || defined(__APPLE__) || defined(__APPLE_CC__)
#define PKG_OS_MACOS_AVAILABLE
#elif defined(_WIN32) || defined(_WIN64) || defined(__WIN32__) || defined(__TOS_WIN__) ||          \
    defined(__WINDOWS__)
#define PKG_OS_WINDOWS_AVAILABLE
#elif defined(__linux__)
#define PKG_OS_LINUX_AVAILABLE
#endif

#ifdef PKG_API_STATIC_DEFINE
#define PKG_API_EXPORT
#else
#ifndef PKG_API_EXPORT
#ifdef pkg_EXPORTS
/* We are building this library */
#if defined(PKG_OS_MACOS_AVAILABLE) || defined(PKG_OS_LINUX_AVAILABLE)
#define PKG_API_EXPORT __attribute__((visibility("default")))
#elif defined(PKG_OS_WINDOWS_AVAILABLE)
#define PKG_API_EXPORT __declspec(dllexport)
#else
#define PKG_API_EXPORT
#endif
#else
/* We are using this library */
#if defined(PKG_OS_MACOS_AVAILABLE) || defined(PKG_OS_LINUX_AVAILABLE)
#define PKG_API_EXPORT __attribute__((visibility("default")))
#elif defined(PKG_OS_WINDOWS_AVAILABLE)
#define PKG_API_EXPORT __declspec(dllimport)
#else
#define PKG_API_EXPORT
#endif
#endif
#endif
#endif
...

and a conanfile.py in the test_package folder as follows:

"""Module containing Conan recipe for package tests."""

import os

from conan.tools.build import can_run
from conan.tools.cmake import CMake, CMakeDeps, CMakeToolchain, cmake_layout

from conan import ConanFile

class TestPackageRecipe(ConanFile):
    """Conan recipe for package tests."""

    name = "test_package"
    author = "Marvin Jones marvin.jones@five.ai"
    description = "Test the packaged Five Scenario Query Engine library."
    package_type = "application"
    settings = "os", "compiler", "build_type", "arch"

    def requirements(self):
        """Declare package requirements."""
        self.requires(self.tested_reference_str)
        self.requires("protobuf/3.21.12")

    def build_requirements(self):
        """Declare tool and test package requirements."""
        self.test_requires("gtest/1.14.0")

    def layout(self):
        """Define package layout."""
        cmake_layout(self)

    def generate(self):
        """Prepare package for configuration."""
        deps = CMakeDeps(self)
        deps.generate()
        toolchain = CMakeToolchain(self)
        toolchain.generate()

    def build(self):
        """Configure, build and test package."""
        cmake = CMake(self)
        cmake.configure()
        cmake.build()

    def test(self):
        """Run package tests."""
        if can_run(self):
            cmd = os.path.join(
                self.cpp.build.bindir, "test_package"
            )
            self.run(cmd, env="conanrun")

How should I set the package_info of the library package to ensure that in the test_package project the PKG_API_STATIC_DEFINE macro is defined when --options:all='pkg/*:shared=False' is set and not defined when --options:all='pkg/*:shared=True' is set?

For example, I am using the following conan invocations to configure, build, test and package the project:

conan install ./ \
  --options:all='protobuf/*:shared=False' \
  --options:all='pkg/*:shared=False' \
  --output-folder ./
cmake --preset conan-release
cmake --build --preset conan-release

cd ./build/Release
source ./generators/conanrun.sh
ctest --label-regex "^.*unit_cpu.*$" --verbose
source ./generators/deactivate_conanrun.sh
cd ../../

conan export-pkg ./ \
  --profile:all=default \
  --options:all='protobuf/*:shared=False' \
  --options:all='pkg/*:shared=False' \
  --output-folder ./

The final conan invocation will fail most notably on the Windows operating system with linker errors (undefined reference to symbol,etc.) because the wrong attribute ends up being used to decorate the public interfaces of the package. This is due to the fact that the PKG_API_STATIC_DEFINE macro is not defined (it should be) and so the PKG_API_EXPORT macro ends up being defined as __declspec(dllimport) when it should not be (when using static linkage to library package).

I thought that using something like:

   ...
   def package_info(self):
        """Add package metadata."""
        self.cpp_info.libs = ["pkg"]
        if self.options.shared:
            self.buildenv_info.define("PKG_API_STATIC_DEFINE", "1")

(in the conanfile for the library package) would work (having read the docs here (https://docs.conan.io/2/tutorial/creating_packages/define_package_information.html#propagating-environment-or-configuration-information-to-consumers) - I am using Conan 2.4.1 and Python 3.12.2). But it doesn't seem to. Could you indicate what the correct approach would be please?

Have you read the CONTRIBUTING guide?

memsharded commented 2 days ago

Hi @marvin-the-mathematician

Thanks for your question. The package_info contains the cpp_info to model different aspects of the C/C++ build of the consumer: https://docs.conan.io/2/reference/conanfile/methods/package_info.html#cpp-info-library-and-build-information

The one that you want is defines, so something like:

def package_info(self):
        """Add package metadata."""
        self.cpp_info.libs = ["pkg"]
        if not self.options.shared:
            self.cpp_info.defines = ["PKG_API_STATIC_DEFINE=1"]

Should work. Please not that I have reversed the condition to if not self.options.shared it seems you were using the opposite logic based on your description of the preprocessor definition above.

Please check that and let us know.

marvin-the-mathematician commented 2 days ago

Thanks for the prompt answer (as always); much appreciated. Indeed, I did copy over the code incorrectly (apologies); luckily I have the correct logic in the real code ;-) I'll give your suggestion a go and let you know how that works out.

I have also just realised that I almost certainly need to map the propagated PKG_API_STATIC_DEFINE environment variable from the environment into a CMake compile definition. So I'll try doing that too. Or perhaps using the self.cpp_info.defines field solves that problem for me.

marvin-the-mathematician commented 2 days ago

Yep. That worked. Thank you so much! You have saved me another day of trying to figure that one out for myself :-) I now have my shared and static library packaging working across Windows 10, macOS Sonoma, Ubuntu Jammy and manylinux2014 in CI with minimal changes needed between the different pipeline scripts for each platform! Perfect.

As a follow up question, if I do currently export a pkgConfig.cmake file, etc. from within the CMakeLists.txt file for a library package will it be ignored by consumers of that package if those consumers are using Conan to manage their dependencies? In other words, if I commit to using Conan throughout the dependency graph can I stop exporting pkgConfig.cmake files, etc.?

memsharded commented 2 days ago

Yep. That worked. Thank you so much! You have saved me another day of trying to figure that one out for myself :-) I now have my shared and static library packaging working across Windows 10, macOS Sonoma, Ubuntu Jammy and manylinux2014 in CI with minimal changes needed between the different pipeline scripts for each platform! Perfect.

Very happy to help :) Don't hesitate to create new tickets as necessary for any further question, if that helps.

Glad to know that Conan is helping with maintaining those different binaries too, thanks for the feedback.

As a follow up question, if I do currently export a pkgConfig.cmake file, etc. from within the CMakeLists.txt file for a library package will it be ignored by consumers of that package if those consumers are using Conan to manage their dependencies? In other words, if I commit to using Conan throughout the dependency graph can I stop exporting pkgConfig.cmake files, etc.?

You can do both. By default, the generated files from CMakeDeps will have higher priority and will be used to locate and use dependencies. But it is also possible to use the packaged xxx-config.cmake files if that makes sense, check https://docs.conan.io/2/examples/tools/cmake/cmake_toolchain/use_package_config_cmake.html (it might have some limitations, check them in the docs page). You can control if CMakeDeps generate files for dependencies or not with the:

def generate(self):
    deps = CMakeDeps(self)
    deps.set_property("mydep", "cmake_find_mode", "none")
    deps.generate()

In general, I find the effort of create a working xxx-config.cmake file higher, specially when the package has dependencies, so I indeed prefer to leverage the Conan generated files with CMakeDeps, but I understand there might be some exceptional cases, specially when a package is very complicated, with tons of components and lots of CMake logic like CMake macros that the consumer needs to execute, where using the in-package xxx-config.cmake might be a bit more convenient. I suggest dropping them, use the generated ones with CMakeDeps, and if there are some complicated situation, re-evaluate that single one.