|
1 | | -import json |
2 | 1 | import os |
3 | 2 |
|
4 | 3 | from conan.api.output import ConanOutput |
|
9 | 8 | from conan.internal.graph.graph_builder import DepsGraphBuilder |
10 | 9 | from conan.internal.graph.profile_node_definer import consumer_definer |
11 | 10 | from conan.errors import ConanException |
| 11 | + |
| 12 | +from conan.internal.model.conanconfig import loadconanconfig, saveconanconfig, loadconanconfig_yml |
12 | 13 | from conan.internal.model.conf import BUILT_IN_CONFS |
13 | 14 | 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 |
16 | 17 |
|
17 | 18 |
|
18 | 19 | class ConfigAPI: |
@@ -48,72 +49,146 @@ def install(self, path_or_url, verify_ssl, config_type=None, args=None, |
48 | 49 | source_folder=source_folder, target_folder=target_folder) |
49 | 50 | self._conan_api.reinit() |
50 | 51 |
|
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): |
56 | 53 | ConanOutput().warning("The 'conan config install-pkg' is experimental", |
57 | 54 | 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 |
61 | 60 |
|
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}") |
63 | 104 |
|
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: |
96 | 111 | 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 |
99 | 115 | 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) |
103 | 136 |
|
| 137 | + out.title("Installing configuration from packages") |
| 138 | + # install things and update the Conan cache "config_versions.json" file |
104 | 139 | from conan.internal.api.config.config_installer import configuration_install |
105 | 140 | cache_folder = self._conan_api.cache_folder |
106 | 141 | 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 |
117 | 192 |
|
118 | 193 | def get(self, name, default=None, check_type=None): |
119 | 194 | """ get the value of a global.conf item |
|
0 commit comments