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

Add core.version_ranges:resolve_prereleases conf #13321

Merged
merged 17 commits into from
Mar 14, 2023
Merged
Show file tree
Hide file tree
Changes from 13 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/api/subapi/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ def load_graph(self, root_node, profile_host, profile_build, lockfile=None, remo
assert profile_build is not None

remotes = remotes or []
builder = DepsGraphBuilder(app.proxy, app.loader, app.range_resolver, remotes,
builder = DepsGraphBuilder(app.proxy, app.loader, app.range_resolver, app.cache, remotes,
update, check_update)
deps_graph = builder.load_graph(root_node, profile_host, profile_build, lockfile)
return deps_graph
Expand Down
4 changes: 3 additions & 1 deletion conans/client/conf/required_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ def validate_conan_version(required_range):
version_range = VersionRange(required_range)
for conditions in version_range.condition_sets:
conditions.prerelease = True
if clientver not in version_range:
# TODO: Think what's the better approach for resolving prereleases.
# Listening to the global conf does not sound useful here
if not version_range.contains(clientver, resolve_prerelease=None):
raise ConanException("Current Conan version ({}) does not satisfy "
"the defined one ({}).".format(clientver, required_range))

Expand Down
25 changes: 15 additions & 10 deletions conans/client/graph/graph_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,17 @@

class DepsGraphBuilder(object):

def __init__(self, proxy, loader, resolver, remotes, update, check_update):
def __init__(self, proxy, loader, resolver, cache, remotes, update, check_update):
self._proxy = proxy
self._loader = loader
self._resolver = resolver
self._cache = cache
self._remotes = remotes # TODO: pass as arg to load_graph()
self._update = update
self._check_update = check_update
self._resolve_prereleases = self._cache.new_config.get('core.version_ranges:'
'resolve_prereleases',
default=None, check_type=bool)

def load_graph(self, root_node, profile_host, profile_build, graph_lock=None):
assert profile_host is not None
Expand Down Expand Up @@ -82,7 +86,7 @@ def _expand_require(self, require, node, graph, profile_host, profile_build, gra
require.ref = prev_ref
else:
self._conflicting_version(require, node, prev_require, prev_node,
prev_ref, base_previous)
prev_ref, base_previous, self._resolve_prereleases)

if prev_node is None:
# new node, must be added and expanded (node -> new_node)
Expand All @@ -99,22 +103,22 @@ def _expand_require(self, require, node, graph, profile_host, profile_build, gra

@staticmethod
def _conflicting_version(require, node,
prev_require, prev_node, prev_ref, base_previous):
prev_require, prev_node, prev_ref, base_previous, resolve_prereleases):
version_range = require.version_range
prev_version_range = prev_require.version_range if prev_node is None else None
if version_range:
# TODO: Check user/channel conflicts first
if prev_version_range is not None:
pass # Do nothing, evaluate current as it were a fixed one
else:
if prev_ref.version in version_range:
if version_range.contains(prev_ref.version, resolve_prereleases):
require.ref = prev_ref
else:
raise GraphError.conflict(node, require, prev_node, prev_require, base_previous)

elif prev_version_range is not None:
# TODO: CHeck user/channel conflicts first
if require.ref.version not in prev_version_range:
# TODO: Check user/channel conflicts first
if not prev_version_range.contains(require.ref.version):
AbrilRBS marked this conversation as resolved.
Show resolved Hide resolved
raise GraphError.conflict(node, require, prev_node, prev_require, base_previous)
else:
def _conflicting_refs(ref1, ref2):
Expand Down Expand Up @@ -162,7 +166,7 @@ def _initialize_requires(self, node, graph, graph_lock):
# This is the first pass over one recipe requires
if graph_lock is not None:
for require in node.conanfile.requires.values():
graph_lock.resolve_locked(node, require)
graph_lock.resolve_locked(node, require, self._resolve_prereleases)

for require in node.conanfile.requires.values():
self._resolve_alias(node, require, graph)
Expand Down Expand Up @@ -215,7 +219,7 @@ def _resolve_recipe(self, ref, graph_lock):
return new_ref, dep_conanfile, recipe_status, remote

@staticmethod
def _resolved_system_tool(node, require, profile_build, profile_host):
def _resolved_system_tool(node, require, profile_build, profile_host, resolve_prereleases):
AbrilRBS marked this conversation as resolved.
Show resolved Hide resolved
if node.context == CONTEXT_HOST and not require.build: # Only for tool_requires
return
system_tool = profile_build.system_tools if node.context == CONTEXT_BUILD \
Expand All @@ -225,13 +229,14 @@ def _resolved_system_tool(node, require, profile_build, profile_host):
for d in system_tool:
if require.ref.name == d.name:
if version_range:
if d.version in version_range:
if version_range.contains(d.version, resolve_prereleases):
return d, ConanFile(str(d)), RECIPE_SYSTEM_TOOL, None
elif require.ref.version == d.version:
return d, ConanFile(str(d)), RECIPE_SYSTEM_TOOL, None

def _create_new_node(self, node, require, graph, profile_host, profile_build, graph_lock):
resolved = self._resolved_system_tool(node, require, profile_build, profile_host)
resolved = self._resolved_system_tool(node, require, profile_build, profile_host,
self._resolve_prereleases)

if resolved is None:
try:
Expand Down
17 changes: 11 additions & 6 deletions conans/client/graph/range_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ def __init__(self, conan_app):
self._cached_cache = {} # Cache caching of search result, so invariant wrt installations
self._cached_remote_found = {} # dict {ref (pkg/*): {remote_name: results (pkg/1, pkg/2)}}
self.resolved_ranges = {}
self._resolve_prereleases = self._cache.new_config.get('core.version_ranges'
':resolve_prereleases',
default=None, check_type=bool)

def resolve(self, require, base_conanref, remotes, update):
version_range = require.version_range
Expand Down Expand Up @@ -55,7 +58,7 @@ def _resolve_local(self, search_ref, version_range):
and ref.channel == search_ref.channel]
self._cached_cache[pattern] = local_found
if local_found:
return self._resolve_version(version_range, local_found)
return self._resolve_version(version_range, local_found, self._resolve_prereleases)

def _search_remote_recipes(self, remote, search_ref):
pattern = str(search_ref)
Expand All @@ -73,18 +76,20 @@ def _resolve_remote(self, search_ref, version_range, remotes, update):
update_candidates = []
for remote in remotes:
remote_results = self._search_remote_recipes(remote, search_ref)
resolved_version = self._resolve_version(version_range, remote_results)
resolved_version = self._resolve_version(version_range, remote_results,
self._resolve_prereleases)
if resolved_version:
if not update:
return resolved_version # Return first valid occurence in first remote
return resolved_version # Return first valid occurrence in first remote
else:
update_candidates.append(resolved_version)
if len(update_candidates) > 0: # pick latest from already resolved candidates
resolved_version = self._resolve_version(version_range, update_candidates)
resolved_version = self._resolve_version(version_range, update_candidates,
self._resolve_prereleases)
return resolved_version

@staticmethod
def _resolve_version(version_range, refs_found):
def _resolve_version(version_range, refs_found, resolve_prereleases):
for ref in reversed(sorted(refs_found)):
if ref.version in version_range:
if version_range.contains(ref.version, resolve_prereleases):
return ref
1 change: 1 addition & 0 deletions conans/model/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"core:default_profile": "Defines the default host profile ('default' by default)",
"core:default_build_profile": "Defines the default build profile (None by default)",
"core:allow_uppercase_pkg_names": "Temporarily (will be removed in 2.X) allow uppercase names",
"core.version_ranges:resolve_prereleases": "Whether version ranges can resolve to pre-releases or not",
"core.upload:retry": "Number of retries in case of failure when uploading to Conan server",
"core.upload:retry_wait": "Seconds to wait between upload attempts to Conan server",
"core.download:parallel": "Number of concurrent threads to download packages",
Expand Down
12 changes: 6 additions & 6 deletions conans/model/graph_lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,12 +201,12 @@ def serialize(self):
result["alias"] = {repr(k): repr(v) for k, v in self._alias.items()}
return result

def resolve_locked(self, node, require):
def resolve_locked(self, node, require, resolve_prereleases):
if require.build or node.context == CONTEXT_BUILD:
locked_refs = self._build_requires.refs()
else:
locked_refs = self._requires.refs()
self._resolve(require, locked_refs)
self._resolve(require, locked_refs, resolve_prereleases)

def resolve_prev(self, node):
if node.context == CONTEXT_BUILD:
Expand All @@ -216,14 +216,14 @@ def resolve_prev(self, node):
if prevs:
return prevs.get(node.package_id)

def _resolve(self, require, locked_refs):
def _resolve(self, require, locked_refs, resolve_prereleases):
version_range = require.version_range
ref = require.ref
matches = [r for r in locked_refs if r.name == ref.name and r.user == ref.user and
r.channel == ref.channel]
if version_range:
for m in matches:
if m.version in version_range:
if version_range.contains(m.version, resolve_prereleases):
require.ref = m
break
else:
Expand All @@ -250,6 +250,6 @@ def _resolve(self, require, locked_refs):
if ref not in matches and not self.partial:
raise ConanException(f"Requirement '{repr(ref)}' not in lockfile")

def resolve_locked_pyrequires(self, require):
def resolve_locked_pyrequires(self, require, resolve_prereleases=None):
locked_refs = self._python_requires.refs() # CHANGE
self._resolve(require, locked_refs)
self._resolve(require, locked_refs, resolve_prereleases)
26 changes: 21 additions & 5 deletions conans/model/version_range.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from collections import namedtuple
from typing import Optional

from conans.errors import ConanException
from conans.model.recipe_ref import Version
Expand Down Expand Up @@ -56,9 +57,14 @@ def first_non_zero(main):
else:
return [_Condition(operator, Version(version))]

def valid(self, version):
def _valid(self, version, conf_resolve_prepreleases):
if version.pre:
if not self.prerelease:
# Follow the expression desires only if core.version_ranges:resolve_prereleases is None,
# else force to the conf's value
if conf_resolve_prepreleases is None:
if not self.prerelease:
return False
elif conf_resolve_prepreleases is False:
return False
for condition in self.conditions:
if condition.operator == ">":
Expand All @@ -83,7 +89,7 @@ class VersionRange:
def __init__(self, expression):
self._expression = expression
tokens = expression.split(",")
prereleases = None
prereleases = False
for t in tokens[1:]:
if "include_prerelease" in t:
prereleases = True
Expand All @@ -96,9 +102,19 @@ def __init__(self, expression):
def __str__(self):
return self._expression

def __contains__(self, version):
def contains(self, version: Version, resolve_prerelease: Optional[bool]):
"""
Whether <version> is inside the version range

:param version: Version to check against
:param resolve_prerelease: If ``True``, ensure prereleases can be resolved in this range
If ``False``, prerelases can NOT be resolved in this range
If ``None``, prereleases are resolved only if this version range expression says so
:return: Whether the version is inside the range
"""
assert isinstance(version, Version), type(version)
for condition_set in self.condition_sets:
if condition_set.valid(version):
if condition_set._valid(version, resolve_prerelease):
return True
return False

108 changes: 108 additions & 0 deletions conans/test/integration/graph/version_ranges/version_range_conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
from utils.tools import TestClient
import textwrap


def test_version_range_conf_nonexplicit_expression():
tc = TestClient()

base = textwrap.dedent("""
from conan import ConanFile

class Base(ConanFile):
name = "base"
""")

tc.save({"base/conanfile.py": base})
tc.run("create base/conanfile.py --version=1.5.1")
tc.run("create base/conanfile.py --version=2.5.0-pre")

conanfilev1 = textwrap.dedent("""
from conan import ConanFile

class Package(ConanFile):
name = "pkg"
version = "1.0"
requires = "base/[>1 <2]"
""")
AbrilRBS marked this conversation as resolved.
Show resolved Hide resolved

conanfilev2 = textwrap.dedent("""
from conan import ConanFile

class Package(ConanFile):
name = "pkg"
version = "2.0"
requires = "base/[>2 <3]"
""")

tc.save({"v1/conanfile.py": conanfilev1, "v2/conanfile.py": conanfilev2})

tc.save({"global.conf": "core.version_ranges:resolve_prereleases=False"}, path=tc.cache.cache_folder)
tc.run("create v1/conanfile.py")
assert "base/[>1 <2]: base/1.5.1" in tc.out
tc.run("create v2/conanfile.py", assert_error=True)
assert "Package 'base/[>2 <3]' not resolved" in tc.out

tc.save({"global.conf": "core.version_ranges:resolve_prereleases=True"}, path=tc.cache.cache_folder)
tc.run("create v1/conanfile.py")
assert "base/[>1 <2]: base/1.5.1" in tc.out
tc.run("create v2/conanfile.py")
assert "base/[>2 <3]: base/2.5.0-pre" in tc.out

tc.save({"global.conf": "core.version_ranges:resolve_prereleases=None"}, path=tc.cache.cache_folder)
tc.run("create v1/conanfile.py")
assert "base/[>1 <2]: base/1.5.1" in tc.out
tc.run("create v2/conanfile.py")
assert "Package 'base/[>2 <3]' not resolved" in tc.out


def test_version_range_conf_explicit_expression():
tc = TestClient()

base = textwrap.dedent("""
from conan import ConanFile

class Base(ConanFile):
name = "base"
""")

tc.save({"base/conanfile.py": base})
tc.run("create base/conanfile.py --version=1.5.1")
tc.run("create base/conanfile.py --version=2.5.0-pre")

conanfilev1 = textwrap.dedent("""
from conan import ConanFile

class Package(ConanFile):
AbrilRBS marked this conversation as resolved.
Show resolved Hide resolved
name = "pkg"
version = "1.0"
requires = "base/[>1- <2]"
""")

conanfilev2 = textwrap.dedent("""
from conan import ConanFile

class Package(ConanFile):
name = "pkg"
version = "2.0"
requires = "base/[>2- <3]"
""")

tc.save({"v1/conanfile.py": conanfilev1, "v2/conanfile.py": conanfilev2})

tc.save({"global.conf": "core.version_ranges:resolve_prereleases=False"}, path=tc.cache.cache_folder)
tc.run("create v1/conanfile.py")
assert "base/[>1- <2]: base/1.5.1" in tc.out
tc.run("create v2/conanfile.py", assert_error=True)
assert "Package 'base/[>2- <3]' not resolved" in tc.out

tc.save({"global.conf": "core.version_ranges:resolve_prereleases=True"}, path=tc.cache.cache_folder)
tc.run("create v1/conanfile.py")
assert "base/[>1- <2]: base/1.5.1" in tc.out
tc.run("create v2/conanfile.py")
assert "base/[>2- <3]: base/2.5.0-pre" in tc.out

tc.save({"global.conf": "core.version_ranges:resolve_prereleases=None"}, path=tc.cache.cache_folder)
tc.run("create v1/conanfile.py")
assert "base/[>1- <2]: base/1.5.1" in tc.out
tc.run("create v2/conanfile.py")
assert "base/[>2- <3]: base/2.5.0-pre" in tc.out
Loading