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

config of package_id default mode #4644

Merged
merged 9 commits into from
Mar 6, 2019
7 changes: 7 additions & 0 deletions conans/client/conf/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,13 @@ def revisions_enabled(self):
except ConanException:
return False

@property
def default_package_id_mode(self):
try:
return self.get_item("general.default_package_id_mode")
except ConanException:
return None
Copy link
Contributor

@lasote lasote Mar 5, 2019

Choose a reason for hiding this comment

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

Should the value be None or semver? Why not present in the conan.conf? I would like to see default_package_id_mode=semver explicit, always. In case someone doesn't have it, return "semver" instead of None

Copy link
Contributor

Choose a reason for hiding this comment

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

The values shouldn't contain the "_mode" suffix.

Copy link
Member

Choose a reason for hiding this comment

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

I would like to see the possible values as comments in the conan.conf too. I even like the idea of having a link to the docs section if we feel this feature is important enough...

Copy link
Member Author

Choose a reason for hiding this comment

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

It is different to not define a new default_package_id_mode, than defining it to semver. This is the logic:

# sha values
        if package_id_mode:
            try:
                getattr(self, package_id_mode)()
            except AttributeError:
                raise ConanException("'%s' is not a known package_id_mode" % package_id_mode)
        else:
            if indirect:
                self.unrelated_mode()
            else:
                self.semver_mode()

So if defaults to semver it will never apply the current unrelated_mode() that is applying to indirect (transitive, but not declared) dependencies, changing the current behavior.

Maybe we need to discuss this, but I thought that the new default mode would apply to all dependencies, transitive indirect dependencies too.

Copy link
Member

Choose a reason for hiding this comment

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

I really fail to see the whole picture now, so I will need a brief explanation


@property
def storage_path(self):
# Try with CONAN_STORAGE_PATH
Expand Down
13 changes: 7 additions & 6 deletions conans/client/graph/graph_binaries.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,15 +151,14 @@ def _evaluate_node(self, node, build_mode, update, evaluated_nodes, remote_name)
node.binary_remote = remote

@staticmethod
def _compute_package_id(node):
def _compute_package_id(node, default_package_id_mode):
conanfile = node.conanfile
neighbors = node.neighbors()
direct_reqs = [] # of PackageReference
indirect_reqs = set() # of PackageReference, avoid duplicates
for neighbor in neighbors:
ref, nconan = neighbor.ref, neighbor.conanfile
pref = PackageReference(ref, neighbor.package_id)
direct_reqs.append(pref)
direct_reqs.append(neighbor.pref)
indirect_reqs.update(nconan.info.requires.refs())
conanfile.options.propagate_downstream(ref, nconan.info.full_options)
# Might be never used, but update original requirement, just in case
Expand All @@ -175,13 +174,14 @@ def _compute_package_id(node):
conanfile.info = ConanInfo.create(conanfile.settings.values,
conanfile.options.values,
direct_reqs,
indirect_reqs)
indirect_reqs,
default_package_id_mode=default_package_id_mode)

# Once we are done, call package_id() to narrow and change possible values
with conanfile_exception_formatter(str(conanfile), "package_id"):
conanfile.package_id()

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

def _handle_private(self, node, deps_graph):
Expand All @@ -196,8 +196,9 @@ def _handle_private(self, node, deps_graph):

def evaluate_graph(self, deps_graph, build_mode, update, remote_name):
evaluated_nodes = {}
default_package_id_mode = self._cache.config.default_package_id_mode
for node in deps_graph.ordered_iterate():
self._compute_package_id(node)
self._compute_package_id(node, default_package_id_mode)
if node.recipe in (RECIPE_CONSUMER, RECIPE_VIRTUAL):
continue
self._evaluate_node(node, build_mode, update, evaluated_nodes, remote_name)
Expand Down
80 changes: 50 additions & 30 deletions conans/model/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,7 @@


class RequirementInfo(object):
def __init__(self, value_str, indirect=False):
""" parse the input into fields name, version...
"""
pref = PackageReference.loads(value_str)
def __init__(self, pref, indirect=False, default_package_id_mode=None):
self.package = pref
self.full_name = pref.ref.name
self.full_version = pref.ref.version
Expand All @@ -26,10 +23,25 @@ def __init__(self, value_str, indirect=False):
self.full_package_id = pref.id

