Skip to content

Commit

Permalink
[feature] Manage shared, fPIC and header_only options automatically (#…
Browse files Browse the repository at this point in the history
…14194)

* [feature] Manage shared, fPIC and header_options automatically

* get os safe

* rename to _conan

* manage exception

* fix cmake test

* fix win tests

* Add package_id method

* fix import

* stupid fix

* add unit test

* add pid to tests

* move out of conanfile

* add package type

* add comment

* review

* remove keyerror to show failing tests

* Update conan/tools/options/helpers.py

Co-authored-by: James <memsharded@gmail.com>

* review

* Update conans/test/utils/mocks.py

Co-authored-by: Rubén Rincón Blanco <git@rinconblanco.es>

* change behavior

* fix test

* review after discussion

---------

Co-authored-by: James <memsharded@gmail.com>
Co-authored-by: Rubén Rincón Blanco <git@rinconblanco.es>
  • Loading branch information
3 people authored Jul 11, 2023
1 parent 936218b commit 2af05b5
Show file tree
Hide file tree
Showing 8 changed files with 219 additions and 9 deletions.
5 changes: 5 additions & 0 deletions conan/tools/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
from conans.model.build_info import CppInfo as _CppInfo
from conan.tools.helpers import (
default_config_options,
default_configure,
default_package_id
)


def CppInfo(conanfile):
Expand Down
19 changes: 19 additions & 0 deletions conan/tools/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from conans.model.pkg_type import PackageType


def default_config_options(conanfile):
if conanfile.settings.get_safe("os") == "Windows":
conanfile.options.rm_safe("fPIC")


def default_configure(conanfile):
if conanfile.options.get_safe("header_only"):
conanfile.options.rm_safe("fPIC")
conanfile.options.rm_safe("shared")
elif conanfile.options.get_safe("shared"):
conanfile.options.rm_safe("fPIC")


def default_package_id(conanfile):
if conanfile.options.get_safe("header_only") or conanfile.package_type is PackageType.HEADER:
conanfile.info.clear()
5 changes: 5 additions & 0 deletions conans/client/conanfile/configure.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from conans.errors import conanfile_exception_formatter
from conans.model.pkg_type import PackageType
from conans.model.requires import BuildRequirements, TestRequirements, ToolRequirements
from conan.tools import default_config_options, default_configure


def run_configure_method(conanfile, down_options, profile_options, ref):
Expand All @@ -9,6 +10,8 @@ def run_configure_method(conanfile, down_options, profile_options, ref):
if hasattr(conanfile, "config_options"):
with conanfile_exception_formatter(conanfile, "config_options"):
conanfile.config_options()
else:
default_config_options(conanfile)

# Assign only the current package options values, but none of the dependencies
is_consumer = conanfile._conan_is_consumer
Expand All @@ -17,6 +20,8 @@ def run_configure_method(conanfile, down_options, profile_options, ref):
if hasattr(conanfile, "configure"):
with conanfile_exception_formatter(conanfile, "configure"):
conanfile.configure()
else:
default_configure(conanfile)

self_options, up_options = conanfile.options.get_upstream_options(down_options, ref,
is_consumer)
Expand Down
4 changes: 3 additions & 1 deletion conans/client/graph/compute_pid.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from conans.errors import conanfile_exception_formatter, ConanInvalidConfiguration, \
conanfile_remove_attr, ConanException
from conans.model.info import ConanInfo, RequirementsInfo, RequirementInfo, PythonRequiresInfo
from conan.tools import default_package_id


def compute_package_id(node, new_config):
Expand Down Expand Up @@ -81,5 +82,6 @@ def run_validate_package_id(conanfile):
with conanfile_exception_formatter(conanfile, "package_id"):
with conanfile_remove_attr(conanfile, ['cpp_info', 'settings', 'options'], "package_id"):
conanfile.package_id()

