Skip to content

Commit 4744d8e

Browse files
authored
Feature/config install pkg lock (#17793)
* conan config install-pkg from lockfile * checking locked config-requires * wip * wip * wip * minor refactor * wip * wip * fixes * reverted to not break existing cache config_version.json * fix test * wip * wip * wip * wip * final proposal * imports * ready for review * review, better API, better UX default to cwd, new command * fix test * fix ignore of conaninfo.txt and conanmanifest.txt * msg for conan config clean
1 parent f9dc1bc commit 4744d8e

File tree

9 files changed

+561
-209
lines changed

9 files changed

+561
-209
lines changed

conan/api/subapi/config.py

Lines changed: 134 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import json
21
import os
32

43
from conan.api.output import ConanOutput
@@ -9,10 +8,12 @@
98
from conan.internal.graph.graph_builder import DepsGraphBuilder
109
from conan.internal.graph.profile_node_definer import consumer_definer
1110
from conan.errors import ConanException
11+
12+
from conan.internal.model.conanconfig import loadconanconfig, saveconanconfig, loadconanconfig_yml
1213
from conan.internal.model.conf import BUILT_IN_CONFS
1314
from conan.internal.model.pkg_type import PackageType
14-
from conan.api.model import RecipeReference, PkgReference
15-
from conan.internal.util.files import load, save, rmdir, remove
15+
from conan.api.model import RecipeReference, Remote
16+
from conan.internal.util.files import rmdir, remove
1617

1718

1819
class ConfigAPI:
@@ -48,72 +49,146 @@ def install(self, path_or_url, verify_ssl, config_type=None, args=None,
4849
source_folder=source_folder, target_folder=target_folder)
4950
self._conan_api.reinit()
5051

51-
def install_pkg(self, ref, lockfile=None, force=False, remotes=None,
52-
profile=None) -> PkgReference:
53-
""" install configuration stored inside a Conan package
54-
The installation of configuration will reinitialize the full ConanAPI
55-
"""
52+
def install_package(self, require, lockfile=None, force=False, remotes=None, profile=None):
5653
ConanOutput().warning("The 'conan config install-pkg' is experimental",
5754
warn_tag="experimental")
58-
conan_api = self._conan_api
59-
remotes = conan_api.remotes.list() if remotes is None else remotes
60-
profile_host = profile_build = profile or conan_api.profiles.get_profile([])
55+
require = RecipeReference.loads(require)
56+
required_pkgs = self.fetch_packages([require], lockfile, remotes, profile)
57+
installed_refs = self._install_pkgs(required_pkgs, force)
58+
self._conan_api.reinit()
59+
return installed_refs
6160

62-
app = ConanApp(self._conan_api)
61+
@staticmethod
62+
def load_conanconfig(path, remotes):
63+
if os.path.isdir(path):
64+
path = os.path.join(path, "conanconfig.yml")
65+
requested_requires, urls = loadconanconfig_yml(path)
66+
if urls:
67+
new_remotes = [Remote(f"config_install_url{'_' + str(i)}", url=url)
68+
for i, url in enumerate(urls)]
69+
remotes = remotes or []
70+
remotes += new_remotes
71+
return requested_requires, remotes
72+
73+
def install_conanconfig(self, path, lockfile=None, force=False, remotes=None, profile=None):
74+
ConanOutput().warning("The 'conan config install-pkg' is experimental",
75+
warn_tag="experimental")
76+
requested_requires, remotes = self.load_conanconfig(path, remotes)
77+
required_pkgs = self.fetch_packages(requested_requires, lockfile, remotes, profile)
78+
installed_refs = self._install_pkgs(required_pkgs, force)
79+
self._conan_api.reinit()
80+
return installed_refs
81+
82+
def _install_pkgs(self, required_pkgs, force):
83+
out = ConanOutput()
84+
out.title("Configuration packages to install")
85+
config_version_file = HomePaths(self._conan_api.home_folder).config_version_path
86+
if not os.path.exists(config_version_file):
87+
config_versions = []
88+
else:
89+
ConanOutput().info(f"Reading existing config-versions file: {config_version_file}")
90+
config_versions = loadconanconfig(config_version_file)
91+
config_versions_dict = {r.name: r for r in config_versions}
92+
if len(config_versions_dict) < len(config_versions):
93+
raise ConanException("There are multiple requirements for the same package "
94+
f"with different versions: {config_version_file}")
95+
96+
new_config = config_versions_dict.copy()
97+
for required_pkg in required_pkgs:
98+
new_config.pop(required_pkg.ref.name, None) # To ensure new order
99+
new_config[required_pkg.ref.name] = required_pkg.ref
100+
final_config_refs = [r for r in new_config.values()]
101+
102+
prev_refs = "\n\t".join(repr(r) for r in config_versions)
103+
out.info(f"Previously installed configuration packages:\n\t{prev_refs}")
63104

64-
# Computation of a very simple graph that requires "ref"
65-
conanfile = app.loader.load_virtual(requires=[RecipeReference.loads(ref)])
66-
consumer_definer(conanfile, profile_host, profile_build)
67-
root_node = Node(ref=None, conanfile=conanfile, context=CONTEXT_HOST, recipe=RECIPE_VIRTUAL)
68-
root_node.is_conf = True
69-
update = ["*"]
70-
builder = DepsGraphBuilder(app.proxy, app.loader, app.range_resolver, app.cache, remotes,
71-
update, update, self._helpers.global_conf)
72-
deps_graph = builder.load_graph(root_node, profile_host, profile_build, lockfile)
73-
74-
# Basic checks of the package: correct package_type and no-dependencies
75-
deps_graph.report_graph_error()
76-
pkg = deps_graph.root.edges[0].dst
77-
ConanOutput().info(f"Configuration from package: {pkg}")
78-
if pkg.conanfile.package_type is not PackageType.CONF:
79-
raise ConanException(f'{pkg.conanfile} is not of package_type="configuration"')
80-
if pkg.edges:
81-
raise ConanException(f"Configuration package {pkg.ref} cannot have dependencies")
82-
83-
# The computation of the "package_id" and the download of the package is done as usual
84-
# By default we allow all remotes, and build_mode=None, always updating
85-
conan_api.graph.analyze_binaries(deps_graph, None, remotes, update=update, lockfile=lockfile)
86-
conan_api.install.install_binaries(deps_graph=deps_graph, remotes=remotes)
87-
88-
# We check if this specific version is already installed
89-
config_pref = pkg.pref.repr_notime()
90-
config_versions = []
91-
config_version_file = HomePaths(conan_api.home_folder).config_version_path
92-
if os.path.exists(config_version_file):
93-
config_versions = json.loads(load(config_version_file))
94-
config_versions = config_versions["config_version"]
95-
if config_pref in config_versions:
105+
new_refs = "\n\t".join(r.repr_notime() for r in final_config_refs)
106+
out.info(f"New configuration packages to install:\n\t{new_refs}")
107+
108+
if list(config_versions_dict) == list(new_config)[:len(config_versions_dict)]:
109+
# There is no conflict in order, can be done safely
110+
if final_config_refs == config_versions:
96111
if force:
97-
ConanOutput().info(f"Package '{pkg}' already configured, "
98-
"but re-installation forced")
112+
out.warning("The requested configurations are identical to the already "
113+
"installed ones, but forcing re-installation because --force")
114+
to_install = required_pkgs
99115
else:
100-
ConanOutput().info(f"Package '{pkg}' already configured, "
101-
"skipping configuration install")
102-
return pkg.pref # Already installed, we can skip repeating the install
116+
out.info("The requested configurations are identical to the already "
117+
"installed ones, skipping re-installation")
118+
to_install = []
119+
else:
120+
out.info("Installing new or updating configuration packages")
121+
to_install = required_pkgs
122+
else:
123+
# Change in order of existing configuration
124+
if force:
125+
out.warning("Installing these configuration packages will break the "
126+
"existing order, with possible side effects. "
127+
"Forcing the installation because --force was defined", warn_tag="risk")
128+
to_install = required_pkgs
129+
else:
130+
msg = ("Installing these configuration packages will break the "
131+
"existing order, with possible side effects, like breaking 'package_ids'.\n"
132+
"If you still want to enforce this configuration you can:\n"
133+
" Use 'conan config clean' first to fully reset your configuration.\n"
134+
" Or use 'conan config install-pkg --force' to force installation.")
135+
raise ConanException(msg)
103136

137+
out.title("Installing configuration from packages")
138+
# install things and update the Conan cache "config_versions.json" file
104139
from conan.internal.api.config.config_installer import configuration_install
105140
cache_folder = self._conan_api.cache_folder
106141
requester = self._helpers.requester
107-
configuration_install(cache_folder, requester, uri=pkg.conanfile.package_folder,
108-
verify_ssl=False, config_type="dir",
109-
ignore=["conaninfo.txt", "conanmanifest.txt"])
110-
# We save the current package full reference in the file for future
111-
# And for ``package_id`` computation
112-
config_versions = {ref.split("/", 1)[0]: ref for ref in config_versions}
113-
config_versions[pkg.pref.ref.name] = pkg.pref.repr_notime()
114-
save(config_version_file, json.dumps({"config_version": list(config_versions.values())}))
115-
self._conan_api.reinit()
116-
return pkg.pref
142+
for pkg in to_install:
143+
out.info(f"Installing configuration from {pkg.ref}")
144+
configuration_install(cache_folder, requester, uri=pkg.conanfile.package_folder,
145+
verify_ssl=False, config_type="dir",
146+
ignore=["conaninfo.txt", "conanmanifest.txt"])
147+
148+
saveconanconfig(config_version_file, final_config_refs)
149+
return final_config_refs
150+
151+
def fetch_packages(self, refs, lockfile=None, remotes=None, profile=None):
152+
""" install configuration stored inside a Conan package
153+
The installation of configuration will reinitialize the full ConanAPI
154+
"""
155+
conan_api = self._conan_api
156+
remotes = conan_api.remotes.list() if remotes is None else remotes
157+
profile_host = profile_build = profile or conan_api.profiles.get_profile([])
158+
159+
app = ConanApp(self._conan_api)
160+
161+
ConanOutput().title("Fetching requested configuration packages")
162+
result = []
163+
for ref in refs:
164+
# Computation of a very simple graph that requires "ref"
165+
# Need to convert input requires to RecipeReference
166+
conanfile = app.loader.load_virtual(requires=[ref])
167+
consumer_definer(conanfile, profile_host, profile_build)
168+
root_node = Node(ref=None, conanfile=conanfile, context=CONTEXT_HOST,
169+
recipe=RECIPE_VIRTUAL)
170+
root_node.is_conf = True
171+
update = ["*"]
172+
builder = DepsGraphBuilder(app.proxy, app.loader, app.range_resolver, app.cache, remotes,
173+
update, update, self._helpers.global_conf)
174+
deps_graph = builder.load_graph(root_node, profile_host, profile_build, lockfile)
175+
176+
# Basic checks of the package: correct package_type and no-dependencies
177+
deps_graph.report_graph_error()
178+
pkg = deps_graph.root.edges[0].dst
179+
ConanOutput().info(f"Configuration from package: {pkg}")
180+
if pkg.conanfile.package_type is not PackageType.CONF:
181+
raise ConanException(f'{pkg.conanfile} is not of package_type="configuration"')
182+
if pkg.edges:
183+
raise ConanException(f"Configuration package {pkg.ref} cannot have dependencies")
184+
185+
# The computation of the "package_id" and the download of the package is done as usual
186+
# By default we allow all remotes, and build_mode=None, always updating
187+
conan_api.graph.analyze_binaries(deps_graph, None, remotes, update=update,
188+
lockfile=lockfile)
189+
conan_api.install.install_binaries(deps_graph=deps_graph, remotes=remotes)
190+
result.append(pkg)
191+
return result
117192

118193
def get(self, name, default=None, check_type=None):
119194
""" get the value of a global.conf item

conan/cli/commands/config.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
import os
2+
13
from conan.api.model import Remote
24
from conan.api.output import cli_out_write
5+
from conan.cli import make_abs_path
36
from conan.cli.command import conan_command, conan_subcommand, OnceArgument
47
from conan.cli.formatters import default_json_formatter
58
from conan.errors import ConanException
@@ -59,8 +62,11 @@ def get_bool_from_text(value): # TODO: deprecate this
5962
def config_install_pkg(conan_api, parser, subparser, *args):
6063
"""
6164
(Experimental) Install the configuration (remotes, profiles, conf), from a Conan package
65+
or from a conanconfig.yml file
6266
"""
63-
subparser.add_argument("item", help="Conan require")
67+
subparser.add_argument("reference", nargs="?",
68+
help="Package reference 'pkg/version' to install configuration from "
69+
"or path to 'conanconfig.yml' file")
6470
subparser.add_argument("-l", "--lockfile", action=OnceArgument,
6571
help="Path to a lockfile. Use --lockfile=\"\" to avoid automatic use of "
6672
"existing 'conan.lock' file")
@@ -78,6 +84,13 @@ def config_install_pkg(conan_api, parser, subparser, *args):
7884
subparser.add_argument("-o", "--options", action="append", help="Options to install config")
7985
args = parser.parse_args(*args)
8086

87+
path = make_abs_path(args.reference or ".")
88+
if os.path.isdir(path):
89+
path = os.path.join(path, "conanconfig.yml")
90+
path = path if os.path.exists(path) else None
91+
if path is None and args.reference is None:
92+
raise ConanException("Must provide a package reference or a path to a conanconfig.yml file")
93+
8194
lockfile = conan_api.lockfile.get_lockfile(lockfile=args.lockfile,
8295
partial=args.lockfile_partial)
8396

@@ -88,9 +101,15 @@ def config_install_pkg(conan_api, parser, subparser, *args):
88101
profiles = [default_profile] if default_profile else []
89102
profile = conan_api.profiles.get_profile(profiles, args.settings, args.options)
90103
remotes = [Remote("config_install_url", url=args.url)] if args.url else None
91-
config_pref = conan_api.config.install_pkg(args.item, lockfile=lockfile, force=args.force,
92-
remotes=remotes, profile=profile)
93-
lockfile = conan_api.lockfile.add_lockfile(lockfile, config_requires=[config_pref.ref])
104+
105+
if path:
106+
conanconfig = path
107+
refs = conan_api.config.install_conanconfig(conanconfig, lockfile=lockfile, force=args.force,
108+
remotes=remotes, profile=profile)
109+
else:
110+
refs = conan_api.config.install_package(args.reference, lockfile=lockfile, force=args.force,
111+
remotes=remotes, profile=profile)
112+
lockfile = conan_api.lockfile.add_lockfile(lockfile, config_requires=refs)
94113
conan_api.lockfile.save_lockfile(lockfile, args.lockfile_out)
95114

96115

0 commit comments

Comments
 (0)