Skip to content

Commit

Permalink
validate_build over settings (#11580)
Browse files Browse the repository at this point in the history
* validate_build over settings

* improvements

* prev none too

* Moved validate_build verification

* Unused import

* Simplified test

* Moved validate_build close to validate
  • Loading branch information
lasote committed Jul 12, 2022
1 parent 4e6001b commit fae0b53
Show file tree
Hide file tree
Showing 7 changed files with 127 additions and 6 deletions.
4 changes: 4 additions & 0 deletions conans/client/conan_command_output.py
Expand Up @@ -140,6 +140,10 @@ def _grab_info_data(self, deps_graph, grab_paths):
item_data["build_id"] = build_id(conanfile)
item_data["context"] = conanfile.context

item_data["invalid_build"] = node.cant_build is not False
if node.cant_build:
item_data["invalid_build_reason"] = node.cant_build

python_requires = getattr(conanfile, "python_requires", None)
if python_requires and not isinstance(python_requires, dict): # no old python requires
item_data["python_requires"] = [repr(r)
Expand Down
2 changes: 2 additions & 0 deletions conans/client/graph/graph.py
Expand Up @@ -91,6 +91,8 @@ def __init__(self, ref, conanfile, context, recipe=None, path=None):
self.id_direct_prefs = None
self.id_indirect_prefs = None

self.cant_build = False # It will set to a str with a reason if the validate_build() fails

@property
def id(self):
return self._id
Expand Down
30 changes: 26 additions & 4 deletions conans/client/graph/graph_binaries.py
Expand Up @@ -48,7 +48,10 @@ def _evaluate_build(node, build_mode):
break
if build_mode.forced(conanfile, ref, with_deps_to_build):
conanfile.output.info('Forced build from source')
node.binary = BINARY_BUILD
if node.cant_build:
node.binary = BINARY_INVALID
else:
node.binary = BINARY_BUILD
node.prev = None
return True

Expand Down Expand Up @@ -178,7 +181,10 @@ def _evaluate_node(self, node, build_mode, update, remotes):
pref = PackageReference(locked.ref, locked.package_id, locked.prev) # Keep locked PREV
self._process_node(node, pref, build_mode, update, remotes)
if node.binary == BINARY_MISSING and build_mode.allowed(node.conanfile):
node.binary = BINARY_BUILD
if node.cant_build:
node.binary = BINARY_INVALID
else:
node.binary = BINARY_BUILD
if node.binary == BINARY_BUILD:
locked.unlock_prev()

Expand Down Expand Up @@ -231,7 +237,10 @@ def _evaluate_node(self, node, build_mode, update, remotes):
if node.binary == BINARY_MISSING and node.package_id == PACKAGE_ID_INVALID:
node.binary = BINARY_INVALID
if node.binary == BINARY_MISSING and build_mode.allowed(node.conanfile):
node.binary = BINARY_BUILD
if node.cant_build:
node.binary = BINARY_INVALID
else:
node.binary = BINARY_BUILD

if locked:
# package_id was not locked, this means a base lockfile that is being completed
Expand All @@ -253,6 +262,8 @@ def _process_node(self, node, pref, build_mode, update, remotes):
node.binary = BINARY_INVALID
return



if self._evaluate_build(node, build_mode):
return

Expand Down Expand Up @@ -293,7 +304,10 @@ def _process_node(self, node, pref, build_mode, update, remotes):
local_recipe_hash = package_layout.recipe_manifest().summary_hash
if local_recipe_hash != recipe_hash:
conanfile.output.info("Outdated package!")
node.binary = BINARY_BUILD
if node.cant_build:
node.binary = BINARY_INVALID
else:
node.binary = BINARY_BUILD
node.prev = None
else:
conanfile.output.info("Package is up to date")
Expand Down Expand Up @@ -400,6 +414,14 @@ def _compute_package_id(self, node, default_package_id_mode, default_python_requ
except ConanInvalidConfiguration as e:
conanfile.info.invalid = str(e)

if hasattr(conanfile, "validate_build") and callable(conanfile.validate_build):
with conanfile_exception_formatter(str(conanfile), "validate_build"):
try:
conanfile.validate_build()
except ConanInvalidConfiguration as e:
# This 'cant_build' will be ignored if we don't have to build the node.
node.cant_build = str(e)

info = conanfile.info
node.package_id = info.package_id()

Expand Down
5 changes: 4 additions & 1 deletion conans/client/graph/graph_builder.py
Expand Up @@ -3,7 +3,7 @@
from conans.client.conanfile.configure import run_configure_method
from conans.client.graph.graph import DepsGraph, Node, RECIPE_EDITABLE, CONTEXT_HOST, CONTEXT_BUILD
from conans.errors import (ConanException, ConanExceptionInUserConanfileMethod,
conanfile_exception_formatter)
conanfile_exception_formatter, ConanInvalidConfiguration)
from conans.model.conan_file import get_env_context_manager
from conans.model.ref import ConanFileReference
from conans.model.requires import Requirements, Requirement
Expand Down Expand Up @@ -65,7 +65,9 @@ def load_graph(self, root_node, check_updates, update, remotes, profile_host, pr
t1 = time.time()
self._expand_node(root_node, dep_graph, Requirements(), None, None, check_updates,
update, remotes, profile_host, profile_build, graph_lock)

logger.debug("GRAPH: Time to load deps %s" % (time.time() - t1))

return dep_graph

def extend_build_requires(self, graph, node, build_requires_refs, check_updates, update,
Expand Down Expand Up @@ -471,4 +473,5 @@ def _create_new_node(self, current_node, dep_graph, requirement, check_updates,

dep_graph.add_node(new_node)
dep_graph.add_edge(current_node, new_node, requirement)

return new_node
1 change: 1 addition & 0 deletions conans/client/graph/graph_manager.py
Expand Up @@ -124,6 +124,7 @@ def load_graph(self, reference, create_reference, graph_info, build_mode, check_
root_node = self._load_root_node(reference, create_reference, profile_host, graph_lock,
root_ref, lockfile_node_id, is_build_require,
require_overrides)

deps_graph = self._resolve_graph(root_node, profile_host, profile_build, graph_lock,
build_mode, check_updates, update, remotes, recorder,
apply_build_requires=apply_build_requires)
Expand Down
8 changes: 7 additions & 1 deletion conans/client/installer.py
Expand Up @@ -423,7 +423,13 @@ def _build(self, nodes_by_level, keep_build, root_node, profile_host, profile_bu
if invalid:
msg = ["There are invalid packages (packages that cannot exist for this configuration):"]
for node in invalid:
msg.append("{}: Invalid ID: {}".format(node.conanfile, node.conanfile.info.invalid))
if node.cant_build:
msg.append("{}: Cannot build "
"for this configuration: {}".format(node.conanfile,
node.cant_build))
else:
msg.append("{}: Invalid ID: {}".format(node.conanfile,
node.conanfile.info.invalid))
raise ConanInvalidConfiguration("\n".join(msg))
self._raise_missing(missing)
processed_package_refs = {}
Expand Down
83 changes: 83 additions & 0 deletions conans/test/integration/graph/test_validate_build.py
@@ -0,0 +1,83 @@
import json
import textwrap

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


def test_basic_validate_build_test():

t = TestClient()
conanfile = textwrap.dedent("""
from conan import ConanFile
from conans.errors import ConanInvalidConfiguration
class myConan(ConanFile):
name = "foo"
version = "1.0"
settings = "os", "arch", "compiler"
def validate_build(self):
if self.settings.compiler == "gcc":
raise ConanInvalidConfiguration("This doesn't build in GCC")
def package_id(self):
del self.info.settings.compiler
""")

settings_gcc = "-s compiler=gcc -s compiler.libcxx=libstdc++11 -s compiler.version=11"
settings_clang = "-s compiler=clang -s compiler.libcxx=libc++ -s compiler.version=8"

t.save({"conanfile.py": conanfile})
t.run(f"create . {settings_gcc}", assert_error=True)

assert "foo/1.0: Cannot build for this configuration: This doesn't build in GCC" in t.out

t.run(f"create . {settings_clang}")

# Now with GCC again, but now we have the binary, we don't need to build, so it doesn't fail
t.run(f"create . {settings_gcc} --build missing")
assert "foo/1.0: Already installed!" in t.out

# But if I force the build... it will fail
t.run(f"create . {settings_gcc} ", assert_error=True)
assert "foo/1.0: Cannot build for this configuration: This doesn't build in GCC" in t.out

# What happens with a conan info?
t.run(f"info foo/1.0@ {settings_gcc} --json=myjson")
myjson = json.loads(t.load("myjson"))
assert myjson[0]["invalid_build"] is True
assert myjson[0]["invalid_build_reason"] == "This doesn't build in GCC"

t.run(f"info foo/1.0@ {settings_clang} --json=myjson")
myjson = json.loads(t.load("myjson"))
assert myjson[0]["invalid_build"] is False
assert "invalid_build_reason" not in myjson[0]


def test_with_options_validate_build_test():
t = TestClient()
conanfile = textwrap.dedent("""
from conan import ConanFile
from conans.errors import ConanInvalidConfiguration
class myConan(ConanFile):
name = "foo"
version = "1.0"
options = {"my_option": [True, False]}
default_options = {"my_option": True}
def validate_build(self):
if not self.options.my_option:
raise ConanInvalidConfiguration("This doesn't build with False option")
""")
t.save({"conanfile.py": conanfile})
t.run("export .")
consumer = GenConanfile().with_require("foo/1.0").with_name("consumer").with_version("1.0")
t.save({"consumer.py": consumer})
t.run("create consumer.py --build missing -o foo/*:my_option=False", assert_error=True)
assert "foo/1.0: Cannot build for this configuration: This doesn't build " \
"with False option" in t.out

t.run("create consumer.py --build missing -o foo/*:my_option=True")

0 comments on commit fae0b53

Please sign in to comment.