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

Feature/lock upgrade #17577

Draft
wants to merge 5 commits into
base: develop2
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
102 changes: 102 additions & 0 deletions conan/cli/commands/lock.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from collections import defaultdict
import os

from conan.api.output import ConanOutput
Expand All @@ -8,6 +9,9 @@
from conan.cli.printers.graph import print_graph_packages, print_graph_basic
from conan.internal.model.lockfile import Lockfile, LOCKFILE
from conan.internal.model.recipe_ref import RecipeReference
from conan.errors import ConanException
from conan.internal.model.version import Version
from conan.internal.model.version_range import VersionRange


@conan_command(group="Consumer")
Expand Down Expand Up @@ -179,3 +183,101 @@ def lock_update(conan_api, parser, subparser, *args):
lockfile.update(requires=args.requires, build_requires=args.build_requires,
python_requires=args.python_requires, config_requires=args.config_requires)
conan_api.lockfile.save_lockfile(lockfile, args.lockfile_out)




@conan_subcommand()
def lock_upgrade(conan_api, parser, subparser, *args):
"""
(Experimental) Upgrade requires, build-requires or python-requires from an existing lockfile given a conanfile
or a reference.
"""

"""
TODOs:
- [ ] args.update should always be True?
- [x] manage input version ranges
- [ ] Consider a new option to enable transitive dependencies on different kinds
(eg. --transitive=all, --transitive=python-requires, ...)
- [ ] Improve the detection of range versions
"""

common_graph_args(subparser)
subparser.add_argument('--update-requires', action="append", help='Update requires from lockfile')
subparser.add_argument('--update-build-requires', action="append", help='Update build-requires from lockfile')
subparser.add_argument('--update-python-requires', action="append", help='Update python-requires from lockfile')
subparser.add_argument('--update-config-requires', action="append", help='Update config-requires from lockfile')
subparser.add_argument('--build-require', action='store_true', default=False, help='Whether the provided reference is a build-require')
subparser.add_argument('--transitives', action='store_true', default=False, help='Upgrade also transitive dependencies')
args = parser.parse_args(*args)

# parameter validation
validate_common_graph_args(args)

if not any([args.update_requires, args.update_build_requires, args.update_python_requires, args.update_config_requires]):
raise ConanException("At least one of --update-requires, --update-build-requires, "
"--update-python-requires or --update-config-requires should be specified")

cwd = os.getcwd()
path = conan_api.local.get_conanfile_path(args.path, cwd, py=None) if args.path else None
remotes = conan_api.remotes.list(args.remote) if not args.no_remote else []
overrides = eval(args.lockfile_overrides) if args.lockfile_overrides else None
lockfile = conan_api.lockfile.get_lockfile(lockfile=args.lockfile, conanfile_path=path,
cwd=cwd, partial=True, overrides=overrides)
profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args)

def expand_graph():
if path:
return conan_api.graph.load_graph_consumer(path, args.name, args.version,
args.user, args.channel,
profile_host, profile_build, lockfile,
remotes, args.update,
is_build_require=args.build_require)
return conan_api.graph.load_graph_requires(args.requires, args.tool_requires,
profile_host, profile_build, lockfile,
remotes, args.update)
requested_update = {"requires": args.update_requires or [],
"build_requires": args.update_build_requires or [],
"python_requires": args.update_python_requires or [],
"config_requires": args.update_config_requires or []}
if args.transitives:
def is_version_range(ref: str):
# TODO: use conan's built in
return ref.find('[') != -1 or ref.find('*') != -1

def match_version(ref, node):
if is_version_range(ref):
return VersionRange(str(RecipeReference.loads(ref).version)).contains(Version(node.ref), resolve_prerelease=True)
else:
return node.ref.matches(ref, is_consumer=None)
updatable_deps = defaultdict(list)
for node in expand_graph().nodes:
if not node.ref:
continue
for kind, deps in requested_update.items():
if any(match_version(ref, node) for ref in deps):
updatable_deps[kind].append(node.ref.repr_notime())
for dep in node.conanfile.dependencies.values():
# TODO: dependencies.build.values() ->
# dep.context # host or build require (?)
updatable_deps[kind].append(dep.ref.repr_notime())
else:
updatable_deps = requested_update
# Remove the lockfile entries that will be updated
lockfile = conan_api.lockfile.remove_lockfile(lockfile,
requires=updatable_deps["requires"],
python_requires=updatable_deps["python_requires"],
build_requires=updatable_deps["build_requires"],
config_requires=updatable_deps["config_requires"])
# Resolve new graph
graph = expand_graph()
print_graph_basic(graph)
graph.report_graph_error()
conan_api.graph.analyze_binaries(graph, args.build, remotes=remotes, update=args.update,
lockfile=lockfile)
print_graph_packages(graph)