else:
default_package_id(conanfile)
conanfile.info.validate()
5 changes: 2 additions & 3 deletions conans/test/functional/toolchains/cmake/test_cmake.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,6 @@ def test_toolchain_win(self, compiler, build_type, runtime, version, cppstd, arc
options = {"shared": shared}
save(self.client.cache.new_config_path, "tools.build:jobs=1")
install_out = self._run_build(settings, options)
self.assertIn("WARN: Toolchain: Ignoring fPIC option defined for Windows", install_out)

# FIXME: Hardcoded VS version and partial toolset check
toolchain_path = os.path.join(self.client.current_folder, "build",
Expand Down Expand Up @@ -314,7 +313,6 @@ def test_toolchain_mingw_win(self, build_type, libcxx, version, cppstd, arch, sh
}
options = {"shared": shared}
install_out = self._run_build(settings, options)
self.assertIn("WARN: Toolchain: Ignoring fPIC option defined for Windows", install_out)
self.assertIn("The C compiler identification is GNU", self.client.out)
toolchain_path = os.path.join(self.client.current_folder, "build",
"conan_toolchain.cmake").replace("\\", "/")
Expand Down Expand Up @@ -396,7 +394,8 @@ def test_toolchain_linux(self, build_type, cppstd, arch, libcxx, shared):
"CMAKE_SHARED_LINKER_FLAGS": arch_str,
"CMAKE_EXE_LINKER_FLAGS": arch_str,
"COMPILE_DEFINITIONS": defines,
"CMAKE_POSITION_INDEPENDENT_CODE": "ON"
# fPIC is managed automatically depending on the shared option value
"CMAKE_POSITION_INDEPENDENT_CODE": "ON" if not shared else ""
}

def _verify_out(marker=">>"):
Expand Down
169 changes: 169 additions & 0 deletions conans/test/integration/options/test_configure_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import textwrap
import unittest

from parameterized import parameterized

from conans.test.utils.tools import TestClient


class ConfigureOptionsTest(unittest.TestCase):
"""
Test config_options(), configure() and package_id() methods can manage shared, fPIC and
header_only options automatically.
"""

@parameterized.expand([
["Linux", False, False, False, [False, False, False]],
["Windows", False, False, False, [False, None, False]],
["Windows", True, False, False, [True, None, False]],
["Windows", False, False, True, [None, None, True]],
["Linux", False, False, True, [None, None, True]],
["Linux", True, True, False, [True, None, False]],
["Linux", True, False, False, [True, None, False]],
["Linux", True, True, True, [None, None, True]],
["Linux", True, True, True, [None, None, True]],
["Linux", False, True, False, [False, True, False]],
["Linux", False, True, False, [False, True, False]],
])
def test_methods_not_defined(self, settings_os, shared, fpic, header_only, result):
"""
Test that options are managed automatically when methods config_otpions and configure are not
defined.
Check that header only package gets its unique package ID.
"""
client = TestClient()
conanfile = textwrap.dedent(f"""\
from conan import ConanFile
class Pkg(ConanFile):
settings = "os", "compiler", "arch", "build_type"
options = {{"shared": [True, False], "fPIC": [True, False], "header_only": [True, False]}}
default_options = {{"shared": {shared}, "fPIC": {fpic}, "header_only": {header_only}}}
def build(self):
shared = self.options.get_safe("shared")
fpic = self.options.get_safe("fPIC")
header_only = self.options.get_safe("header_only")
self.output.info(f"shared: {{shared}}, fPIC: {{fpic}}, header only: {{header_only}}")
""")
client.save({"conanfile.py": conanfile})
client.run(f"create . --name=pkg --version=0.1 -s os={settings_os}")
result = f"shared: {result[0]}, fPIC: {result[1]}, header only: {result[2]}"
self.assertIn(result, client.out)
if header_only:
self.assertIn("Package 'da39a3ee5e6b4b0d3255bfef95601890afd80709' created", client.out)

@parameterized.expand([
["Linux", False, False, False, [False, False, False]],
["Linux", False, False, True, [False, False, True]],
["Linux", False, True, False, [False, True, False]],
["Linux", False, True, True, [False, True, True]],
["Linux", True, False, False, [True, False, False]],
["Linux", True, False, True, [True, False, True]],
["Linux", True, True, False, [True, True, False]],
["Linux", True, True, True, [True, True, True]],
["Windows", False, False, False, [False, False, False]],
["Windows", False, False, True, [False, False, True]],
["Windows", False, True, False, [False, True, False]],
["Windows", False, True, True, [False, True, True]],
["Windows", True, False, False, [True, False, False]],
["Windows", True, False, True, [True, False, True]],
["Windows", True, True, False, [True, True, False]],
["Windows", True, True, True, [True, True, True]],
])
def test_optout(self, settings_os, shared, fpic, header_only, result):
"""
Test that options are not managed automatically when methods are defined.
Check that header only package gets its unique package ID.
"""
client = TestClient()
conanfile = textwrap.dedent(f"""\
from conan import ConanFile
class Pkg(ConanFile):
settings = "os", "compiler", "arch", "build_type"
options = {{"shared": [True, False], "fPIC": [True, False], "header_only": [True, False]}}
default_options = {{"shared": {shared}, "fPIC": {fpic}, "header_only": {header_only}}}
def config_options(self):
pass
def configure(self):
pass
def build(self):
shared = self.options.get_safe("shared")
fpic = self.options.get_safe("fPIC")
header_only = self.options.get_safe("header_only")
self.output.info(f"shared: {{shared}}, fPIC: {{fpic}}, header only: {{header_only}}")
""")
client.save({"conanfile.py": conanfile})
client.run(f"create . --name=pkg --version=0.1 -s os={settings_os}")
result = f"shared: {result[0]}, fPIC: {result[1]}, header only: {result[2]}"
self.assertIn(result, client.out)
if header_only:
self.assertIn("Package 'da39a3ee5e6b4b0d3255bfef95601890afd80709' created", client.out)

@parameterized.expand([
["Linux", False, False, False, [False, False, False]],
["Windows", False, False, False, [False, None, False]],
["Windows", True, False, False, [True, None, False]],
["Windows", False, False, True, [None, None, True]],
["Linux", False, False, True, [None, None, True]],
["Linux", True, True, False, [True, None, False]],
["Linux", True, False, False, [True, None, False]],
["Linux", True, True, True, [None, None, True]],
["Linux", True, True, True, [None, None, True]],
["Linux", False, True, False, [False, True, False]],
["Linux", False, True, False, [False, True, False]],
])
def test_methods_defined_explicit(self, settings_os, shared, fpic, header_only, result):
"""
Test that options are managed when the tool is used in methods defined by user.
Check that header only package gets its unique package ID.
"""
client = TestClient()
conanfile = textwrap.dedent(f"""\
from conan import ConanFile
from conan.tools import default_config_options, default_configure
class Pkg(ConanFile):
settings = "os", "compiler", "arch", "build_type"
options = {{"shared": [True, False], "fPIC": [True, False], "header_only": [True, False]}}
default_options = {{"shared": {shared}, "fPIC": {fpic}, "header_only": {header_only}}}
def config_options(self):
default_config_options(self)
def configure(self):
default_configure(self)
def build(self):
shared = self.options.get_safe("shared")
fpic = self.options.get_safe("fPIC")
header_only = self.options.get_safe("header_only")
self.output.info(f"shared: {{shared}}, fPIC: {{fpic}}, header only: {{header_only}}")
""")
client.save({"conanfile.py": conanfile})
client.run(f"create . --name=pkg --version=0.1 -s os={settings_os}")
result = f"shared: {result[0]}, fPIC: {result[1]}, header only: {result[2]}"
self.assertIn(result, client.out)
if header_only:
self.assertIn("Package 'da39a3ee5e6b4b0d3255bfef95601890afd80709' created", client.out)

def test_header_package_type_pid(self):
"""
Test that we get the pid for header only when package type is set to header-library
"""
client = TestClient()
conanfile = textwrap.dedent(f"""\
from conan import ConanFile
class Pkg(ConanFile):
settings = "os", "compiler", "arch", "build_type"
package_type = "header-library"
""")
client.save({"conanfile.py": conanfile})
client.run(f"create . --name=pkg --version=0.1")
self.assertIn("Package 'da39a3ee5e6b4b0d3255bfef95601890afd80709' created", client.out)
9 changes: 5 additions & 4 deletions conans/test/integration/package_id/test_default_package_id.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@


@pytest.mark.parametrize("typedep, typeconsumer, different_id",
[("header-library", "application", True),
("header-library", "shared-library", True),
("header-library", "static-library", True),
[("header-library", "application", False),
("header-library", "shared-library", False),
("header-library", "static-library", False),
("static-library", "application", True),
("static-library", "shared-library", True),
("static-library", "header-library", False),
Expand All @@ -33,7 +33,8 @@ def test_default_package_id_options(typedep, typeconsumer, different_id):
pid1 = c.created_package_id("dep/0.1")
c.run("create dep -o dep/*:myopt=False")
pid2 = c.created_package_id("dep/0.1")
assert pid1 != pid2
if typedep != "header-library":
assert pid1 != pid2

c.run("create consumer -o dep/*:myopt=True")
pid1 = c.created_package_id("consumer/0.1")
Expand Down
12 changes: 11 additions & 1 deletion conans/test/utils/mocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ def __getattr__(self, name):
except KeyError:
raise ConanException("'%s' value not defined" % name)

def rm_safe(self, name):
self.values.pop(name, None)


class MockCppInfo(object):
def __init__(self):
Expand All @@ -81,6 +84,7 @@ class MockConanfile(ConanFile):
def __init__(self, settings, options=None, runner=None):
self.display_name = ""
self._conan_node = None
self.package_type = "unknown"
self.folders = Folders()
self.settings = settings
self.settings_build = settings
Expand All @@ -90,9 +94,15 @@ def __init__(self, settings, options=None, runner=None):
self.conf = Conf()

class MockConanInfo:
pass
settings = None
options = None

def clear(self):
self.settings = {}
self.options = {}
self.info = MockConanInfo()
self.info.settings = settings # Incomplete, only settings for Cppstd Min/Max tests
self.info.options = options

def run(self, *args, **kwargs):
if self.runner:
Expand Down

0 comments on commit 2af05b5

Please sign in to comment.