From f215e1e77c04a6e5ead35fd38184b65fb95cd5fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Rinc=C3=B3n=20Blanco?= Date: Wed, 27 Dec 2023 14:13:39 +0100 Subject: [PATCH 1/7] Fix graph explain not showing some differences in requirements if missing --- conan/api/subapi/list.py | 6 ++ .../command_v2/test_graph_find_binaries.py | 56 +++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/conan/api/subapi/list.py b/conan/api/subapi/list.py index 3ff3bf6f39d..dca054a6b2d 100644 --- a/conan/api/subapi/list.py +++ b/conan/api/subapi/list.py @@ -250,6 +250,12 @@ def __init__(self, pref, binary_config, expected_config, remote=None): if not existing or r != existing: self.deps_diff.setdefault("expected", []).append(repr(r)) self.deps_diff.setdefault("existing", []).append(repr(existing)) + expected_requires = {r.name: r for r in expected_requires} + for r in binary_requires.values(): + existing = expected_requires.get(r.name) + if not existing or r != existing: + self.deps_diff.setdefault("expected", []).append(repr(existing)) + self.deps_diff.setdefault("existing", []).append(repr(r)) def __lt__(self, other): return self.distance < other.distance diff --git a/conans/test/integration/command_v2/test_graph_find_binaries.py b/conans/test/integration/command_v2/test_graph_find_binaries.py index 25812f4e459..860d049de4f 100644 --- a/conans/test/integration/command_v2/test_graph_find_binaries.py +++ b/conans/test/integration/command_v2/test_graph_find_binaries.py @@ -203,6 +203,62 @@ def test_other_dependencies(self, client): assert textwrap.indent(expected, " ") in c.out +def test_change_in_package_type(): + tc = TestClient(light=True) + tc.save({ + "libc/conanfile.py": GenConanfile("libc", "1.0"), + "libb/conanfile.py": GenConanfile("libb", "1.0") + .with_requires("libc/1.0"), + "liba/conanfile.py": GenConanfile("liba", "1.0") + .with_requires("libb/1.0") + }) + + tc.run("create libc") + tc.run("create libb") + tc.run("create liba") + + tc.save({ + "libc/conanfile.py": GenConanfile("libc", "1.0") + .with_package_type("application") + }) + tc.run("create libc") + + tc.run("create liba", assert_error=True) + assert "Missing binary: libb/1.0" in tc.out + + tc.run("graph explain --requires=liba/1.0") + # This fails, graph explain thinks everything is ok + assert "explanation: This binary is an exact match for the defined inputs" not in tc.out + + +def test_conf_difference_shown(): + tc = TestClient(light=True) + tc.save({ + "libc/conanfile.py": GenConanfile("libc", "1.0"), + "libb/conanfile.py": GenConanfile("libb", "1.0") + .with_requires("libc/1.0"), + "liba/conanfile.py": GenConanfile("liba", "1.0") + .with_requires("libb/1.0") + }) + tc.save_home({"global.conf": "tools.info.package_id:confs=['user.*']"}) + + tc.run("create libc") + tc.run("create libb") + tc.run("create liba") + + tc.run("remove libc/*:* -c") + + tc.run("create libc -c user.foo:bar=42") + + tc.run("create liba", assert_error=True) + assert "Missing prebuilt package for 'libc/1.0'" in tc.out + + tc.run("graph explain --requires=liba/1.0") + # This fails, graph explain thinks everything is ok + assert "conf: user.foo:bar=42" in tc.out + assert "explanation: This binary is an exact match for the defined inputs" not in tc.out + + class TestDistance: def test_multiple_distance_ordering(self): tc = TestClient() From ecf4900a539350ecc2d29088291e979e11fd171f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Rinc=C3=B3n=20Blanco?= Date: Thu, 28 Dec 2023 09:24:49 +0100 Subject: [PATCH 2/7] Requirements need to be added to a set --- conan/api/subapi/list.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/conan/api/subapi/list.py b/conan/api/subapi/list.py index dca054a6b2d..24a1475c30a 100644 --- a/conan/api/subapi/list.py +++ b/conan/api/subapi/list.py @@ -248,14 +248,14 @@ def __init__(self, pref, binary_config, expected_config, remote=None): for r in expected_requires: existing = binary_requires.get(r.name) if not existing or r != existing: - self.deps_diff.setdefault("expected", []).append(repr(r)) - self.deps_diff.setdefault("existing", []).append(repr(existing)) + self.deps_diff.setdefault("expected", set()).add(repr(r)) + self.deps_diff.setdefault("existing", set()).add(repr(existing)) expected_requires = {r.name: r for r in expected_requires} for r in binary_requires.values(): existing = expected_requires.get(r.name) if not existing or r != existing: - self.deps_diff.setdefault("expected", []).append(repr(existing)) - self.deps_diff.setdefault("existing", []).append(repr(r)) + self.deps_diff.setdefault("expected", set()).add(repr(existing)) + self.deps_diff.setdefault("existing", set()).add(repr(r)) def __lt__(self, other): return self.distance < other.distance From 28b20f35a4f78151b18921391bce1f8cd68856a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Rinc=C3=B3n=20Blanco?= Date: Thu, 28 Dec 2023 09:38:33 +0100 Subject: [PATCH 3/7] Fix tests --- .../test/integration/command_v2/test_graph_find_binaries.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/conans/test/integration/command_v2/test_graph_find_binaries.py b/conans/test/integration/command_v2/test_graph_find_binaries.py index 860d049de4f..7682d8279e9 100644 --- a/conans/test/integration/command_v2/test_graph_find_binaries.py +++ b/conans/test/integration/command_v2/test_graph_find_binaries.py @@ -240,7 +240,7 @@ def test_conf_difference_shown(): "liba/conanfile.py": GenConanfile("liba", "1.0") .with_requires("libb/1.0") }) - tc.save_home({"global.conf": "tools.info.package_id:confs=['user.*']"}) + tc.save_home({"global.conf": "tools.info.package_id:confs=['user.foo:bar']"}) tc.run("create libc") tc.run("create libb") @@ -254,9 +254,7 @@ def test_conf_difference_shown(): assert "Missing prebuilt package for 'libc/1.0'" in tc.out tc.run("graph explain --requires=liba/1.0") - # This fails, graph explain thinks everything is ok assert "conf: user.foo:bar=42" in tc.out - assert "explanation: This binary is an exact match for the defined inputs" not in tc.out class TestDistance: From 745433226f175f92246f0fbb4189d2dbe3948e00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Rinc=C3=B3n=20Blanco?= Date: Thu, 4 Jan 2024 12:33:08 +0100 Subject: [PATCH 4/7] Cover the rest of the conan_info cases --- conan/api/subapi/list.py | 81 ++++++++++++++++++- .../command_v2/test_graph_find_binaries.py | 61 ++++++++++++++ 2 files changed, 141 insertions(+), 1 deletion(-) diff --git a/conan/api/subapi/list.py b/conan/api/subapi/list.py index 24a1475c30a..7418b703fc3 100644 --- a/conan/api/subapi/list.py +++ b/conan/api/subapi/list.py @@ -228,6 +228,16 @@ def __init__(self, pref, binary_config, expected_config, remote=None): diff.setdefault("expected", []).append(f"{k}={v}") diff.setdefault("existing", []).append(f"{k}={value}") + # Settings_target + self.settings_target_diff = {} + binary_settings_target = binary_config.get("settings_target", {}) + expected_settings_target = expected_config.get("settings_target", {}) + for k, v in expected_settings_target.items(): + value = binary_settings_target.get(k) + if value is not None and value != v: + self.settings_target_diff.setdefault("expected", []).append(f"{k}={v}") + self.settings_target_diff.setdefault("existing", []).append(f"{k}={value}") + # Options self.options_diff = {} binary_options = binary_config.get("options", {}) @@ -257,6 +267,59 @@ def __init__(self, pref, binary_config, expected_config, remote=None): self.deps_diff.setdefault("expected", set()).add(repr(existing)) self.deps_diff.setdefault("existing", set()).add(repr(r)) + # Build requires + self.build_requires_diff = {} + binary_build_requires = binary_config.get("build_requires", []) + expected_build_requires = expected_config.get("build_requires", []) + binary_build_requires = [RecipeReference.loads(r) for r in binary_build_requires] + expected_build_requires = [RecipeReference.loads(r) for r in expected_build_requires] + binary_build_requires = {r.name: r for r in binary_build_requires} + for r in expected_build_requires: + existing = binary_build_requires.get(r.name) + if not existing or r != existing: + self.build_requires_diff.setdefault("expected", set()).add(repr(r)) + self.build_requires_diff.setdefault("existing", set()).add(repr(existing)) + expected_build_requires = {r.name: r for r in expected_build_requires} + for r in binary_build_requires.values(): + existing = expected_build_requires.get(r.name) + if not existing or r != existing: + self.build_requires_diff.setdefault("expected", set()).add(repr(existing)) + self.build_requires_diff.setdefault("existing", set()).add(repr(r)) + + # Python requires + self.python_requires_diff = {} + binary_python_requires = binary_config.get("python_requires", []) + expected_python_requires = expected_config.get("python_requires", []) + binary_python_requires = [RecipeReference.loads(r) for r in binary_python_requires] + expected_python_requires = [RecipeReference.loads(r) for r in expected_python_requires] + binary_python_requires = {r.name: r for r in binary_python_requires} + for r in expected_python_requires: + existing = binary_python_requires.get(r.name) + if not existing or r != existing: + self.python_requires_diff.setdefault("expected", set()).add(repr(r)) + self.python_requires_diff.setdefault("existing", set()).add(repr(existing)) + expected_python_requires = {r.name: r for r in expected_python_requires} + for r in binary_python_requires.values(): + existing = expected_python_requires.get(r.name) + if not existing or r != existing: + self.python_requires_diff.setdefault("expected", set()).add(repr(existing)) + self.python_requires_diff.setdefault("existing", set()).add(repr(r)) + + # Confs + self.confs_diff = {} + binary_confs = binary_config.get("conf", {}) + expected_confs = expected_config.get("conf", {}) + for k, v in expected_confs.items(): + value = binary_confs.get(k) + if value != v: + self.confs_diff.setdefault("expected", []).append(f"{k}={v}") + self.confs_diff.setdefault("existing", []).append(f"{k}={value}") + for k, v in binary_confs.items(): + value = expected_confs.get(k) + if value != v: + self.confs_diff.setdefault("expected", []).append(f"{k}={value}") + self.confs_diff.setdefault("existing", []).append(f"{k}={v}") + def __lt__(self, other): return self.distance < other.distance @@ -265,22 +328,38 @@ def explanation(self): return "This binary belongs to another OS or Architecture, highly incompatible." if self.settings_diff: return "This binary was built with different settings." + if self.settings_target_diff: + return "This binary was built with different settings_target." if self.options_diff: return "This binary was built with the same settings, but different options" if self.deps_diff: return "This binary has same settings and options, but different dependencies" + if self.build_requires_diff: + return "This binary has same settings, options and dependencies, but different build_requires" + if self.python_requires_diff: + return "This binary has same settings, options and dependencies, but different python_requires" + if self.confs_diff: + return "This binary has same settings, options and dependencies, but different confs" return "This binary is an exact match for the defined inputs" @property def distance(self): return (len(self.platform_diff.get("expected", [])), len(self.settings_diff.get("expected", [])), + len(self.settings_target_diff.get("expected", [])), len(self.options_diff.get("expected", [])), - len(self.deps_diff.get("expected", []))) + len(self.deps_diff.get("expected", [])), + len(self.build_requires_diff.get("expected", [])), + len(self.python_requires_diff.get("expected", [])), + len(self.confs_diff.get("expected", []))) def serialize(self): return {"platform": self.platform_diff, "settings": self.settings_diff, + "settings_target": self.settings_target_diff, "options": self.options_diff, "dependencies": self.deps_diff, + "build_requires": self.build_requires_diff, + "python_requires": self.python_requires_diff, + "confs": self.confs_diff, "explanation": self.explanation()} diff --git a/conans/test/integration/command_v2/test_graph_find_binaries.py b/conans/test/integration/command_v2/test_graph_find_binaries.py index 7682d8279e9..cdd6bc8027b 100644 --- a/conans/test/integration/command_v2/test_graph_find_binaries.py +++ b/conans/test/integration/command_v2/test_graph_find_binaries.py @@ -153,6 +153,67 @@ def test_different_platform(self, client): assert pkg1["diff"]["explanation"] == "This binary belongs to another OS or Architecture, " \ "highly incompatible." + def test_different_conf(self, client): + # We find closest match in other platforms + c = client + c.save_home({"global.conf": "tools.info.package_id:confs=['user.foo:bar']"}) + c.run("graph explain --requires=lib/1.0 -c user.foo:bar=42 -s os=Linux") + expected = textwrap.dedent("""\ + remote: Local Cache + settings: Linux, Release + options: shared=False + diff + confs + expected: user.foo:bar=42 + existing: user.foo:bar=None + explanation: This binary has same settings, options and dependencies, but different confs + """) + assert textwrap.indent(expected, " ") in c.out + c.run("graph explain --requires=lib/1.0 -pr macos --format=json") + cache = json.loads(c.stdout)["closest_binaries"] + revisions = cache["lib/1.0"]["revisions"] + pkgs = revisions["5313a980ea0c56baeb582c510d6d9fbc"]["packages"] + assert len(pkgs) == 1 + pkg1 = pkgs["c2dd2d51b5074bdb5b7d717929372de09830017b"] + assert pkg1["diff"]["platform"] == {'existing': ['os=Windows'], 'expected': ['os=Macos']} + assert pkg1["diff"]["settings"] == {} + assert pkg1["diff"]["options"] == {} + assert pkg1["diff"]["dependencies"] == {} + assert pkg1["diff"]["explanation"] == "This binary belongs to another OS or Architecture, " \ + "highly incompatible." + + def test_different_python_requires(self): + c = TestClient(light=True) + c.save({"tool/conanfile.py": GenConanfile("tool"), + "lib/conanfile.py": GenConanfile("lib", "1.0") + .with_python_requires("tool/[>=1.0]")}) + c.run("create tool --version=1.0 -s os=Linux") + c.run("create lib -s os=Linux") + c.run("create tool --version=2.0 -s os=Linux") + c.run("graph explain --requires=lib/1.0 -s os=Linux") + expected = textwrap.dedent("""\ + remote: Local Cache + python_requires: tool/1.0.Z + diff + python_requires + expected: tool/2.0.Z + existing: tool/1.0.Z + explanation: This binary has same settings, options and dependencies, but different python_requires + """) + assert textwrap.indent(expected, " ") in c.out + c.run("graph explain --requires=lib/1.0 -s os=Linux --format=json") + cache = json.loads(c.stdout)["closest_binaries"] + revisions = cache["lib/1.0"]["revisions"] + pkgs = revisions["5313a980ea0c56baeb582c510d6d9fbc"]["packages"] + assert len(pkgs) == 1 + pkg1 = pkgs["c2dd2d51b5074bdb5b7d717929372de09830017b"] + assert pkg1["diff"]["platform"] == {'existing': ['os=Windows'], 'expected': ['os=Macos']} + assert pkg1["diff"]["settings"] == {} + assert pkg1["diff"]["options"] == {} + assert pkg1["diff"]["dependencies"] == {} + assert pkg1["diff"]["explanation"] == "This binary belongs to another OS or Architecture, " \ + "highly incompatible." + class TestMissingBinaryDeps: @pytest.fixture(scope="class") From e684747904ef98baac8d11a852c58827c5df5d20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Rinc=C3=B3n=20Blanco?= Date: Tue, 9 Jan 2024 09:11:35 +0100 Subject: [PATCH 5/7] Refactor to clean up the code --- conan/api/subapi/list.py | 114 ++++++++++++++------------------------- 1 file changed, 41 insertions(+), 73 deletions(-) diff --git a/conan/api/subapi/list.py b/conan/api/subapi/list.py index 7418b703fc3..16bb6fa0416 100644 --- a/conan/api/subapi/list.py +++ b/conan/api/subapi/list.py @@ -216,7 +216,7 @@ def __init__(self, pref, binary_config, expected_config, remote=None): self.pref = pref self.binary_config = binary_config - # Settings + # Settings, special handling for os/arch self.platform_diff = {} self.settings_diff = {} binary_settings = binary_config.get("settings", {}) @@ -228,97 +228,65 @@ def __init__(self, pref, binary_config, expected_config, remote=None): diff.setdefault("expected", []).append(f"{k}={v}") diff.setdefault("existing", []).append(f"{k}={value}") - # Settings_target self.settings_target_diff = {} - binary_settings_target = binary_config.get("settings_target", {}) - expected_settings_target = expected_config.get("settings_target", {}) - for k, v in expected_settings_target.items(): - value = binary_settings_target.get(k) - if value is not None and value != v: - self.settings_target_diff.setdefault("expected", []).append(f"{k}={v}") - self.settings_target_diff.setdefault("existing", []).append(f"{k}={value}") + self.calculate_diff(binary_config.get("settings_target", {}), + expected_config.get("settings_target", {}), + self.settings_target_diff) - # Options self.options_diff = {} - binary_options = binary_config.get("options", {}) - expected_options = expected_config.get("options", {}) - for k, v in expected_options.items(): - value = binary_options.get(k) - if value is not None and value != v: - self.options_diff.setdefault("expected", []).append(f"{k}={v}") - self.options_diff.setdefault("existing", []).append(f"{k}={value}") + self.calculate_diff(binary_config.get("options", {}), + expected_config.get("options", {}), + self.options_diff) - # Requires self.deps_diff = {} - binary_requires = binary_config.get("requires", []) - expected_requires = expected_config.get("requires", []) + self.calculate_requirement_diff(binary_config.get("requires", []), + expected_config.get("requires", []), + self.deps_diff) + + self.build_requires_diff = {} + self.calculate_requirement_diff(binary_config.get("build_requires", []), + expected_config.get("build_requires", []), + self.build_requires_diff) + + self.python_requires_diff = {} + self.calculate_requirement_diff(binary_config.get("python_requires", []), + expected_config.get("python_requires", []), + self.python_requires_diff) + + self.confs_diff = {} + self.calculate_diff(binary_config.get("conf", {}), + expected_config.get("conf", {}), + self.confs_diff, + reverse=True) + + def calculate_requirement_diff(self, binary_requires, expected_requires, output): binary_requires = [RecipeReference.loads(r) for r in binary_requires] expected_requires = [RecipeReference.loads(r) for r in expected_requires] binary_requires = {r.name: r for r in binary_requires} for r in expected_requires: existing = binary_requires.get(r.name) if not existing or r != existing: - self.deps_diff.setdefault("expected", set()).add(repr(r)) - self.deps_diff.setdefault("existing", set()).add(repr(existing)) + output.setdefault("expected", set()).add(repr(r)) + output.setdefault("existing", set()).add(repr(existing)) expected_requires = {r.name: r for r in expected_requires} for r in binary_requires.values(): existing = expected_requires.get(r.name) if not existing or r != existing: - self.deps_diff.setdefault("expected", set()).add(repr(existing)) - self.deps_diff.setdefault("existing", set()).add(repr(r)) + output.setdefault("expected", set()).add(repr(existing)) + output.setdefault("existing", set()).add(repr(r)) - # Build requires - self.build_requires_diff = {} - binary_build_requires = binary_config.get("build_requires", []) - expected_build_requires = expected_config.get("build_requires", []) - binary_build_requires = [RecipeReference.loads(r) for r in binary_build_requires] - expected_build_requires = [RecipeReference.loads(r) for r in expected_build_requires] - binary_build_requires = {r.name: r for r in binary_build_requires} - for r in expected_build_requires: - existing = binary_build_requires.get(r.name) - if not existing or r != existing: - self.build_requires_diff.setdefault("expected", set()).add(repr(r)) - self.build_requires_diff.setdefault("existing", set()).add(repr(existing)) - expected_build_requires = {r.name: r for r in expected_build_requires} - for r in binary_build_requires.values(): - existing = expected_build_requires.get(r.name) - if not existing or r != existing: - self.build_requires_diff.setdefault("expected", set()).add(repr(existing)) - self.build_requires_diff.setdefault("existing", set()).add(repr(r)) - - # Python requires - self.python_requires_diff = {} - binary_python_requires = binary_config.get("python_requires", []) - expected_python_requires = expected_config.get("python_requires", []) - binary_python_requires = [RecipeReference.loads(r) for r in binary_python_requires] - expected_python_requires = [RecipeReference.loads(r) for r in expected_python_requires] - binary_python_requires = {r.name: r for r in binary_python_requires} - for r in expected_python_requires: - existing = binary_python_requires.get(r.name) - if not existing or r != existing: - self.python_requires_diff.setdefault("expected", set()).add(repr(r)) - self.python_requires_diff.setdefault("existing", set()).add(repr(existing)) - expected_python_requires = {r.name: r for r in expected_python_requires} - for r in binary_python_requires.values(): - existing = expected_python_requires.get(r.name) - if not existing or r != existing: - self.python_requires_diff.setdefault("expected", set()).add(repr(existing)) - self.python_requires_diff.setdefault("existing", set()).add(repr(r)) - - # Confs - self.confs_diff = {} - binary_confs = binary_config.get("conf", {}) - expected_confs = expected_config.get("conf", {}) + def calculate_diff(self, binary_confs, expected_confs, output, reverse=False): for k, v in expected_confs.items(): value = binary_confs.get(k) if value != v: - self.confs_diff.setdefault("expected", []).append(f"{k}={v}") - self.confs_diff.setdefault("existing", []).append(f"{k}={value}") - for k, v in binary_confs.items(): - value = expected_confs.get(k) - if value != v: - self.confs_diff.setdefault("expected", []).append(f"{k}={value}") - self.confs_diff.setdefault("existing", []).append(f"{k}={v}") + output.setdefault("expected", []).append(f"{k}={v}") + output.setdefault("existing", []).append(f"{k}={value}") + if reverse: + for k, v in binary_confs.items(): + value = expected_confs.get(k) + if value != v: + output.setdefault("expected", []).append(f"{k}={value}") + output.setdefault("existing", []).append(f"{k}={v}") def __lt__(self, other): return self.distance < other.distance From 929fd5316f4c78fc15e7e2d475108652cdac1398 Mon Sep 17 00:00:00 2001 From: memsharded Date: Tue, 9 Jan 2024 11:17:18 +0100 Subject: [PATCH 6/7] review --- conan/api/subapi/list.py | 62 ++++++++++--------- .../command_v2/test_graph_find_binaries.py | 39 ++++++------ 2 files changed, 54 insertions(+), 47 deletions(-) diff --git a/conan/api/subapi/list.py b/conan/api/subapi/list.py index d59f0b5b82b..3e217be367f 100644 --- a/conan/api/subapi/list.py +++ b/conan/api/subapi/list.py @@ -239,63 +239,67 @@ def __init__(self, pref, binary_config, expected_config, remote=None): diff.setdefault("existing", []).append(f"{k}={value}") self.settings_target_diff = {} - self.calculate_diff(binary_config.get("settings_target", {}), - expected_config.get("settings_target", {}), - self.settings_target_diff) + self._calculate_diff(binary_config.get("settings_target", {}), + expected_config.get("settings_target", {}), + self.settings_target_diff) self.options_diff = {} - self.calculate_diff(binary_config.get("options", {}), - expected_config.get("options", {}), - self.options_diff) + self._calculate_diff(binary_config.get("options", {}), + expected_config.get("options", {}), + self.options_diff) self.deps_diff = {} - self.calculate_requirement_diff(binary_config.get("requires", []), - expected_config.get("requires", []), - self.deps_diff) + self._calculate_requirement_diff(binary_config.get("requires", []), + expected_config.get("requires", []), + self.deps_diff) self.build_requires_diff = {} - self.calculate_requirement_diff(binary_config.get("build_requires", []), - expected_config.get("build_requires", []), - self.build_requires_diff) + self._calculate_requirement_diff(binary_config.get("build_requires", []), + expected_config.get("build_requires", []), + self.build_requires_diff) self.python_requires_diff = {} - self.calculate_requirement_diff(binary_config.get("python_requires", []), - expected_config.get("python_requires", []), - self.python_requires_diff) + self._calculate_requirement_diff(binary_config.get("python_requires", []), + expected_config.get("python_requires", []), + self.python_requires_diff) self.confs_diff = {} - self.calculate_diff(binary_config.get("conf", {}), - expected_config.get("conf", {}), - self.confs_diff, - reverse=True) + self._calculate_diff(binary_config.get("conf", {}), + expected_config.get("conf", {}), + self.confs_diff) - def calculate_requirement_diff(self, binary_requires, expected_requires, output): + @staticmethod + def _calculate_requirement_diff(binary_requires, expected_requires, output): binary_requires = [RecipeReference.loads(r) for r in binary_requires] expected_requires = [RecipeReference.loads(r) for r in expected_requires] binary_requires = {r.name: r for r in binary_requires} for r in expected_requires: existing = binary_requires.get(r.name) if not existing or r != existing: - output.setdefault("expected", set()).add(repr(r)) - output.setdefault("existing", set()).add(repr(existing)) + output.setdefault("expected", []).append(repr(r)) + output.setdefault("existing", []).append(repr(existing)) expected_requires = {r.name: r for r in expected_requires} for r in binary_requires.values(): existing = expected_requires.get(r.name) if not existing or r != existing: - output.setdefault("expected", set()).add(repr(existing)) - output.setdefault("existing", set()).add(repr(r)) + if repr(existing) not in output.get("expected", ()): + output.setdefault("expected", []).append(repr(existing)) + if repr(r) not in output.get("existing", ()): + output.setdefault("existing", []).append(repr(r)) - def calculate_diff(self, binary_confs, expected_confs, output, reverse=False): + @staticmethod + def _calculate_diff(binary_confs, expected_confs, output): for k, v in expected_confs.items(): value = binary_confs.get(k) if value != v: output.setdefault("expected", []).append(f"{k}={v}") output.setdefault("existing", []).append(f"{k}={value}") - if reverse: - for k, v in binary_confs.items(): - value = expected_confs.get(k) - if value != v: + for k, v in binary_confs.items(): + value = expected_confs.get(k) + if value != v: + if f"{k}={value}" not in output.get("expected", ()): output.setdefault("expected", []).append(f"{k}={value}") + if f"{k}={v}" not in output.get("existing", ()): output.setdefault("existing", []).append(f"{k}={v}") def __lt__(self, other): diff --git a/conans/test/integration/command_v2/test_graph_find_binaries.py b/conans/test/integration/command_v2/test_graph_find_binaries.py index cdd6bc8027b..dfeb5a06032 100644 --- a/conans/test/integration/command_v2/test_graph_find_binaries.py +++ b/conans/test/integration/command_v2/test_graph_find_binaries.py @@ -8,7 +8,7 @@ class TestFilterProfile: - @pytest.fixture(scope="class") + @pytest.fixture() def client(self): c = TestClient() c.save({"lib/conanfile.py": GenConanfile("lib", "1.0").with_settings("os", "build_type") @@ -169,28 +169,29 @@ def test_different_conf(self, client): explanation: This binary has same settings, options and dependencies, but different confs """) assert textwrap.indent(expected, " ") in c.out - c.run("graph explain --requires=lib/1.0 -pr macos --format=json") + c.run("graph explain --requires=lib/1.0 -c user.foo:bar=42 -s os=Linux -f=json") cache = json.loads(c.stdout)["closest_binaries"] revisions = cache["lib/1.0"]["revisions"] pkgs = revisions["5313a980ea0c56baeb582c510d6d9fbc"]["packages"] assert len(pkgs) == 1 - pkg1 = pkgs["c2dd2d51b5074bdb5b7d717929372de09830017b"] - assert pkg1["diff"]["platform"] == {'existing': ['os=Windows'], 'expected': ['os=Macos']} + pkg1 = pkgs["499989797d9192081b8f16f7d797b107a2edd8da"] + assert pkg1["diff"]["platform"] == {} assert pkg1["diff"]["settings"] == {} assert pkg1["diff"]["options"] == {} assert pkg1["diff"]["dependencies"] == {} - assert pkg1["diff"]["explanation"] == "This binary belongs to another OS or Architecture, " \ - "highly incompatible." + assert pkg1["diff"]["confs"] == {"expected": ["user.foo:bar=42"], + "existing": ["user.foo:bar=None"]} + assert pkg1["diff"]["explanation"] == "This binary has same settings, options and " \ + "dependencies, but different confs" def test_different_python_requires(self): c = TestClient(light=True) c.save({"tool/conanfile.py": GenConanfile("tool"), - "lib/conanfile.py": GenConanfile("lib", "1.0") - .with_python_requires("tool/[>=1.0]")}) - c.run("create tool --version=1.0 -s os=Linux") - c.run("create lib -s os=Linux") - c.run("create tool --version=2.0 -s os=Linux") - c.run("graph explain --requires=lib/1.0 -s os=Linux") + "lib/conanfile.py": GenConanfile("lib", "1.0").with_python_requires("tool/[>=1.0]")}) + c.run("create tool --version=1.0") + c.run("create lib") + c.run("create tool --version=2.0") + c.run("graph explain --requires=lib/1.0") expected = textwrap.dedent("""\ remote: Local Cache python_requires: tool/1.0.Z @@ -204,19 +205,21 @@ def test_different_python_requires(self): c.run("graph explain --requires=lib/1.0 -s os=Linux --format=json") cache = json.loads(c.stdout)["closest_binaries"] revisions = cache["lib/1.0"]["revisions"] - pkgs = revisions["5313a980ea0c56baeb582c510d6d9fbc"]["packages"] + pkgs = revisions["7bf17caa5bf9d2ed1dd8b337e9623fc0"]["packages"] assert len(pkgs) == 1 - pkg1 = pkgs["c2dd2d51b5074bdb5b7d717929372de09830017b"] - assert pkg1["diff"]["platform"] == {'existing': ['os=Windows'], 'expected': ['os=Macos']} + pkg1 = pkgs["5ccdb706197ca94edc0ecee9ef0d0b11b887d937"] + assert pkg1["diff"]["platform"] == {} assert pkg1["diff"]["settings"] == {} assert pkg1["diff"]["options"] == {} assert pkg1["diff"]["dependencies"] == {} - assert pkg1["diff"]["explanation"] == "This binary belongs to another OS or Architecture, " \ - "highly incompatible." + assert pkg1["diff"]["python_requires"] == {"expected": ["tool/2.0.Z"], + "existing": ["tool/1.0.Z"]} + assert pkg1["diff"]["explanation"] == "This binary has same settings, options and " \ + "dependencies, but different python_requires" class TestMissingBinaryDeps: - @pytest.fixture(scope="class") + @pytest.fixture() def client(self): c = TestClient() c.save({"dep/conanfile.py": GenConanfile("dep").with_settings("os"), From 0e5a0a0fc2887742eabe80985b740b0b78676e1d Mon Sep 17 00:00:00 2001 From: memsharded Date: Wed, 10 Jan 2024 01:14:42 +0100 Subject: [PATCH 7/7] review and tests --- conan/api/subapi/list.py | 74 +++--- conan/cli/commands/graph.py | 14 +- .../command_v2/test_graph_find_binaries.py | 218 ++++++++++++++---- 3 files changed, 203 insertions(+), 103 deletions(-) diff --git a/conan/api/subapi/list.py b/conan/api/subapi/list.py index 3e217be367f..343992ac0a2 100644 --- a/conan/api/subapi/list.py +++ b/conan/api/subapi/list.py @@ -221,55 +221,35 @@ def explain_missing_binaries(self, ref, conaninfo, remotes): class _BinaryDistance: - def __init__(self, pref, binary_config, expected_config, remote=None): + def __init__(self, pref, binary, expected, remote=None): self.remote = remote self.pref = pref - self.binary_config = binary_config + self.binary_config = binary # Settings, special handling for os/arch - self.platform_diff = {} - self.settings_diff = {} - binary_settings = binary_config.get("settings", {}) - expected_settings = expected_config.get("settings", {}) - for k, v in expected_settings.items(): - value = binary_settings.get(k) - if value is not None and value != v: - diff = self.platform_diff if k in ("os", "arch") else self.settings_diff - diff.setdefault("expected", []).append(f"{k}={v}") - diff.setdefault("existing", []).append(f"{k}={value}") - - self.settings_target_diff = {} - self._calculate_diff(binary_config.get("settings_target", {}), - expected_config.get("settings_target", {}), - self.settings_target_diff) - - self.options_diff = {} - self._calculate_diff(binary_config.get("options", {}), - expected_config.get("options", {}), - self.options_diff) - - self.deps_diff = {} - self._calculate_requirement_diff(binary_config.get("requires", []), - expected_config.get("requires", []), - self.deps_diff) - - self.build_requires_diff = {} - self._calculate_requirement_diff(binary_config.get("build_requires", []), - expected_config.get("build_requires", []), - self.build_requires_diff) - - self.python_requires_diff = {} - self._calculate_requirement_diff(binary_config.get("python_requires", []), - expected_config.get("python_requires", []), - self.python_requires_diff) - - self.confs_diff = {} - self._calculate_diff(binary_config.get("conf", {}), - expected_config.get("conf", {}), - self.confs_diff) + binary_settings = binary.get("settings", {}) + expected_settings = expected.get("settings", {}) + + platform = {k: v for k, v in binary_settings.items() if k in ("os", "arch")} + expected_platform = {k: v for k, v in expected_settings.items() if k in ("os", "arch")} + self.platform_diff = self._calculate_diff(platform, expected_platform) + + binary_settings = {k: v for k, v in binary_settings.items() if k not in ("os", "arch")} + expected_settings = {k: v for k, v in expected_settings.items() if k not in ("os", "arch")} + self.settings_diff = self._calculate_diff(binary_settings, expected_settings) + + self.settings_target_diff = self._calculate_diff(binary, expected, "settings_target") + self.options_diff = self._calculate_diff(binary, expected, "options") + self.deps_diff = self._requirement_diff(binary, expected, "requires") + self.build_requires_diff = self._requirement_diff(binary, expected, "build_requires") + self.python_requires_diff = self._requirement_diff(binary, expected, "python_requires") + self.confs_diff = self._calculate_diff(binary, expected, "conf") @staticmethod - def _calculate_requirement_diff(binary_requires, expected_requires, output): + def _requirement_diff(binary_requires, expected_requires, item): + binary_requires = binary_requires.get(item, {}) + expected_requires = expected_requires.get(item, {}) + output = {} binary_requires = [RecipeReference.loads(r) for r in binary_requires] expected_requires = [RecipeReference.loads(r) for r in expected_requires] binary_requires = {r.name: r for r in binary_requires} @@ -286,9 +266,14 @@ def _calculate_requirement_diff(binary_requires, expected_requires, output): output.setdefault("expected", []).append(repr(existing)) if repr(r) not in output.get("existing", ()): output.setdefault("existing", []).append(repr(r)) + return output @staticmethod - def _calculate_diff(binary_confs, expected_confs, output): + def _calculate_diff(binary_confs, expected_confs, item=None): + if item is not None: + binary_confs = binary_confs.get(item, {}) + expected_confs = expected_confs.get(item, {}) + output = {} for k, v in expected_confs.items(): value = binary_confs.get(k) if value != v: @@ -301,6 +286,7 @@ def _calculate_diff(binary_confs, expected_confs, output): output.setdefault("expected", []).append(f"{k}={value}") if f"{k}={v}" not in output.get("existing", ()): output.setdefault("existing", []).append(f"{k}={v}") + return output def __lt__(self, other): return self.distance < other.distance diff --git a/conan/cli/commands/graph.py b/conan/cli/commands/graph.py index 9c06c567bca..d9c773a00f5 100644 --- a/conan/cli/commands/graph.py +++ b/conan/cli/commands/graph.py @@ -255,17 +255,15 @@ def graph_explain(conan_api, parser, subparser, *args): # compute ref and conaninfo missing = args.missing for node in deps_graph.ordered_iterate(): - if node.binary == BINARY_MISSING: - if not missing or ref_matches(node.ref, missing, is_consumer=None): - ref = node.ref - conaninfo = node.conanfile.info - break + if ((not missing and node.binary == BINARY_MISSING) # First missing binary or + or (missing and ref_matches(node.ref, missing, is_consumer=None))): # specified one + ref = node.ref + conaninfo = node.conanfile.info + break else: raise ConanException("There is no missing binary") pkglist = conan_api.list.explain_missing_binaries(ref, conaninfo, remotes) ConanOutput().title("Closest binaries") - return { - "closest_binaries": pkglist.serialize(), - } + return {"closest_binaries": pkglist.serialize()} diff --git a/conans/test/integration/command_v2/test_graph_find_binaries.py b/conans/test/integration/command_v2/test_graph_find_binaries.py index dfeb5a06032..5c92ee87a78 100644 --- a/conans/test/integration/command_v2/test_graph_find_binaries.py +++ b/conans/test/integration/command_v2/test_graph_find_binaries.py @@ -18,11 +18,9 @@ def client(self): c.run("create lib -s os=Windows -o *:shared=True") return c - def test_settings_exact_match_incomplete(self, client): - # We find 2 exact matches for os=Windows + def test_exact_match(self, client): c = client - c.save({"windows": "[settings]\nos=Windows"}) - c.run("graph explain --requires=lib/1.0 -pr windows") + c.run("graph explain --requires=lib/1.0 --missing=lib/1.0 -s os=Windows") expected = textwrap.dedent("""\ settings: Windows, Release options: shared=False @@ -30,7 +28,7 @@ def test_settings_exact_match_incomplete(self, client): explanation: This binary is an exact match for the defined inputs """) assert textwrap.indent(expected, " ") in c.out - c.run("graph explain --requires=lib/1.0 -pr windows --format=json") + c.run("graph explain --requires=lib/1.0 --missing=lib/1.0 -s os=Windows --format=json") cache = json.loads(c.stdout)["closest_binaries"] revisions = cache["lib/1.0"]["revisions"] pkgs = revisions["5313a980ea0c56baeb582c510d6d9fbc"]["packages"] @@ -42,16 +40,18 @@ def test_settings_exact_match_incomplete(self, client): assert pkg1["diff"]["dependencies"] == {} assert pkg1["diff"]["explanation"] == "This binary is an exact match for the defined inputs" - def test_settings_exact_match_complete(self, client): - # We find 2 exact matches for os=Windows shared=True + def test_settings_incomplete(self, client): c = client - c.save({"windows": "[settings]\nos=Windows\n[options]\n*:shared=True"}) + c.save({"windows": "[settings]\nos=Windows"}) c.run("graph explain --requires=lib/1.0 -pr windows") expected = textwrap.dedent("""\ settings: Windows, Release - options: shared=True + options: shared=False diff - explanation: This binary is an exact match for the defined inputs + settings + expected: build_type=None + existing: build_type=Release + explanation: This binary was built with different settings. """) assert textwrap.indent(expected, " ") in c.out c.run("graph explain --requires=lib/1.0 -pr windows --format=json") @@ -59,17 +59,33 @@ def test_settings_exact_match_complete(self, client): revisions = cache["lib/1.0"]["revisions"] pkgs = revisions["5313a980ea0c56baeb582c510d6d9fbc"]["packages"] assert len(pkgs) == 1 - pkg1 = pkgs["c2dd2d51b5074bdb5b7d717929372de09830017b"] + pkg1 = pkgs["3d714b452400b3c3d6a964f42d5ec5004a6f22dc"] assert pkg1["diff"]["platform"] == {} - assert pkg1["diff"]["settings"] == {} + assert pkg1["diff"]["settings"] == {'existing': ['build_type=Release'], + 'expected': ['build_type=None']} assert pkg1["diff"]["options"] == {} assert pkg1["diff"]["dependencies"] == {} - assert pkg1["diff"]["explanation"] == "This binary is an exact match for the defined inputs" + assert pkg1["diff"]["explanation"] == "This binary was built with different settings." + + def test_settings_with_option(self, client): + c = client + c.save({"windows": "[settings]\nos=Windows\n[options]\n*:shared=True"}) + c.run("graph explain --requires=lib/1.0 -pr windows") + expected = textwrap.dedent("""\ + settings: Windows, Release + options: shared=True + diff + settings + expected: build_type=None + existing: build_type=Release + explanation: This binary was built with different settings. + """) + assert textwrap.indent(expected, " ") in c.out def test_different_option(self, client): # We find 1 closest match in Linux static c = client - c.save({"linux": "[settings]\nos=Linux\n[options]\n*:shared=True"}) + c.save({"linux": "[settings]\nos=Linux\nbuild_type=Release\n[options]\n*:shared=True"}) c.run("graph explain --requires=lib/1.0 -pr linux") expected = textwrap.dedent("""\ remote: Local Cache @@ -95,6 +111,36 @@ def test_different_option(self, client): assert pkg1["diff"]["explanation"] == "This binary was built with the same settings, " \ "but different options" + def test_different_option_none(self): + # We find 1 closest match in Linux static + c = TestClient() + c.save({"conanfile.py": GenConanfile("lib", "1.0").with_option("opt", [None, 1])}) + c.run("create .") + c.run("graph explain --requires=lib/1.0 -o *:opt=1") + expected = textwrap.dedent("""\ + remote: Local Cache + diff + options + expected: opt=1 + existing: opt=None + explanation: This binary was built with the same settings, but different options + """) + assert textwrap.indent(expected, " ") in c.out + + c.run("remove * -c") + c.run("create . -o *:opt=1") + c.run("graph explain --requires=lib/1.0") + expected = textwrap.dedent("""\ + remote: Local Cache + options: opt=1 + diff + options + expected: opt=None + existing: opt=1 + explanation: This binary was built with the same settings, but different options + """) + assert textwrap.indent(expected, " ") in c.out + def test_different_setting(self, client): # We find 1 closest match in Linux static c = client @@ -124,6 +170,44 @@ def test_different_setting(self, client): assert pkg1["diff"]["dependencies"] == {} assert pkg1["diff"]["explanation"] == "This binary was built with different settings." + def test_different_settings_target(self): + c = TestClient() + conanfile = textwrap.dedent("""\ + from conan import ConanFile + class Pkg(ConanFile): + name = "tool" + version = "1.0" + def package_id(self): + self.info.settings_target = self.settings_target.copy() + self.info.settings_target.constrained(["os"]) + """) + c.save({"conanfile.py": conanfile}) + c.run("create . --build-require -s:b os=Windows -s:h os=Linux") + + c.run("graph explain --tool-requires=tool/1.0 -s:b os=Windows -s:h os=Macos") + expected = textwrap.dedent("""\ + remote: Local Cache + settings_target: os=Linux + diff + settings_target + expected: os=Macos + existing: os=Linux + explanation: This binary was built with different settings_target. + """) + assert textwrap.indent(expected, " ") in c.out + c.run("graph explain --tool-requires=tool/1.0 -s:b os=Windows -s:h os=Macos --format=json") + cache = json.loads(c.stdout)["closest_binaries"] + revisions = cache["tool/1.0"]["revisions"] + pkgs = revisions["4cc4b286a46dc2ed188d8c417eadb4e6"]["packages"] + assert len(pkgs) == 1 + pkg1 = pkgs["d66135125c07cc240b8d6adda090b76d60341205"] + assert pkg1["diff"]["platform"] == {} + assert pkg1["diff"]["settings_target"] == {'existing': ['os=Linux'], + 'expected': ['os=Macos']} + assert pkg1["diff"]["options"] == {} + assert pkg1["diff"]["dependencies"] == {} + assert pkg1["diff"]["explanation"] == "This binary was built with different settings_target." + def test_different_platform(self, client): # We find closest match in other platforms c = client @@ -184,39 +268,6 @@ def test_different_conf(self, client): assert pkg1["diff"]["explanation"] == "This binary has same settings, options and " \ "dependencies, but different confs" - def test_different_python_requires(self): - c = TestClient(light=True) - c.save({"tool/conanfile.py": GenConanfile("tool"), - "lib/conanfile.py": GenConanfile("lib", "1.0").with_python_requires("tool/[>=1.0]")}) - c.run("create tool --version=1.0") - c.run("create lib") - c.run("create tool --version=2.0") - c.run("graph explain --requires=lib/1.0") - expected = textwrap.dedent("""\ - remote: Local Cache - python_requires: tool/1.0.Z - diff - python_requires - expected: tool/2.0.Z - existing: tool/1.0.Z - explanation: This binary has same settings, options and dependencies, but different python_requires - """) - assert textwrap.indent(expected, " ") in c.out - c.run("graph explain --requires=lib/1.0 -s os=Linux --format=json") - cache = json.loads(c.stdout)["closest_binaries"] - revisions = cache["lib/1.0"]["revisions"] - pkgs = revisions["7bf17caa5bf9d2ed1dd8b337e9623fc0"]["packages"] - assert len(pkgs) == 1 - pkg1 = pkgs["5ccdb706197ca94edc0ecee9ef0d0b11b887d937"] - assert pkg1["diff"]["platform"] == {} - assert pkg1["diff"]["settings"] == {} - assert pkg1["diff"]["options"] == {} - assert pkg1["diff"]["dependencies"] == {} - assert pkg1["diff"]["python_requires"] == {"expected": ["tool/2.0.Z"], - "existing": ["tool/1.0.Z"]} - assert pkg1["diff"]["explanation"] == "This binary has same settings, options and " \ - "dependencies, but different python_requires" - class TestMissingBinaryDeps: @pytest.fixture() @@ -266,6 +317,73 @@ def test_other_dependencies(self, client): """) assert textwrap.indent(expected, " ") in c.out + def test_different_python_requires(self): + c = TestClient(light=True) + c.save({"tool/conanfile.py": GenConanfile("tool"), + "lib/conanfile.py": GenConanfile("lib", "1.0").with_python_requires("tool/[>=1.0]")}) + c.run("create tool --version=1.0") + c.run("create lib") + c.run("create tool --version=2.0") + c.run("graph explain --requires=lib/1.0") + expected = textwrap.dedent("""\ + remote: Local Cache + python_requires: tool/1.0.Z + diff + python_requires + expected: tool/2.0.Z + existing: tool/1.0.Z + explanation: This binary has same settings, options and dependencies, but different python_requires + """) + assert textwrap.indent(expected, " ") in c.out + c.run("graph explain --requires=lib/1.0 -s os=Linux --format=json") + cache = json.loads(c.stdout)["closest_binaries"] + revisions = cache["lib/1.0"]["revisions"] + pkgs = revisions["7bf17caa5bf9d2ed1dd8b337e9623fc0"]["packages"] + assert len(pkgs) == 1 + pkg1 = pkgs["5ccdb706197ca94edc0ecee9ef0d0b11b887d937"] + assert pkg1["diff"]["platform"] == {} + assert pkg1["diff"]["settings"] == {} + assert pkg1["diff"]["options"] == {} + assert pkg1["diff"]["dependencies"] == {} + assert pkg1["diff"]["python_requires"] == {"expected": ["tool/2.0.Z"], + "existing": ["tool/1.0.Z"]} + assert pkg1["diff"]["explanation"] == "This binary has same settings, options and " \ + "dependencies, but different python_requires" + + def test_build_requires(self): + c = TestClient(light=True) + c.save_home({"global.conf": "core.package_id:default_build_mode=minor_mode"}) + c.save({"tool/conanfile.py": GenConanfile("tool"), + "lib/conanfile.py": GenConanfile("lib", "1.0").with_tool_requires("tool/[>=1.0]")}) + c.run("create tool --version=1.0") + c.run("create lib") + c.run("create tool --version=2.0") + c.run("graph explain --requires=lib/1.0") + expected = textwrap.dedent("""\ + remote: Local Cache + build_requires: tool/1.0.Z + diff + build_requires + expected: tool/2.0.Z + existing: tool/1.0.Z + explanation: This binary has same settings, options and dependencies, but different build_requires + """) + assert textwrap.indent(expected, " ") in c.out + c.run("graph explain --requires=lib/1.0 -s os=Linux --format=json") + cache = json.loads(c.stdout)["closest_binaries"] + revisions = cache["lib/1.0"]["revisions"] + pkgs = revisions["4790f7f1561b52be5d39f0bc8e9acbed"]["packages"] + assert len(pkgs) == 1 + pkg1 = pkgs["c9d96a611b8c819f35728d58d743f6a78a1b5942"] + assert pkg1["diff"]["platform"] == {} + assert pkg1["diff"]["settings"] == {} + assert pkg1["diff"]["options"] == {} + assert pkg1["diff"]["dependencies"] == {} + assert pkg1["diff"]["build_requires"] == {"expected": ["tool/2.0.Z"], + "existing": ["tool/1.0.Z"]} + assert pkg1["diff"]["explanation"] == "This binary has same settings, options and " \ + "dependencies, but different build_requires" + def test_change_in_package_type(): tc = TestClient(light=True) @@ -299,10 +417,8 @@ def test_conf_difference_shown(): tc = TestClient(light=True) tc.save({ "libc/conanfile.py": GenConanfile("libc", "1.0"), - "libb/conanfile.py": GenConanfile("libb", "1.0") - .with_requires("libc/1.0"), - "liba/conanfile.py": GenConanfile("liba", "1.0") - .with_requires("libb/1.0") + "libb/conanfile.py": GenConanfile("libb", "1.0").with_requires("libc/1.0"), + "liba/conanfile.py": GenConanfile("liba", "1.0").with_requires("libb/1.0") }) tc.save_home({"global.conf": "tools.info.package_id:confs=['user.foo:bar']"})