lockfile = conan_api.lockfile.update_lockfile(lockfile, graph, args.lockfile_packages,
clean=args.lockfile_clean)
conan_api.lockfile.save_lockfile(lockfile, args.lockfile_out or "conan.lock")
171 changes: 170 additions & 1 deletion test/integration/lockfile/test_user_overrides.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import pytest

from conan.test.assets.genconanfile import GenConanfile
from conan.test.utils.tools import TestClient
from conan.test.utils.tools import NO_SETTINGS_PACKAGE_ID, TestClient


def test_user_overrides():
Expand Down Expand Up @@ -360,3 +360,172 @@ def test_lock_update(self, kind, old, new):
lock = c.load("conan.lock")
assert old not in lock
assert new in lock


class TestLockUpgrade:
@pytest.mark.parametrize("kind, pkg, old, new", [
("requires", "math", "math/1.0", "math/1.1"),
("build-requires", "cmake", "cmake/1.0", "cmake/1.1"), # TODO there is not a --build-requires
# ("python-requires", "mytool", "mytool/1.0", "mytool/1.1"), # TODO nor a --python-requires
])
def test_lock_upgrade(self, kind, pkg, old, new):
c = TestClient(light=True)
c.save({f"{pkg}/conanfile.py": GenConanfile(pkg)})

c.run(f"export {pkg} --version=1.0")
rev0 = c.exported_recipe_revision()
kind_create = "tool-requires" if "build-requires" == kind else kind
c.run(f"lock create --{kind_create}={pkg}/[*]")
lock = c.load("conan.lock")
assert f"{old}#{rev0}" in lock

c.run(f"export {pkg} --version=1.1")
rev1 = c.exported_recipe_revision()
c.run(f"lock upgrade --{kind_create}={pkg}/[*] --update-{kind}={pkg}/[*]")
# c.run(f"lock upgrade --{kind_create}={pkg}/[*] --update-{kind}={pkg}/[*] --transitives") # TODO this is failing because of []
# c.run(f"lock upgrade --{kind_create}={pkg}/[*] --update-{kind}={old} --transitives")
lock = c.load("conan.lock")
print(lock)
assert f"{old}#{rev0}" not in lock
assert f"{new}#{rev1}" in lock


def test_lock_upgrade_path(self):
c = TestClient(light=True)
c.save({"liba/conanfile.py": GenConanfile("liba"),
"libb/conanfile.py": GenConanfile("libb"),
"libc/conanfile.py": GenConanfile("libc"),
"libd/conanfile.py": GenConanfile("libd")})
c.run(f"export liba --version=1.0")
c.run(f"export libb --version=1.0")
c.run(f"export libc --version=1.0")
c.run(f"export libd --version=1.0")
c.save(
{
f"conanfile.py": GenConanfile()
.with_requires(f"liba/[>=1.0 <2]")
.with_requires("libb/[<1.2]")
.with_tool_requires("libc/[>=1.0]")
.with_python_requires("libd/[>=1.0 <1.2]")
}
)

c.run("lock create .")
lock = c.load("conan.lock")
assert "liba/1.0" in lock
assert "libb/1.0" in lock
assert "libc/1.0" in lock
assert "libd/1.0" in lock

# Check versions are updated accordingly
c.run(f"export liba --version=1.9")
c.run(f"export libb --version=1.1")
c.run(f"export libb --version=1.2")
c.run(f"export libc --version=1.1")
c.run(f"export libd --version=1.1")
c.run("lock upgrade . --update-requires=liba/1.0 --update-requires=libb/[*] --update-build-requires=libc/[*] --update-python-requires=libd/1.0")
lock = c.load("conan.lock")
assert "liba/1.9" in lock
assert "libb/1.1" in lock
assert "libc/1.1" in lock
assert "libd/1.1" in lock

