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

[develop2] lockfiles alias support #12525

Merged
merged 2 commits into from
Nov 15, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 3 additions & 0 deletions conans/client/graph/graph_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,9 @@ def _prepare_node(node, profile_host, profile_build, down_options):
def _initialize_requires(self, node, graph, graph_lock):
# Introduce the current requires to define overrides
# This is the first pass over one recipe requires
if hasattr(node.conanfile, "python_requires"):
graph.aliased.update(node.conanfile.python_requires.aliased)

if graph_lock is not None:
for require in node.conanfile.requires.values():
graph_lock.resolve_locked(node, require)
Expand Down
21 changes: 12 additions & 9 deletions conans/client/graph/python_requires.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class PyRequires(object):
""" this is the object that replaces the declared conanfile.py_requires"""
def __init__(self):
self._pyrequires = {} # {pkg-name: PythonRequire}
self.aliased = {} # To store the aliases of py_requires, necessary for lockfiles too

def all_refs(self):
return [r.ref for r in self._pyrequires.values()]
Expand Down Expand Up @@ -79,21 +80,24 @@ def load_py_requires(self, conanfile, loader, graph_lock, remotes):
def _resolve_py_requires(self, py_requires_refs, graph_lock, loader, remotes):
result = PyRequires()
for py_requires_ref in py_requires_refs:
py_requires_ref = self._resolve_ref(py_requires_ref, graph_lock, remotes)
py_requires_ref = RecipeReference.loads(py_requires_ref)
requirement = Requirement(py_requires_ref)
resolved_ref = self._resolve_ref(requirement, graph_lock, remotes)
try:
py_require = self._cached_py_requires[py_requires_ref]
py_require = self._cached_py_requires[resolved_ref]
except KeyError:
pyreq_conanfile = self._load_pyreq_conanfile(loader, graph_lock, py_requires_ref,
pyreq_conanfile = self._load_pyreq_conanfile(loader, graph_lock, resolved_ref,
remotes)
conanfile, module, new_ref, path, recipe_status, remote = pyreq_conanfile
py_require = PyRequire(module, conanfile, new_ref, path, recipe_status, remote)
self._cached_py_requires[py_requires_ref] = py_require
self._cached_py_requires[resolved_ref] = py_require
if requirement.alias:
# Remove the recipe_revision, to do the same in alias as normal requires
result.aliased[requirement.alias] = RecipeReference.loads(str(new_ref))
result.add_pyrequire(py_require)
return result

def _resolve_ref(self, py_requires_ref, graph_lock, remotes):
ref = RecipeReference.loads(py_requires_ref)
requirement = Requirement(ref)
def _resolve_ref(self, requirement, graph_lock, remotes):
if graph_lock:
graph_lock.resolve_locked_pyrequires(requirement)
ref = requirement.ref
Expand All @@ -102,8 +106,7 @@ def _resolve_ref(self, py_requires_ref, graph_lock, remotes):
if alias is not None:
ref = alias
else:
resolved_ref = self._range_resolver.resolve(requirement, "py_require", remotes)
ref = resolved_ref
ref = self._range_resolver.resolve(requirement, "py_require", remotes)
return ref

def _load_pyreq_conanfile(self, loader, graph_lock, ref, remotes):
Expand Down
57 changes: 36 additions & 21 deletions conans/model/graph_lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,13 @@ def __init__(self, deps_graph=None, lock_packages=False):
self._requires = _LockRequires()
self._python_requires = _LockRequires()
self._build_requires = _LockRequires()
self.alias = {} # TODO: Alias locking needs to be tested more
self._alias = {}
self.partial = False

if deps_graph is None:
return

self.update_lock(deps_graph, lock_packages)
self.alias = deps_graph.aliased

def update_lock(self, deps_graph, lock_packages=False):
for graph_node in deps_graph.nodes:
Expand All @@ -112,6 +111,8 @@ def update_lock(self, deps_graph, lock_packages=False):
else:
self._requires.add(graph_node.ref, pids)

self._alias.update(deps_graph.aliased)

self._requires.sort()
self._build_requires.sort()
self._python_requires.sort()
Expand Down Expand Up @@ -174,20 +175,31 @@ def deserialize(data):
if version and version != LOCKFILE_VERSION:
raise ConanException("This lockfile was created with an incompatible "
"version. Please regenerate the lockfile")
graph_lock._requires = _LockRequires.deserialize(data["requires"])
graph_lock._build_requires = _LockRequires.deserialize(data["build_requires"])
graph_lock._python_requires = _LockRequires.deserialize(data["python_requires"])
if "requires" in data:
graph_lock._requires = _LockRequires.deserialize(data["requires"])
if "build_requires" in data:
graph_lock._build_requires = _LockRequires.deserialize(data["build_requires"])
if "python_requires" in data:
graph_lock._python_requires = _LockRequires.deserialize(data["python_requires"])
if "alias" in data:
graph_lock._alias = {RecipeReference.loads(k): RecipeReference.loads(v)
for k, v in data["alias"].items()}
return graph_lock

def serialize(self):
""" returns the object serialized as a dict of plain python types
that can be converted to json
"""
return {"version": LOCKFILE_VERSION,
"requires": self._requires.serialize(),
"build_requires": self._build_requires.serialize(),
"python_requires": self._python_requires.serialize()
}
result = {"version": LOCKFILE_VERSION}
if self._requires:
result["requires"] = self._requires.serialize()
if self._build_requires:
result["build_requires"] = self._build_requires.serialize()
if self._python_requires:
result["python_requires"] = self._python_requires.serialize()
if self._alias:
result["alias"] = {repr(k): repr(v) for k, v in self._alias.items()}
return result

def resolve_locked(self, node, require):
if require.build or node.context == CONTEXT_BUILD:
Expand All @@ -205,12 +217,11 @@ def resolve_prev(self, node):
return prevs.get(node.package_id)

def _resolve(self, require, locked_refs):
ref = require.ref
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:
matches = [r for r in locked_refs if r.name == ref.name and r.user == ref.user and
r.channel == ref.channel]
for m in matches:
if m.version in version_range:
require.ref = m
Expand All @@ -221,18 +232,22 @@ def _resolve(self, require, locked_refs):
else:
alias = require.alias
if alias:
require.ref = self.alias.get(require.ref, require.ref)
elif require.ref.revision is None:
for r in locked_refs:
if r.name == ref.name and r.version == ref.version and r.user == ref.user and \
r.channel == ref.channel:
require.ref = r
locked_alias = self._alias.get(alias)
if locked_alias is not None:
require.ref = locked_alias
elif not self.partial:
raise ConanException(f"Requirement alias '{alias}' not in lockfile")
ref = require.ref
if ref.revision is None:
for m in matches:
if m.version == ref.version:
require.ref = m
break
else:
if not self.partial:
raise ConanException(f"Requirement '{ref}' not in lockfile")
else:
if ref not in locked_refs and not self.partial:
if ref not in matches and not self.partial:
raise ConanException(f"Requirement '{repr(ref)}' not in lockfile")

def resolve_locked_pyrequires(self, require):
Expand Down
145 changes: 145 additions & 0 deletions conans/test/integration/lockfile/test_lock_alias.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import os
import textwrap

import pytest

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


@pytest.mark.parametrize("requires", ["requires", "tool_requires"])
def test_conanfile_txt_deps_ranges(requires):
"""
conanfile.txt locking it dependencies (with version ranges) using alias
"""
client = TestClient()
client.save({"pkg/conanfile.py": GenConanfile("pkg"),
"consumer/conanfile.txt": f"[{requires}]\npkg/(latest)"})
client.run("create pkg --version=0.1")
client.run("create pkg --version=0.2")
with client.chdir("alias"):
client.run("new alias -d name=pkg -d version=latest -d target=0.1")
client.run("export .")
client.run("lock create consumer/conanfile.txt")
assert "pkg/0.1" in client.out
assert '"pkg/latest": "pkg/0.1"' in client.load("consumer/conan.lock")

# Change the alias
with client.chdir("alias"):
client.run("new alias -d name=pkg -d version=latest -d target=0.2 -f")
client.run("export .")
client.run("install consumer/conanfile.txt") # use conan.lock by default
assert "pkg/0.1" in client.out
assert "pkg/0.2" not in client.out

os.remove(os.path.join(client.current_folder, "consumer/conan.lock"))
client.run("install consumer/conanfile.txt")
assert "pkg/0.2" in client.out
assert "pkg/0.1" not in client.out


@pytest.mark.parametrize("requires", ["requires", "tool_requires"])
def test_conanfile_txt_deps_ranges_lock_revisions(requires):
"""
conanfile.txt locking it dependencies (with version ranges)
"""
client = TestClient()
client.save({"pkg/conanfile.py": GenConanfile("pkg"),
"consumer/conanfile.txt": f"[{requires}]\npkg/(latest)"})
client.run("create pkg --version=0.1")
client.assert_listed_require({"pkg/0.1#a9ec2e5fbb166568d4670a9cd1ef4b26": "Cache"})
client.run("create pkg --version=0.2")
with client.chdir("alias"):
client.run("new alias -d name=pkg -d version=latest -d target=0.1")
client.run("export .")
client.run("lock create consumer/conanfile.txt")
assert "pkg/0.1#a9ec2e5fbb166568d4670a9cd1ef4b26" in client.out
assert '"pkg/latest": "pkg/0.1"' in client.load("consumer/conan.lock")

# Create a new revision
client.save({"pkg/conanfile.py": GenConanfile("pkg").with_class_attribute("potato=42")})
client.run("create pkg --version=0.1")
client.assert_listed_require({"pkg/0.1#8d60cd02b0b4aa8fe8b3cae32944c61b": "Cache"})
client.run("install consumer/conanfile.txt") # use conan.lock by default
assert "pkg/0.1#a9ec2e5fbb166568d4670a9cd1ef4b26" in client.out
assert "pkg/0.1#8d60cd02b0b4aa8fe8b3cae32944c61b" not in client.out

os.remove(os.path.join(client.current_folder, "consumer/conan.lock"))
client.run("install consumer/conanfile.txt")
assert "pkg/0.1#a9ec2e5fbb166568d4670a9cd1ef4b26" not in client.out
assert "pkg/0.1#8d60cd02b0b4aa8fe8b3cae32944c61b" in client.out


def test_alias_pyrequires():
"""
python_requires can also be aliased
"""
client = TestClient()
conanfile = textwrap.dedent("""
from conan import ConanFile
class Pkg(ConanFile):
python_requires = "pkg/(latest)"
""")
client.save({"pkg/conanfile.py": GenConanfile("pkg"),
"consumer/conanfile.py": conanfile})
client.run("export pkg --version=0.1")
client.run("export pkg --version=0.2")
with client.chdir("alias"):
client.run("new alias -d name=pkg -d version=latest -d target=0.1")
client.run("export .")
client.run("lock create consumer/conanfile.py")
assert "pkg/0.1" in client.out
assert '"pkg/latest": "pkg/0.1"' in client.load("consumer/conan.lock")

# Change the alias
with client.chdir("alias"):
client.run("new alias -d name=pkg -d version=latest -d target=0.2 -f")
client.run("export .")
client.run("install consumer/conanfile.py") # use conan.lock by default
assert "pkg/0.1" in client.out
assert "pkg/0.2" not in client.out

os.remove(os.path.join(client.current_folder, "consumer/conan.lock"))
client.run("install consumer/conanfile.py")
assert "pkg/0.2" in client.out
assert "pkg/0.1" not in client.out


def test_alias_pyrequires_revisions():
"""
python_requires can also be aliased, and revisions correctly resolved
"""
client = TestClient()
conanfile = textwrap.dedent("""
from conan import ConanFile
class Pkg(ConanFile):
python_requires = "pkg/(latest)"
""")
client.save({"pkg/conanfile.py": GenConanfile("pkg"),
"consumer/conanfile.py": conanfile})
client.run("export pkg --version=0.1")
assert "pkg/0.1#a9ec2e5fbb166568d4670a9cd1ef4b26" in client.out
client.run("export pkg --version=0.2")
with client.chdir("alias"):
client.run("new alias -d name=pkg -d version=latest -d target=0.1")
client.run("export .")
client.run("lock create consumer/conanfile.py")
assert "pkg/0.1" in client.out
assert '"pkg/latest": "pkg/0.1"' in client.load("consumer/conan.lock")

# Create a new revision
client.save({"pkg/conanfile.py": GenConanfile("pkg").with_class_attribute("potato=42")})
client.run("create pkg --version=0.1")
assert "pkg/0.1#8d60cd02b0b4aa8fe8b3cae32944c61b" in client.out

# Lockfile should force the previous revision
client.run("install consumer/conanfile.py") # use conan.lock by default
assert "pkg/0.1#a9ec2e5fbb166568d4670a9cd1ef4b26" in client.out
assert "pkg/0.1#8d60cd02b0b4aa8fe8b3cae32944c61b" not in client.out
assert "pkg/0.2" not in client.out

os.remove(os.path.join(client.current_folder, "consumer/conan.lock"))
client.run("install consumer/conanfile.py")
assert "pkg/0.1#a9ec2e5fbb166568d4670a9cd1ef4b26" not in client.out
assert "pkg/0.1#8d60cd02b0b4aa8fe8b3cae32944c61b" in client.out
assert "pkg/0.2" not in client.out