# sha values
if indirect:
self.unrelated_mode()
if default_package_id_mode:
try:
getattr(self, default_package_id_mode)()
except AttributeError:
raise ConanException("'%s' is not a known package_id_mode" % default_package_id_mode)
else:
self.semver()
if indirect:
self.unrelated_mode()
else:
self.semver_mode()

danimtb marked this conversation as resolved.
Show resolved Hide resolved
def copy(self):
# Useful for build_id()
result = RequirementInfo(self.package)
for f in ("name", "version", "user", "channel", "package_id"):
setattr(result, f, getattr(self, f))
f = "full_%s" % f
setattr(result, f, getattr(self, f))
return result

def dumps(self):
if not self.name:
Expand All @@ -43,8 +55,9 @@ def dumps(self):

@property
def sha(self):
return "/".join([str(n) for n in [self.name, self.version, self.user, self.channel,
self.package_id]])
vals = [str(n) for n in (self.name, self.version, self.user, self.channel, self.package_id)]
# This is done later to NOT affect existing package-IDs (before revisions)
return "/".join(vals)

def unrelated_mode(self):
self.name = self.version = self.user = self.channel = self.package_id = None
Expand All @@ -54,7 +67,7 @@ def semver_mode(self):
self.version = self.full_version.stable()
self.user = self.channel = self.package_id = None

semver = semver_mode
semver = semver_mode # Remove Conan 2.0

def full_version_mode(self):
self.name = self.full_name
Expand Down Expand Up @@ -97,12 +110,16 @@ def full_package_mode(self):


class RequirementsInfo(object):
def __init__(self, requires):
def __init__(self, prefs, default_package_id_mode):
# {PackageReference: RequirementInfo}
self._data = {r: RequirementInfo(str(r)) for r in requires}
self._data = {pref: RequirementInfo(pref, default_package_id_mode=default_package_id_mode)
for pref in prefs}

def copy(self):
return RequirementsInfo(self._data.keys())
# For build_id() implementation
result = RequirementsInfo([], None)
result._data = {pref: req_info.copy() for pref, req_info in self._data.items()}
return result

def clear(self):
self._data = {}
Expand All @@ -111,12 +128,13 @@ def remove(self, *args):
for name in args:
del self._data[self._get_key(name)]

def add(self, indirect_reqs):
def add(self, prefs_indirect, default_package_id_mode):
""" necessary to propagate from upstream the real
package requirements
"""
for r in indirect_reqs:
self._data[r] = RequirementInfo(str(r), indirect=True)
for r in prefs_indirect:
self._data[r] = RequirementInfo(r, indirect=True,
default_package_id_mode=default_package_id_mode)