# Check version conanfile version range is respected
c.run(f"export libd --version=1.2")
c.run("lock upgrade . --update-python-requires=libd/*")
lock = c.load("conan.lock")
assert "libd/1.1" in lock
assert "libd/1.2" not in lock

def test_lock_upgrade_transitives(self):
c = TestClient(light=True)
c.save({"liba/conanfile.py": GenConanfile("liba"),
"libb/conanfile.py": GenConanfile("libb").with_requires("libc/[<2.0]"),
"libc/conanfile.py": GenConanfile("libc")})
c.run(f"export liba --version=1.0")
c.run(f"export libc --version=1.0")
c.run(f"create libb --version=1.0 --build=missing")
c.save(
{
f"conanfile.py": GenConanfile()
.with_requires(f"liba/[>=1.0 <2]")
.with_requires("libb/[<1.2]")
}
)
c.run("lock create .")
lock = c.load("conan.lock")
assert "liba/1.0" in lock
assert "libb/1.0" in lock
assert "libc/1.0" in lock

# Check if transitive dependencies of libb (libc) also gets updated
c.run(f"export libc --version=1.1")
c.run(f"create libb --version=1.1 --build=missing")
c.run("lock upgrade . --update-requires=libb/1.0 --transitives")
lock = c.load("conan.lock")
assert "liba/1.0" in lock
assert "libb/1.1" in lock
assert "libc/1.1" in lock

# Check if new transitive dependency is added -> this should work even without --transitive
c.save({"libb/conanfile.py": GenConanfile("libb").with_requires("libc/[<2.0]").with_requires("libd/[<2.0]"),
"libd/conanfile.py": GenConanfile("libd")})
c.run(f"export libd --version=1.0")
c.run(f"create libb --version=1.1 --build=missing")
c.run("lock upgrade . --update-requires=libb/1.1")
lock = c.load("conan.lock")
assert "liba/1.0" in lock
assert "libb/1.1" in lock
assert "libc/1.1" in lock
assert "libd/1.0" in lock

# Check double transitive upgrade
c.save({"libb/conanfile.py": GenConanfile("libb").with_requires("libc/[<2.0]"),
"libc/conanfile.py": GenConanfile("libc").with_requires("libd/[<2.0]"),
"libd/conanfile.py": GenConanfile("libd")})
c.run(f"export libd --version=1.0")
c.run(f"create libc --version=1.1 --build=missing")
c.run(f"create libb --version=1.1 --build=missing")
c.run("lock upgrade . --update-requires=libb/1.1 --transitives")
lock = c.load("conan.lock")
assert "liba/1.0" in lock
assert "libb/1.1" in lock
assert "libc/1.1" in lock
assert "libd/1.0" in lock
c.run(f"export libd --version=1.1")
c.run("lock upgrade . --update-requires=libb/1.1 --transitives")
lock = c.load("conan.lock")
assert "liba/1.0" in lock
assert "libb/1.1" in lock
assert "libc/1.1" in lock
assert "libd/1.1" in lock

def test_lock_upgrade_build_transitives(self):
c = TestClient(light=True)
c.save({"liba/conanfile.py": GenConanfile("liba"),
"libb/conanfile.py": GenConanfile("libb").with_build_requires("libc/[<2.0]"),
"libc/conanfile.py": GenConanfile("libc").with_requires("libd/[<2.0]"),
"libd/conanfile.py": GenConanfile("libd")})
c.run(f"export libd --version=1.0")
c.run(f"create libc --version=1.0 --build=missing")
c.run(f"create libb --version=1.0 --build=missing")
c.run(f"export liba --version=1.0")
c.save(
{
f"conanfile.py": GenConanfile()
.with_requires(f"liba/[>=1.0 <2]")
.with_build_requires("libb/[<1.2]")
}
)
c.run("lock create .")
c.run(f"export libd --version=1.1")
c.run(f"create libc --version=1.1 --build=missing")
c.run(f"create libb --version=1.1 --build=missing")
c.run("lock upgrade . --update-build-requires=libb/1.0 --transitives")
lock = c.load("conan.lock")
print(c.out)
print(lock)
assert "liba/1.0" in lock
assert "libb/1.1" in lock
assert "libc/1.1" in lock
# assert "libd/1.1" in lock # TODO is failing -> we are not getting right the build_requires transitives
Loading