Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Explore improved error message for missing component requirements #17319

Draft
wants to merge 3 commits into
base: develop2
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion conan/tools/apple/xcodedeps.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ def _transitive_components(component):
else:
public_deps.append((_format_name(d.ref.name),) * 2)

required_components = dep.cpp_info.required_components if dep.cpp_info.required_components else public_deps
required_components = [(c[0], c[1]) for c in dep.cpp_info.required_components] if dep.cpp_info.required_components else public_deps
# In case dep is editable and package_folder=None
pkg_folder = dep.package_folder or dep.recipe_folder
root_content = self.get_content_for_component(require, dep_name, dep_name, pkg_folder, [dep.cpp_info],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ def get_deps_targets_names(self):
# Declared cppinfo.requires or .components[].requires
transitive_reqs = self.cmakedeps.get_transitive_requires(self.conanfile)
if self.conanfile.cpp_info.required_components:
for dep_name, component_name in self.conanfile.cpp_info.required_components:
for dep_name, component_name, _ in self.conanfile.cpp_info.required_components:
try:
# if not dep_name, it is internal, from current self.conanfile
req = transitive_reqs[dep_name] if dep_name is not None else self.conanfile
Expand Down
28 changes: 17 additions & 11 deletions conans/model/build_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -685,10 +685,13 @@ def check_component_requires(self, conanfile):
return
# Accumulate all external requires
comps = self.required_components
missing_internal = [c[1] for c in comps if c[0] is None and c[1] not in self.components]
missing_internal = [(c[1], c[2]) for c in comps if c[0] is None and c[1] not in self.components]
if missing_internal:
raise ConanException(f"{conanfile}: Internal components not found: {missing_internal}")
external = [c[0] for c in comps if c[0] is not None]
msgs = [f"'{req}'"
+ (f" (required from component '{req_comp}')" if req_comp else "")
for req, req_comp in missing_internal]
raise ConanException(f"{conanfile}: Internal components not found: {', '.join(msgs)}")
external = [(c[0], c[2]) for c in comps if c[0] is not None]
if not external:
return
# Only direct host (not test) dependencies can define required components
Expand All @@ -697,29 +700,32 @@ def check_component_requires(self, conanfile):
direct_dependencies = [r.ref.name for r, d in conanfile.dependencies.items() if r.direct
and not r.build and not r.is_test and r.visible and not r.override]

for e in external:
for e, req_comp in external:
if e not in direct_dependencies:
extra_msg = f" (required from component '{req_comp}')" if req_comp else ""
raise ConanException(
f"{conanfile}: required component package '{e}::' not in dependencies")
f"{conanfile}: required component package '{e}::'{extra_msg} not in dependencies")
# TODO: discuss if there are cases that something is required but not transitive
external = [e[0] for e in external]
for e in direct_dependencies:
if e not in external:
raise ConanException(
f"{conanfile}: Required package '{e}' not in component 'requires'")

@property
def required_components(self):
"""Returns a list of tuples with (require, component_name) required by the package
If the require is internal (to another component), the require will be None"""
"""Returns a list of tuples with (require, component_name, dependee_component_name) required by the package
- If the require is internal (to another component), the require will be None
- If the requirement comes from the main cpp_info, the dependee_component_name will be None"""
# FIXME: Cache the value
# First aggregate without repetition, respecting the order
ret = [r for r in self._package.requires]
for comp in self.components.values():
ret = [(r, None) for r in self._package.requires]
for comp_name, comp in self.components.items():
for r in comp.requires:
if r not in ret:
ret.append(r)
ret.append((r, comp_name))
# Then split the names
ret = [r.split("::") if "::" in r else (None, r) for r in ret]
ret = [(*r[0].split("::"), r[1]) if "::" in r[0] else (None, *r) for r in ret]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the main change - required_components now returns a 3-tuple. This is a draft because as far as I can see, this might be breaking behaviour as this function is public, so not too sure on the best approach.

Also note that as far as I can tell, the unpacking operator * was implemented in Python 3.5 so we're good to use it

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Public, but not documented apparently.
Still, it reads a bit risky, yes, not fully sure if worth the risk. Maybe a different approach like re-computing things when the check fails for better error messages (making it exclusively local to the error handling)?

return ret

def deduce_full_cpp_info(self, conanfile):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,85 +116,6 @@ def package_info(self):
assert "Component 'top::not-existing' not found in 'top' package requirement" in t.out


# TODO: This is CMakeDeps Independent, move it out of here
def test_unused_requirement(top_conanfile):
""" Requires should include all listed requirements
This error is known when creating the package if the requirement is consumed.
"""
consumer = textwrap.dedent("""
from conan import ConanFile

class Recipe(ConanFile):
requires = "top/version", "top2/version"
def package_info(self):
self.cpp_info.requires = ["top::other"]
""")
t = TestClient()
t.save({'top.py': top_conanfile, 'consumer.py': consumer})
t.run('create top.py --name=top --version=version')
t.run('create top.py --name=top2 --version=version')
t.run('create consumer.py --name=wrong --version=version', assert_error=True)
assert "ERROR: wrong/version: Required package 'top2' not in component 'requires" in t.out