def refs(self):
""" used for updating downstream requirements with this
Expand Down Expand Up @@ -146,7 +164,8 @@ def sha(self):
# Remove requirements without a name, i.e. indirect transitive requirements
data = {k: v for k, v in self._data.items() if v.name}
for key in sorted(data):
result.append(data[key].sha)
s = data[key].sha
result.append(s)
return sha1('\n'.join(result).encode())

def dumps(self):
Expand Down Expand Up @@ -218,17 +237,17 @@ def copy(self):
return result

@staticmethod
def create(settings, options, requires, indirect_requires):
def create(settings, options, prefs_direct, prefs_indirect, default_package_id_mode):
result = ConanInfo()
result.full_settings = settings
result.settings = settings.copy()
result.full_options = options
result.options = options.copy()
result.options.clear_indirect()
result.full_requires = _PackageReferenceList(requires)
result.requires = RequirementsInfo(requires)
result.requires.add(indirect_requires)
result.full_requires.extend(indirect_requires)
result.full_requires = _PackageReferenceList(prefs_direct)
result.requires = RequirementsInfo(prefs_direct, default_package_id_mode)
result.requires.add(prefs_indirect, default_package_id_mode)
result.full_requires.extend(prefs_indirect)
result.recipe_hash = None
result.env_values = EnvValues()
result.vs_toolset_compatible()
Expand All @@ -239,6 +258,9 @@ def create(settings, options, requires, indirect_requires):

@staticmethod
def loads(text):
# This is used for search functionality, search prints info from this file
# Other use is from the BinariesAnalyzer, to get the recipe_hash and know
# if package is outdated
parser = ConfigParser(text, ["settings", "full_settings", "options", "full_options",
"requires", "full_requires", "scope", "recipe_hash", "env"],
raise_unexpected_field=False)
Expand All @@ -248,7 +270,8 @@ def loads(text):
result.options = OptionsValues.loads(parser.options)
result.full_options = OptionsValues.loads(parser.full_options)
result.full_requires = _PackageReferenceList.loads(parser.full_requires)
result.requires = RequirementsInfo(result.full_requires)
# Requires after load are not used for any purpose, CAN'T be used, they are not correct
result.requires = RequirementsInfo(result.full_requires, None)
result.recipe_hash = parser.recipe_hash or None

# TODO: Missing handling paring of requires, but not necessary now
Expand Down Expand Up @@ -308,18 +331,15 @@ def package_id(self):
""" The package_id of a conans is the sha1 of its specific requirements,
options and settings
"""
package_id = getattr(self, "_package_id", None)
if package_id:
return package_id

result = []
result.append(self.settings.sha)
# Only are valid requires for OPtions those Non-Dev who are still in requires
self.options.filter_used(self.requires.pkg_names)
result.append(self.options.sha)
result.append(self.requires.sha)
requires_sha = self.requires.sha
result.append(requires_sha)

package_id = sha1('\n'.join(result).encode())
self._package_id = package_id
return package_id

def serialize_min(self):
Expand All @@ -335,7 +355,7 @@ def serialize_min(self):
def header_only(self):
self.settings.clear()
self.options.clear()
self.requires.unrelated_mode()
self.requires.clear()

def vs_toolset_compatible(self):
"""Default behaviour, same package for toolset v140 with compiler=Visual Studio 15 than
Expand Down
103 changes: 103 additions & 0 deletions conans/test/functional/graph/graph_manager_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import os
import unittest

from mock import Mock

from conans.client.cache.cache import ClientCache
from conans.client.graph.graph_manager import GraphManager
from conans.client.graph.proxy import ConanProxy
from conans.client.graph.python_requires import ConanPythonRequire
from conans.client.graph.range_resolver import RangeResolver
from conans.client.installer import BinaryInstaller
from conans.client.loader import ConanFileLoader
from conans.client.recorder.action_recorder import ActionRecorder
from conans.model.graph_info import GraphInfo
from conans.model.manifest import FileTreeManifest
from conans.model.options import OptionsValues
from conans.model.profile import Profile
from conans.model.ref import ConanFileReference
from conans.test.unittests.model.transitive_reqs_test import MockSearchRemote
from conans.test.utils.conanfile import TestConanFile
from conans.test.utils.test_files import temp_folder
from conans.test.utils.tools import TestBufferConanOutput
from conans.util.files import save


class GraphManagerTest(unittest.TestCase):

def setUp(self):
self.output = TestBufferConanOutput()
cache_folder = temp_folder()
cache = ClientCache(cache_folder, os.path.join(cache_folder, ".conan"), self.output)
self.cache = cache
self.remote_search = MockSearchRemote()
remote_manager = None
self.resolver = RangeResolver(cache, self.remote_search)
proxy = ConanProxy(cache, self.output, remote_manager)
self.loader = ConanFileLoader(None, self.output, ConanPythonRequire(None, None))
self.manager = GraphManager(self.output, cache, remote_manager, self.loader, proxy,
self.resolver)
hook_manager = Mock()
recorder = Mock()
self.binary_installer = BinaryInstaller(cache, self.output, remote_manager, recorder,
hook_manager)

def _cache_recipe(self, reference, test_conanfile, revision=None):
if isinstance(test_conanfile, TestConanFile):
test_conanfile.info = True
ref = ConanFileReference.loads(reference)
save(self.cache.conanfile(ref), str(test_conanfile))
with self.cache.package_layout(ref).update_metadata() as metadata:
metadata.recipe.revision = revision or "123"
manifest = FileTreeManifest.create(self.cache.export(ref))
manifest.save(self.cache.export(ref))

def build_graph(self, content, profile_build_requires=None, ref=None):
path = temp_folder()
path = os.path.join(path, "conanfile.py")
save(path, str(content))
self.loader.cached_conanfiles = {}