# TODO: This is CMakeDeps Independent, move it out of here
def test_unused_tool_requirement(top_conanfile):
""" Requires should include all listed requirements
This error is known when creating the package if the requirement is consumed.
"""
consumer = textwrap.dedent("""
from conan import ConanFile

class Recipe(ConanFile):
requires = "top/version"
tool_requires = "top2/version"
def package_info(self):
self.cpp_info.requires = ["top::other"]
""")
t = TestClient()
t.save({'top.py': top_conanfile, 'consumer.py': consumer})
t.run('create top.py --name=top --version=version')
t.run('create top.py --name=top2 --version=version')
t.run('create consumer.py --name=wrong --version=version')
# This runs without crashing, because it is not chcking that top::other doesn't exist


# TODO: This is CMakeDeps Independent, move it out of here
def test_wrong_requirement(top_conanfile):
""" If we require a wrong requirement, we get a meaninful error.
This error is known when creating the package if the requirement is not there.
"""
consumer = textwrap.dedent("""
from conan import ConanFile

class Recipe(ConanFile):
requires = "top/version"
def package_info(self):
self.cpp_info.requires = ["top::cmp1", "other::other"]
""")
t = TestClient()
t.save({'top.py': top_conanfile, 'consumer.py': consumer})
t.run('create top.py --name=top --version=version')
t.run('create consumer.py --name=wrong --version=version', assert_error=True)
assert "ERROR: wrong/version: required component package 'other::' not in dependencies" in t.out


# TODO: This is CMakeDeps Independent, move it out of here
def test_missing_internal():
consumer = textwrap.dedent("""
from conan import ConanFile

class Recipe(ConanFile):
def package_info(self):
self.cpp_info.components["cmp1"].requires = ["other"]
""")
t = TestClient()
t.save({'conanfile.py': consumer})
t.run('create . --name=wrong --version=version', assert_error=True)
assert "ERROR: wrong/version: Internal components not found: ['other']" in t.out


@pytest.mark.tool("cmake")
def test_components_system_libs():
conanfile = textwrap.dedent("""
Expand Down
76 changes: 76 additions & 0 deletions test/integration/test_components_error.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import os
import textwrap

import pytest

from conan.test.assets.genconanfile import GenConanfile
from conan.test.utils.tools import TestClient


Expand Down Expand Up @@ -68,3 +71,76 @@ def package_info(self):
c.save({"conanfile.py": conanfile})
c.run("create .", assert_error=True)
assert 'The expected type for test_property is "list", but "str" was found' in c.out


@pytest.mark.parametrize("component", [True, False])
def test_unused_requirement(component):
""" Requires should include all listed requirements
This error is known when creating the package if the requirement is consumed.
"""

t = TestClient(light=True)
conanfile = textwrap.dedent(f"""
from conan import ConanFile
class Consumer(ConanFile):
name = "wrong"
version = "version"
requires = "top/version", "top2/version"

def package_info(self):
self.cpp_info{'.components["foo"]' if component else ''}.requires = ["top::other"]
""")
t.save({"top/conanfile.py": GenConanfile().with_package_info({"components": {"cmp1": {"libs": ["top_cmp1"]}}}),
"conanfile.py": conanfile})
t.run('create top --name=top --version=version')
t.run('create top --name=top2 --version=version')
t.run('create .', assert_error=True)
assert "ERROR: wrong/version: Required package 'top2' not in component 'requires" in t.out
AbrilRBS marked this conversation as resolved.
Show resolved Hide resolved


@pytest.mark.parametrize("component", [True, False])
def test_wrong_requirement(component):
""" If we require a wrong requirement, we get a meaninful error.
This error is known when creating the package if the requirement is not there.
"""
t = TestClient(light=True)
conanfile = textwrap.dedent(f"""
from conan import ConanFile
class Consumer(ConanFile):
name = "wrong"
version = "version"
requires = "top/version"

def package_info(self):
self.cpp_info{'.components["foo"]' if component else ''}.requires = ["top::cmp1", "other::other"]
""")
t.save({"top/conanfile.py": GenConanfile().with_package_info({"components": {"cmp1": {"libs": ["top_cmp1"]}}}),
"conanfile.py": conanfile})
t.run('create top --name=top --version=version')
t.run('create .', assert_error=True)
if component:
assert "ERROR: wrong/version: required component package 'other::' (required from component 'foo') not in dependencies" in t.out
else:
assert "ERROR: wrong/version: required component package 'other::' not in dependencies" in t.out


@pytest.mark.parametrize("component", [True, False])
def test_missing_internal(component):
consumer = textwrap.dedent(f"""
from conan import ConanFile

class Recipe(ConanFile):
def package_info(self):
self.cpp_info{'.components["foo"]' if component else ''}.requires = ["other", "another"]
self.cpp_info{'.components["bar"]' if component else ''}.requires = ["other", "another"]
""")
t = TestClient(light=True)
t.save({'conanfile.py': consumer})
t.run('create . --name=wrong --version=version', assert_error=True)
if component:
assert ("ERROR: wrong/version: Internal components not found: 'other' (required from component 'foo'),"
" 'another' (required from component 'foo'),"
" 'other' (required from component 'bar'),"
" 'another' (required from component 'bar')") in t.out
else:
assert "ERROR: wrong/version: Internal components not found: 'other', 'another'" in t.out