profile = Profile()
if profile_build_requires:
profile.build_requires = profile_build_requires
profile.process_settings(self.cache)
update = check_updates = False
recorder = ActionRecorder()
remote_name = None
build_mode = []
ref = ref or ConanFileReference(None, None, None, None, validate=False)
options = OptionsValues()
graph_info = GraphInfo(profile, options, root_ref=ref)
deps_graph, _ = self.manager.load_graph(path, None, graph_info,
build_mode, check_updates, update,
remote_name, recorder)
self.binary_installer.install(deps_graph, False, graph_info)
return deps_graph

def _check_node(self, node, ref, deps, build_deps, dependents, closure, public_deps):
conanfile = node.conanfile
ref = ConanFileReference.loads(str(ref))
self.assertEqual(node.ref.full_repr(), ref.full_repr())
self.assertEqual(conanfile.name, ref.name)
self.assertEqual(len(node.dependencies), len(deps) + len(build_deps))
self.assertEqual(len(node.dependants), len(dependents))

public_deps = {n.name: n for n in public_deps}
self.assertEqual(node.public_deps, public_deps)

# The recipe requires is resolved to the reference WITH revision!
self.assertEqual(len(deps), len(conanfile.requires))
for dep in deps:
self.assertEqual(conanfile.requires[dep.name].ref,
dep.ref)

self.assertEqual(closure, node.public_closure)
libs = []
envs = []
for n in closure:
libs.append("mylib%s%slib" % (n.ref.name, n.ref.version))
envs.append("myenv%s%senv" % (n.ref.name, n.ref.version))
self.assertEqual(conanfile.deps_cpp_info.libs, libs)
env = {"MYENV": envs} if envs else {}
self.assertEqual(conanfile.deps_env_info.vars, env)
35 changes: 35 additions & 0 deletions conans/test/functional/graph/package_id_modes_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from conans.test.functional.graph.graph_manager_base import GraphManagerTest
from conans.test.utils.conanfile import TestConanFile


class PackageIDGraphTests(GraphManagerTest):

def test_recipe_modes(self):
configs = []
mode = "semver_mode"
memsharded marked this conversation as resolved.
Show resolved Hide resolved
configs.append((mode, "liba/1.1.1@user/testing", "8ecbf93ba63522ffb32573610c80ab4dcb399b52"))
configs.append((mode, "liba/1.1.2@user/testing", "8ecbf93ba63522ffb32573610c80ab4dcb399b52"))
configs.append((mode, "liba/1.2.1@other/stable", "8ecbf93ba63522ffb32573610c80ab4dcb399b52"))
mode = "patch_mode"
configs.append((mode, "liba/1.1.1@user/testing", "bd664570d5174c601d5d417bc19257c4dba48f2e"))
configs.append((mode, "liba/1.1.2@user/testing", "fb1f766173191d44b67156c6b9ac667422e99286"))
configs.append((mode, "liba/1.1.1@other/stable", "bd664570d5174c601d5d417bc19257c4dba48f2e"))
mode = "full_recipe_mode"
configs.append((mode, "liba/1.1.1@user/testing", "9cbe703e1dee73a2a6807f71d8551c5f1e1b08fd"))
configs.append((mode, "liba/1.1.2@user/testing", "42a9ff9024adabbd54849331cf01be7d95139948"))
configs.append((mode, "liba/1.1.1@user/stable", "b41d6c026473cffed4abded4b0eaa453497be1d2"))

for package_id_mode, ref, package_id in configs:
self.cache.config.set_item("general.default_package_id_mode", package_id_mode)
libb_ref = "libb/0.1@user/testing"
self._cache_recipe(ref, TestConanFile("liba", "0.1.1"))
self._cache_recipe(libb_ref, TestConanFile("libb", "0.1", requires=[ref]))
deps_graph = self.build_graph(TestConanFile("app", "0.1", requires=[libb_ref]))

self.assertEqual(3, len(deps_graph.nodes))
app = deps_graph.root
libb = app.dependencies[0].dst
liba = libb.dependencies[0].dst

self.assertEqual(liba.package_id, "5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9")
self.assertEqual(libb.package_id, package_id)
Loading