From 4cd281112cdd16cf4f5ec80d23ac9bb12f6a580e Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Fri, 16 May 2025 10:55:31 +0200 Subject: [PATCH 001/110] Proof of concept --- conan/api/subapi/cache.py | 15 ++-- conan/internal/api/migrations.py | 2 + conan/internal/api/uploader.py | 28 ++------ conan/internal/cache/home_paths.py | 4 ++ conan/internal/rest/remote_manager.py | 14 ++-- conan/internal/util/compression.py | 69 +++++++++++++++++++ conan/internal/util/files.py | 11 --- test/unittests/util/files/tar_extract_test.py | 6 +- 8 files changed, 97 insertions(+), 52 deletions(-) create mode 100644 conan/internal/util/compression.py diff --git a/conan/api/subapi/cache.py b/conan/api/subapi/cache.py index b0d3ede2d10..ffa05bf3b66 100644 --- a/conan/api/subapi/cache.py +++ b/conan/api/subapi/cache.py @@ -18,6 +18,7 @@ from conan.api.model import RecipeReference from conan.internal.util.dates import revision_timestamp_now from conan.internal.util.files import rmdir, mkdir, remove +from conan.internal.util.compression import tar_extract class CacheAPI: @@ -133,6 +134,8 @@ def save(self, package_list, tgz_path, no_source=False): mkdir(os.path.dirname(tgz_path)) name = os.path.basename(tgz_path) compresslevel = global_conf.get("core.gzip:compresslevel", check_type=int) + # compress_fn = self._load_compress_plugin() + with open(tgz_path, "wb") as tgz_handle: tgz = gzopen_without_timestamps(name, fileobj=tgz_handle, compresslevel=compresslevel) @@ -182,14 +185,10 @@ def restore(self, path): cache = PkgCache(self.conan_api.cache_folder, self.conan_api.config.global_conf) cache_folder = cache.store # Note, this is not the home, but the actual package cache - - with open(path, mode='rb') as file_handler: - the_tar = tarfile.open(fileobj=file_handler) - fileobj = the_tar.extractfile("pkglist.json") - pkglist = fileobj.read() - the_tar.extraction_filter = (lambda member, _: member) # fully_trusted (Py 3.14) - the_tar.extractall(path=cache_folder) - the_tar.close() + tar_extract(self.conan_api.cache_folder, path, cache_folder) + # Retrieve the package list from the already extracted archive + with open(os.path.join(cache_folder, "pkglist.json")) as file_handler: + pkglist = file_handler.read() # After unzipping the files, we need to update the DB that references these files out = ConanOutput() diff --git a/conan/internal/api/migrations.py b/conan/internal/api/migrations.py index fc54424e940..da3faf4d3cd 100644 --- a/conan/internal/api/migrations.py +++ b/conan/internal/api/migrations.py @@ -4,6 +4,7 @@ from conan.api.output import ConanOutput from conan.internal.default_settings import migrate_settings_file +from conan.internal.util.compression import migrate_compression_plugin from conans.migrations import Migrator from conan.internal.util.dates import timestamp_now from conan.internal.util.files import load, save @@ -51,6 +52,7 @@ def _apply_migrations(self, old_version): # Update profile plugin from conan.internal.api.profile.profile_loader import migrate_profile_plugin migrate_profile_plugin(self.cache_folder) + migrate_compression_plugin(self.cache_folder) if old_version and old_version < "2.0.14-": _migrate_pkg_db_lru(self.cache_folder, old_version) diff --git a/conan/internal/api/uploader.py b/conan/internal/api/uploader.py index b075e4f07c7..f8921fa1c5e 100644 --- a/conan/internal/api/uploader.py +++ b/conan/internal/api/uploader.py @@ -12,8 +12,9 @@ from conan.errors import ConanException from conan.internal.paths import (CONAN_MANIFEST, CONANFILE, EXPORT_SOURCES_TGZ_NAME, EXPORT_TGZ_NAME, PACKAGE_TGZ_NAME, CONANINFO) +from conan.internal.util.compression import tar_compress from conan.internal.util.files import (clean_dirty, is_dirty, gather_files, - set_dirty_context_manager, mkdir, human_size) + mkdir, human_size) UPLOAD_POLICY_FORCE = "force-upload" UPLOAD_POLICY_SKIP = "skip-upload" @@ -156,8 +157,8 @@ def add_tgz(tgz_name, tgz_files): result[tgz_name] = tgz elif tgz_files: compresslevel = self._global_conf.get("core.gzip:compresslevel", check_type=int) - tgz = compress_files(tgz_files, tgz_name, download_export_folder, - compresslevel=compresslevel, ref=ref) + tgz = tar_compress(self._app.cache_folder, tgz_files, tgz_name, download_export_folder, + compresslevel=compresslevel, ref=ref) result[tgz_name] = tgz add_tgz(EXPORT_TGZ_NAME, files) @@ -203,8 +204,8 @@ def _compress_package_files(self, layout, pref): if not os.path.isfile(package_tgz): tgz_files = {f: path for f, path in files.items()} compresslevel = self._global_conf.get("core.gzip:compresslevel", check_type=int) - tgz_path = compress_files(tgz_files, PACKAGE_TGZ_NAME, download_pkg_folder, - compresslevel=compresslevel, ref=pref) + tgz_path = tar_compress(self._app.cache_folder, tgz_files, PACKAGE_TGZ_NAME, download_pkg_folder, + compresslevel=compresslevel, ref=pref) assert tgz_path == package_tgz assert os.path.exists(package_tgz) @@ -271,23 +272,6 @@ def gzopen_without_timestamps(name, fileobj, compresslevel=None): return t -def compress_files(files, name, dest_dir, compresslevel=None, ref=None): - t1 = time.time() - # FIXME, better write to disk sequentially and not keep tgz contents in memory - tgz_path = os.path.join(dest_dir, name) - ConanOutput(scope=str(ref)).info(f"Compressing {name}") - with set_dirty_context_manager(tgz_path), open(tgz_path, "wb") as tgz_handle: - tgz = gzopen_without_timestamps(name, fileobj=tgz_handle, compresslevel=compresslevel) - for filename, abs_path in sorted(files.items()): - # recursive is False in case it is a symlink to a folder - tgz.add(abs_path, filename, recursive=False) - tgz.close() - - duration = time.time() - t1 - ConanOutput().debug(f"{name} compressed in {duration} time") - return tgz_path - - def _total_size(cache_files): total_size = 0 for file in cache_files.values(): diff --git a/conan/internal/cache/home_paths.py b/conan/internal/cache/home_paths.py index 5674fbba145..5d27bfd5a0e 100644 --- a/conan/internal/cache/home_paths.py +++ b/conan/internal/cache/home_paths.py @@ -90,3 +90,7 @@ def settings_path_user(self): @property def config_version_path(self): return os.path.join(self._home, "config_version.json") + + @property + def compression_plugin_path(self): + return os.path.join(self._home, _EXTENSIONS_FOLDER, _PLUGINS, "compression.py") diff --git a/conan/internal/rest/remote_manager.py b/conan/internal/rest/remote_manager.py index d87f3d849bb..eeaf5e939b7 100644 --- a/conan/internal/rest/remote_manager.py +++ b/conan/internal/rest/remote_manager.py @@ -17,7 +17,8 @@ from conan.api.model import RecipeReference from conan.internal.util.files import rmdir, human_size from conan.internal.paths import EXPORT_SOURCES_TGZ_NAME, EXPORT_TGZ_NAME, PACKAGE_TGZ_NAME -from conan.internal.util.files import mkdir, tar_extract +from conan.internal.util.files import mkdir +from conan.internal.util.compression import tar_extract class RemoteManager: @@ -81,7 +82,7 @@ def get_recipe(self, ref, remote, metadata=None): tgz_file = zipped_files.pop(EXPORT_TGZ_NAME, None) if tgz_file: - uncompress_file(tgz_file, export_folder, scope=str(ref)) + uncompress_file(self._home_folder, tgz_file, export_folder, scope=str(ref)) mkdir(export_folder) for file_name, file_path in zipped_files.items(): # copy CONANFILE shutil.move(file_path, os.path.join(export_folder, file_name)) @@ -123,7 +124,7 @@ def get_recipe_sources(self, ref, layout, remote): self._signer.verify(ref, download_folder, files=zipped_files) tgz_file = zipped_files[EXPORT_SOURCES_TGZ_NAME] - uncompress_file(tgz_file, export_sources_folder, scope=str(ref)) + uncompress_file(self._home_folder, tgz_file, export_sources_folder, scope=str(ref)) def get_package(self, pref, remote, metadata=None): output = ConanOutput(scope=str(pref.ref)) @@ -171,7 +172,7 @@ def _get_package(self, layout, pref, remote, scoped_output, metadata): tgz_file = zipped_files.pop(PACKAGE_TGZ_NAME, None) package_folder = layout.package() - uncompress_file(tgz_file, package_folder, scope=str(pref.ref)) + uncompress_file(self._home_folder, tgz_file, package_folder, scope=str(pref.ref)) mkdir(package_folder) # Just in case it doesn't exist, because uncompress did nothing for file_name, file_path in zipped_files.items(): # copy CONANINFO and CONANMANIFEST shutil.move(file_path, os.path.join(package_folder, file_name)) @@ -281,15 +282,14 @@ def _call_remote(self, remote, method, *args, **kwargs): raise ConanException(exc, remote=remote) -def uncompress_file(src_path, dest_folder, scope=None): +def uncompress_file(cache_folder, src_path, dest_folder, scope=None): try: filesize = os.path.getsize(src_path) big_file = filesize > 10000000 # 10 MB if big_file: hs = human_size(filesize) ConanOutput(scope=scope).info(f"Decompressing {hs} {os.path.basename(src_path)}") - with open(src_path, mode='rb') as file_handler: - tar_extract(file_handler, dest_folder) + tar_extract(cache_folder, src_path, dest_folder) except Exception as e: error_msg = "Error while extracting downloaded file '%s' to %s\n%s\n"\ % (src_path, dest_folder, str(e)) diff --git a/conan/internal/util/compression.py b/conan/internal/util/compression.py new file mode 100644 index 00000000000..046e32d1407 --- /dev/null +++ b/conan/internal/util/compression.py @@ -0,0 +1,69 @@ +import os +from conan.internal.cache.home_paths import HomePaths +from conan.internal.loader import load_python_file +from conan.internal.errors import ConanException + +def tar_extract(cache_folder, src_path, destination_dir): + compress_plugin = _load_compress_plugin(cache_folder) + compress_plugin.tar_extract(src_path, destination_dir) + +def tar_compress(cache_folder, files, name, dest_dir, compresslevel=None, ref=None): + compress_plugin = _load_compress_plugin(cache_folder) + return compress_plugin.tar_compress(files, name, dest_dir, compresslevel, ref) + +def _load_compress_plugin(cache_folder): + compression_plugin_path = HomePaths(cache_folder).compression_plugin_path + if not os.path.exists(compression_plugin_path): + # TODO + raise ConanException("The 'compression.py' plugin file doesn't exist. If you want " + "to disable it, edit its contents instead of removing it") + + mod, _ = load_python_file(compression_plugin_path) + if not hasattr(mod, "tar_extract") or not hasattr(mod, "tar_compress"): + raise ConanException("The 'compression.py' plugin does not contain required `tar_extract` or `tar_compress` functions") + return mod + + +_default_compression_plugin = """\ +# This file was generated by Conan. Remove this comment if you edit this file or Conan +# will destroy your changes. + +import os +import time +import tarfile +from conan.api.output import ConanOutput + +def tar_compress(files, name, dest_dir, compresslevel=None, ref=None): + t1 = time.time() + # FIXME, better write to disk sequentially and not keep tgz contents in memory + tgz_path = os.path.join(dest_dir, name) + ConanOutput(scope=str(ref)).info(f"Compressing {name}") + with set_dirty_context_manager(tgz_path), open(tgz_path, "wb") as tgz_handle: + tgz = gzopen_without_timestamps(name, fileobj=tgz_handle, compresslevel=compresslevel) + for filename, abs_path in sorted(files.items()): + # recursive is False in case it is a symlink to a folder + tgz.add(abs_path, filename, recursive=False) + tgz.close() + + duration = time.time() - t1 + ConanOutput().debug(f"{name} compressed in {duration} time") + return tgz_path + + +def tar_extract(src_path, destination_dir): + with open(src_path, mode='rb') as file_handler: + the_tar = tarfile.open(fileobj=file_handler) + # NOTE: The errorlevel=2 has been removed because it was failing in Win10, it didn't allow to + # "could not change modification time", with time=0 + # the_tar.errorlevel = 2 # raise exception if any error + the_tar.extraction_filter = (lambda member, path: member) # fully_trusted, avoid Py3.14 break + the_tar.extractall(path=destination_dir) + the_tar.close() +""" + +def migrate_compression_plugin(cache_folder): + from conan.internal.api.migrations import update_file + + profile_plugin_file = HomePaths(cache_folder).compression_plugin_path + update_file(profile_plugin_file, _default_compression_plugin) + diff --git a/conan/internal/util/files.py b/conan/internal/util/files.py index 98746479ae0..01b9a76fe64 100644 --- a/conan/internal/util/files.py +++ b/conan/internal/util/files.py @@ -6,7 +6,6 @@ import shutil import stat import sys -import tarfile import time from contextlib import contextmanager @@ -256,16 +255,6 @@ def mkdir(path): os.makedirs(path) -def tar_extract(fileobj, destination_dir): - the_tar = tarfile.open(fileobj=fileobj) - # NOTE: The errorlevel=2 has been removed because it was failing in Win10, it didn't allow to - # "could not change modification time", with time=0 - # the_tar.errorlevel = 2 # raise exception if any error - the_tar.extraction_filter = (lambda member, path: member) # fully_trusted, avoid Py3.14 break - the_tar.extractall(path=destination_dir) - the_tar.close() - - def merge_directories(src, dst): from conan.tools.files import copy copy(None, pattern="*", src=src, dst=dst) diff --git a/test/unittests/util/files/tar_extract_test.py b/test/unittests/util/files/tar_extract_test.py index 6f031a56a13..9c71f5d07b8 100644 --- a/test/unittests/util/files/tar_extract_test.py +++ b/test/unittests/util/files/tar_extract_test.py @@ -50,13 +50,11 @@ def check_files(destination_dir): with chdir(working_dir): # Unpack and check destination_dir = os.path.join(self.tmp_folder, "dest") - with open(self.tgz_file, 'rb') as file_handler: - tar_extract(file_handler, destination_dir) + tar_extract(self.tgz_file, destination_dir) check_files(destination_dir) # Unpack and check (now we have a symlinked local folder) os.symlink(temp_folder(), "folder") destination_dir = os.path.join(self.tmp_folder, "dest2") - with open(self.tgz_file, 'rb') as file_handler: - tar_extract(file_handler, destination_dir) + tar_extract(self.tgz_file, destination_dir) check_files(destination_dir) From 156e036a882a3b1d7c084024eea67c603092fed0 Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Fri, 16 May 2025 11:44:29 +0200 Subject: [PATCH 002/110] Simplify --- conan/api/subapi/cache.py | 6 +- conan/internal/api/migrations.py | 2 - conan/internal/api/uploader.py | 25 ++---- conan/internal/rest/remote_manager.py | 10 +-- conan/internal/util/compression.py | 84 ++++++++++--------- conan/test/utils/test_files.py | 2 +- .../integration/command/upload/upload_test.py | 2 +- .../util/files/strip_root_extract_test.py | 2 +- test/unittests/util/files/tar_extract_test.py | 5 +- 9 files changed, 61 insertions(+), 77 deletions(-) diff --git a/conan/api/subapi/cache.py b/conan/api/subapi/cache.py index ffa05bf3b66..b27b9c06565 100644 --- a/conan/api/subapi/cache.py +++ b/conan/api/subapi/cache.py @@ -6,7 +6,6 @@ from conan.api.model import PackagesList from conan.api.output import ConanOutput -from conan.internal.api.uploader import gzopen_without_timestamps from conan.internal.cache.cache import PkgCache from conan.internal.cache.conan_reference_layout import EXPORT_SRC_FOLDER, EXPORT_FOLDER, SRC_FOLDER, \ METADATA, DOWNLOAD_EXPORT_FOLDER @@ -18,7 +17,7 @@ from conan.api.model import RecipeReference from conan.internal.util.dates import revision_timestamp_now from conan.internal.util.files import rmdir, mkdir, remove -from conan.internal.util.compression import tar_extract +from conan.internal.util.compression import gzopen_without_timestamps, tar_extract class CacheAPI: @@ -134,7 +133,6 @@ def save(self, package_list, tgz_path, no_source=False): mkdir(os.path.dirname(tgz_path)) name = os.path.basename(tgz_path) compresslevel = global_conf.get("core.gzip:compresslevel", check_type=int) - # compress_fn = self._load_compress_plugin() with open(tgz_path, "wb") as tgz_handle: tgz = gzopen_without_timestamps(name, fileobj=tgz_handle, @@ -185,7 +183,7 @@ def restore(self, path): cache = PkgCache(self.conan_api.cache_folder, self.conan_api.config.global_conf) cache_folder = cache.store # Note, this is not the home, but the actual package cache - tar_extract(self.conan_api.cache_folder, path, cache_folder) + tar_extract(path, cache_folder, cache_folder=self.conan_api.cache_folder) # Retrieve the package list from the already extracted archive with open(os.path.join(cache_folder, "pkglist.json")) as file_handler: pkglist = file_handler.read() diff --git a/conan/internal/api/migrations.py b/conan/internal/api/migrations.py index da3faf4d3cd..fc54424e940 100644 --- a/conan/internal/api/migrations.py +++ b/conan/internal/api/migrations.py @@ -4,7 +4,6 @@ from conan.api.output import ConanOutput from conan.internal.default_settings import migrate_settings_file -from conan.internal.util.compression import migrate_compression_plugin from conans.migrations import Migrator from conan.internal.util.dates import timestamp_now from conan.internal.util.files import load, save @@ -52,7 +51,6 @@ def _apply_migrations(self, old_version): # Update profile plugin from conan.internal.api.profile.profile_loader import migrate_profile_plugin migrate_profile_plugin(self.cache_folder) - migrate_compression_plugin(self.cache_folder) if old_version and old_version < "2.0.14-": _migrate_pkg_db_lru(self.cache_folder, old_version) diff --git a/conan/internal/api/uploader.py b/conan/internal/api/uploader.py index f8921fa1c5e..06cf59da330 100644 --- a/conan/internal/api/uploader.py +++ b/conan/internal/api/uploader.py @@ -12,9 +12,9 @@ from conan.errors import ConanException from conan.internal.paths import (CONAN_MANIFEST, CONANFILE, EXPORT_SOURCES_TGZ_NAME, EXPORT_TGZ_NAME, PACKAGE_TGZ_NAME, CONANINFO) -from conan.internal.util.compression import tar_compress from conan.internal.util.files import (clean_dirty, is_dirty, gather_files, mkdir, human_size) +from conan.internal.util.compression import tar_compress UPLOAD_POLICY_FORCE = "force-upload" UPLOAD_POLICY_SKIP = "skip-upload" @@ -157,8 +157,8 @@ def add_tgz(tgz_name, tgz_files): result[tgz_name] = tgz elif tgz_files: compresslevel = self._global_conf.get("core.gzip:compresslevel", check_type=int) - tgz = tar_compress(self._app.cache_folder, tgz_files, tgz_name, download_export_folder, - compresslevel=compresslevel, ref=ref) + tgz = tar_compress(tgz_files, tgz_name, download_export_folder, + compresslevel=compresslevel, ref=ref, cache_folder=self._app.cache_folder) result[tgz_name] = tgz add_tgz(EXPORT_TGZ_NAME, files) @@ -204,8 +204,8 @@ def _compress_package_files(self, layout, pref): if not os.path.isfile(package_tgz): tgz_files = {f: path for f, path in files.items()} compresslevel = self._global_conf.get("core.gzip:compresslevel", check_type=int) - tgz_path = tar_compress(self._app.cache_folder, tgz_files, PACKAGE_TGZ_NAME, download_pkg_folder, - compresslevel=compresslevel, ref=pref) + tgz_path = tar_compress(tgz_files, PACKAGE_TGZ_NAME, download_pkg_folder, + compresslevel=compresslevel, ref=pref, cache_folder=self._app.cache_folder) assert tgz_path == package_tgz assert os.path.exists(package_tgz) @@ -257,21 +257,6 @@ def upload_package(self, pref, prev_bundle, remote): output.debug(f"Upload {pref} in {duration} time") -def gzopen_without_timestamps(name, fileobj, compresslevel=None): - """ !! Method overrided by laso to pass mtime=0 (!=None) to avoid time.time() was - setted in Gzip file causing md5 to change. Not possible using the - previous tarfile open because arguments are not passed to GzipFile constructor - """ - compresslevel = compresslevel if compresslevel is not None else 9 # default Gzip = 9 - fileobj = gzip.GzipFile(name, "w", compresslevel, fileobj, mtime=0) - # Format is forced because in Python3.8, it changed and it generates different tarfiles - # with different checksums, which break hashes of tgzs - # PAX_FORMAT is the default for Py38, lets make it explicit for older Python versions - t = tarfile.TarFile.taropen(name, "w", fileobj, format=tarfile.PAX_FORMAT) - t._extfileobj = False - return t - - def _total_size(cache_files): total_size = 0 for file in cache_files.values(): diff --git a/conan/internal/rest/remote_manager.py b/conan/internal/rest/remote_manager.py index eeaf5e939b7..9ebff0a07ab 100644 --- a/conan/internal/rest/remote_manager.py +++ b/conan/internal/rest/remote_manager.py @@ -82,7 +82,7 @@ def get_recipe(self, ref, remote, metadata=None): tgz_file = zipped_files.pop(EXPORT_TGZ_NAME, None) if tgz_file: - uncompress_file(self._home_folder, tgz_file, export_folder, scope=str(ref)) + uncompress_file(tgz_file, export_folder, scope=str(ref), cache_folder=self._home_folder) mkdir(export_folder) for file_name, file_path in zipped_files.items(): # copy CONANFILE shutil.move(file_path, os.path.join(export_folder, file_name)) @@ -124,7 +124,7 @@ def get_recipe_sources(self, ref, layout, remote): self._signer.verify(ref, download_folder, files=zipped_files) tgz_file = zipped_files[EXPORT_SOURCES_TGZ_NAME] - uncompress_file(self._home_folder, tgz_file, export_sources_folder, scope=str(ref)) + uncompress_file(tgz_file, export_sources_folder, scope=str(ref), cache_folder=self._home_folder) def get_package(self, pref, remote, metadata=None): output = ConanOutput(scope=str(pref.ref)) @@ -172,7 +172,7 @@ def _get_package(self, layout, pref, remote, scoped_output, metadata): tgz_file = zipped_files.pop(PACKAGE_TGZ_NAME, None) package_folder = layout.package() - uncompress_file(self._home_folder, tgz_file, package_folder, scope=str(pref.ref)) + uncompress_file(tgz_file, package_folder, scope=str(pref.ref), cache_folder=self._home_folder) mkdir(package_folder) # Just in case it doesn't exist, because uncompress did nothing for file_name, file_path in zipped_files.items(): # copy CONANINFO and CONANMANIFEST shutil.move(file_path, os.path.join(package_folder, file_name)) @@ -282,14 +282,14 @@ def _call_remote(self, remote, method, *args, **kwargs): raise ConanException(exc, remote=remote) -def uncompress_file(cache_folder, src_path, dest_folder, scope=None): +def uncompress_file(src_path, dest_folder, scope=None, cache_folder=None): try: filesize = os.path.getsize(src_path) big_file = filesize > 10000000 # 10 MB if big_file: hs = human_size(filesize) ConanOutput(scope=scope).info(f"Decompressing {hs} {os.path.basename(src_path)}") - tar_extract(cache_folder, src_path, dest_folder) + tar_extract(src_path, dest_folder, cache_folder=cache_folder) except Exception as e: error_msg = "Error while extracting downloaded file '%s' to %s\n%s\n"\ % (src_path, dest_folder, str(e)) diff --git a/conan/internal/util/compression.py b/conan/internal/util/compression.py index 046e32d1407..d72bfa023e2 100644 --- a/conan/internal/util/compression.py +++ b/conan/internal/util/compression.py @@ -1,39 +1,34 @@ -import os from conan.internal.cache.home_paths import HomePaths from conan.internal.loader import load_python_file from conan.internal.errors import ConanException -def tar_extract(cache_folder, src_path, destination_dir): - compress_plugin = _load_compress_plugin(cache_folder) - compress_plugin.tar_extract(src_path, destination_dir) +import os +import gzip +import time +import tarfile +from conan.api.output import ConanOutput +from conan.internal.util.files import set_dirty_context_manager -def tar_compress(cache_folder, files, name, dest_dir, compresslevel=None, ref=None): +def tar_extract(src_path, destination_dir, cache_folder=None): compress_plugin = _load_compress_plugin(cache_folder) - return compress_plugin.tar_compress(files, name, dest_dir, compresslevel, ref) - -def _load_compress_plugin(cache_folder): - compression_plugin_path = HomePaths(cache_folder).compression_plugin_path - if not os.path.exists(compression_plugin_path): - # TODO - raise ConanException("The 'compression.py' plugin file doesn't exist. If you want " - "to disable it, edit its contents instead of removing it") - - mod, _ = load_python_file(compression_plugin_path) - if not hasattr(mod, "tar_extract") or not hasattr(mod, "tar_compress"): - raise ConanException("The 'compression.py' plugin does not contain required `tar_extract` or `tar_compress` functions") - return mod + if compress_plugin: + return compress_plugin.tar_extract(src_path, destination_dir) + with open(src_path, mode='rb') as file_handler: + the_tar = tarfile.open(fileobj=file_handler) + # NOTE: The errorlevel=2 has been removed because it was failing in Win10, it didn't allow to + # "could not change modification time", with time=0 + # the_tar.errorlevel = 2 # raise exception if any error + the_tar.extraction_filter = (lambda member, path: member) # fully_trusted, avoid Py3.14 break + the_tar.extractall(path=destination_dir) + the_tar.close() -_default_compression_plugin = """\ -# This file was generated by Conan. Remove this comment if you edit this file or Conan -# will destroy your changes. -import os -import time -import tarfile -from conan.api.output import ConanOutput +def tar_compress(files, name, dest_dir, compresslevel=None, ref=None, cache_folder=None): + compress_plugin = _load_compress_plugin(cache_folder) + if compress_plugin: + return compress_plugin.tar_compress(files, name, dest_dir, compresslevel, ref) -def tar_compress(files, name, dest_dir, compresslevel=None, ref=None): t1 = time.time() # FIXME, better write to disk sequentially and not keep tgz contents in memory tgz_path = os.path.join(dest_dir, name) @@ -50,20 +45,29 @@ def tar_compress(files, name, dest_dir, compresslevel=None, ref=None): return tgz_path -def tar_extract(src_path, destination_dir): - with open(src_path, mode='rb') as file_handler: - the_tar = tarfile.open(fileobj=file_handler) - # NOTE: The errorlevel=2 has been removed because it was failing in Win10, it didn't allow to - # "could not change modification time", with time=0 - # the_tar.errorlevel = 2 # raise exception if any error - the_tar.extraction_filter = (lambda member, path: member) # fully_trusted, avoid Py3.14 break - the_tar.extractall(path=destination_dir) - the_tar.close() -""" +def gzopen_without_timestamps(name, fileobj, compresslevel=None): + """ !! Method overrided by laso to pass mtime=0 (!=None) to avoid time.time() was + setted in Gzip file causing md5 to change. Not possible using the + previous tarfile open because arguments are not passed to GzipFile constructor + """ + compresslevel = compresslevel if compresslevel is not None else 9 # default Gzip = 9 + fileobj = gzip.GzipFile(name, "w", compresslevel, fileobj, mtime=0) + # Format is forced because in Python3.8, it changed and it generates different tarfiles + # with different checksums, which break hashes of tgzs + # PAX_FORMAT is the default for Py38, lets make it explicit for older Python versions + t = tarfile.TarFile.taropen(name, "w", fileobj, format=tarfile.PAX_FORMAT) + t._extfileobj = False + return t -def migrate_compression_plugin(cache_folder): - from conan.internal.api.migrations import update_file - profile_plugin_file = HomePaths(cache_folder).compression_plugin_path - update_file(profile_plugin_file, _default_compression_plugin) +def _load_compress_plugin(cache_folder): + if not cache_folder: + return None + compression_plugin_path = HomePaths(cache_folder).compression_plugin_path + if not os.path.exists(compression_plugin_path): + return None + mod, _ = load_python_file(compression_plugin_path) + if not hasattr(mod, "tar_extract") or not hasattr(mod, "tar_compress"): + raise ConanException("The 'compression.py' plugin does not contain required `tar_extract` or `tar_compress` functions") + return mod diff --git a/conan/test/utils/test_files.py b/conan/test/utils/test_files.py index 7b3f011116d..55b7c173fab 100644 --- a/conan/test/utils/test_files.py +++ b/conan/test/utils/test_files.py @@ -8,7 +8,7 @@ import time from io import BytesIO -from conan.internal.api.uploader import gzopen_without_timestamps +from conan.internal.util.compression import gzopen_without_timestamps from conan.tools.files.files import untargz from conan.internal.subsystems import get_cased_path from conan.errors import ConanException diff --git a/test/integration/command/upload/upload_test.py b/test/integration/command/upload/upload_test.py index 63368208747..1bb1044d534 100644 --- a/test/integration/command/upload/upload_test.py +++ b/test/integration/command/upload/upload_test.py @@ -6,6 +6,7 @@ import unittest from collections import OrderedDict +from conan.internal.util.compression import gzopen_without_timestamps import pytest from mock import patch from requests import Response @@ -13,7 +14,6 @@ from conan.errors import ConanException from conan.api.model import PkgReference from conan.api.model import RecipeReference -from conan.internal.api.uploader import gzopen_without_timestamps from conan.internal.paths import EXPORT_SOURCES_TGZ_NAME, PACKAGE_TGZ_NAME from conan.test.utils.tools import NO_SETTINGS_PACKAGE_ID, TestClient, TestServer, \ GenConanfile, TestRequester, TestingResponse diff --git a/test/unittests/util/files/strip_root_extract_test.py b/test/unittests/util/files/strip_root_extract_test.py index 1e219be7802..0f86c4b1165 100644 --- a/test/unittests/util/files/strip_root_extract_test.py +++ b/test/unittests/util/files/strip_root_extract_test.py @@ -3,7 +3,7 @@ import unittest import zipfile -from conan.internal.api.uploader import gzopen_without_timestamps +from conan.internal.util.compression import gzopen_without_timestamps from conan.tools.files.files import untargz, unzip from conan.errors import ConanException from conan.internal.model.manifest import gather_files diff --git a/test/unittests/util/files/tar_extract_test.py b/test/unittests/util/files/tar_extract_test.py index 9c71f5d07b8..f00b1b825a6 100644 --- a/test/unittests/util/files/tar_extract_test.py +++ b/test/unittests/util/files/tar_extract_test.py @@ -2,12 +2,11 @@ import platform import tarfile import unittest - import pytest -from conan.internal.api.uploader import gzopen_without_timestamps +from conan.internal.util.compression import gzopen_without_timestamps, tar_extract from conan.test.utils.test_files import temp_folder -from conan.internal.util.files import tar_extract, save, gather_files, chdir +from conan.internal.util.files import save, gather_files, chdir class TarExtractTest(unittest.TestCase): From c7b5dff762d692e7e77b2f8dc3ae78c4ff813c6d Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Fri, 16 May 2025 12:29:31 +0200 Subject: [PATCH 003/110] Added tar_compressor --- conan/api/subapi/cache.py | 17 ++++++++--------- conan/internal/util/compression.py | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/conan/api/subapi/cache.py b/conan/api/subapi/cache.py index b27b9c06565..32e9759efca 100644 --- a/conan/api/subapi/cache.py +++ b/conan/api/subapi/cache.py @@ -1,8 +1,6 @@ import json import os import shutil -import tarfile -from io import BytesIO from conan.api.model import PackagesList from conan.api.output import ConanOutput @@ -17,7 +15,7 @@ from conan.api.model import RecipeReference from conan.internal.util.dates import revision_timestamp_now from conan.internal.util.files import rmdir, mkdir, remove -from conan.internal.util.compression import gzopen_without_timestamps, tar_extract +from conan.internal.util.compression import tar_compressor, tar_extract class CacheAPI: @@ -135,8 +133,8 @@ def save(self, package_list, tgz_path, no_source=False): compresslevel = global_conf.get("core.gzip:compresslevel", check_type=int) with open(tgz_path, "wb") as tgz_handle: - tgz = gzopen_without_timestamps(name, fileobj=tgz_handle, - compresslevel=compresslevel) + tgz = tar_compressor(name, fileobj=tgz_handle, compresslevel=compresslevel, + cache_path=self.conan_api.cache_folder) for ref, ref_bundle in package_list.refs().items(): ref_layout = cache.recipe_layout(ref) recipe_folder = os.path.relpath(ref_layout.base_folder, cache_folder) @@ -170,11 +168,12 @@ def save(self, package_list, tgz_path, no_source=False): out.info(f"Saving {pref} metadata: {metadata_folder}") tgz.add(os.path.join(cache_folder, metadata_folder), metadata_folder, recursive=True) + # Create pgklist.json to add it to the tgz serialized = json.dumps(package_list.serialize(), indent=2) - info = tarfile.TarInfo(name="pkglist.json") - data = serialized.encode('utf-8') - info.size = len(data) - tgz.addfile(tarinfo=info, fileobj=BytesIO(data)) + pkglist_path = os.path.join(cache_folder, "pkglist.json") + with open(pkglist_path, "w") as file_handler: + file_handler.write(serialized) + tgz.add(pkglist_path, "pkglist.json", recursive=False) tgz.close() def restore(self, path): diff --git a/conan/internal/util/compression.py b/conan/internal/util/compression.py index d72bfa023e2..e237b39ce13 100644 --- a/conan/internal/util/compression.py +++ b/conan/internal/util/compression.py @@ -44,6 +44,13 @@ def tar_compress(files, name, dest_dir, compresslevel=None, ref=None, cache_fold ConanOutput().debug(f"{name} compressed in {duration} time") return tgz_path +def tar_compressor(name, fileobj, compresslevel, cache_path=None): + compress_plugin = _load_compress_plugin(cache_path) + if compress_plugin: + return compress_plugin.TarCompressor(name, fileobj, compresslevel) + else: + return gzopen_without_timestamps(name, fileobj, compresslevel) + def gzopen_without_timestamps(name, fileobj, compresslevel=None): """ !! Method overrided by laso to pass mtime=0 (!=None) to avoid time.time() was @@ -71,3 +78,14 @@ def _load_compress_plugin(cache_folder): if not hasattr(mod, "tar_extract") or not hasattr(mod, "tar_compress"): raise ConanException("The 'compression.py' plugin does not contain required `tar_extract` or `tar_compress` functions") return mod + + +""" +Plugin `compression.py` interface: + + def tar_extract(src_path, destination_dir) -> None + def tar_compress(files, name, dest_dir, compresslevel=None, ref=None) -> str + class TarCompressor(name, fileobj, compresslevel) + def add(self, abs_path, filename, recursive=True) -> None + def close() -> None +""" From be30ba2494347d974d4cc1567dccc5d9be9fb8bc Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Fri, 16 May 2025 13:05:10 +0200 Subject: [PATCH 004/110] Extra simplify --- conan/internal/api/uploader.py | 48 ++++++++++++++++--- conan/internal/rest/remote_manager.py | 11 +++-- conan/internal/util/compression.py | 8 ++-- conan/internal/util/files.py | 11 +++++ conan/test/utils/test_files.py | 2 +- .../integration/command/upload/upload_test.py | 2 +- .../util/files/strip_root_extract_test.py | 2 +- test/unittests/util/files/tar_extract_test.py | 11 +++-- 8 files changed, 75 insertions(+), 20 deletions(-) diff --git a/conan/internal/api/uploader.py b/conan/internal/api/uploader.py index 06cf59da330..2d020f39827 100644 --- a/conan/internal/api/uploader.py +++ b/conan/internal/api/uploader.py @@ -12,9 +12,9 @@ from conan.errors import ConanException from conan.internal.paths import (CONAN_MANIFEST, CONANFILE, EXPORT_SOURCES_TGZ_NAME, EXPORT_TGZ_NAME, PACKAGE_TGZ_NAME, CONANINFO) +from conan.internal.util.compression import load_compress_plugin from conan.internal.util.files import (clean_dirty, is_dirty, gather_files, - mkdir, human_size) -from conan.internal.util.compression import tar_compress + set_dirty_context_manager, mkdir, human_size) UPLOAD_POLICY_FORCE = "force-upload" UPLOAD_POLICY_SKIP = "skip-upload" @@ -157,8 +157,8 @@ def add_tgz(tgz_name, tgz_files): result[tgz_name] = tgz elif tgz_files: compresslevel = self._global_conf.get("core.gzip:compresslevel", check_type=int) - tgz = tar_compress(tgz_files, tgz_name, download_export_folder, - compresslevel=compresslevel, ref=ref, cache_folder=self._app.cache_folder) + tgz = compress_files(tgz_files, tgz_name, download_export_folder, + compresslevel=compresslevel, ref=ref, cache_folder=self._app.cache_folder) result[tgz_name] = tgz add_tgz(EXPORT_TGZ_NAME, files) @@ -204,8 +204,8 @@ def _compress_package_files(self, layout, pref): if not os.path.isfile(package_tgz): tgz_files = {f: path for f, path in files.items()} compresslevel = self._global_conf.get("core.gzip:compresslevel", check_type=int) - tgz_path = tar_compress(tgz_files, PACKAGE_TGZ_NAME, download_pkg_folder, - compresslevel=compresslevel, ref=pref, cache_folder=self._app.cache_folder) + tgz_path = compress_files(tgz_files, PACKAGE_TGZ_NAME, download_pkg_folder, + compresslevel=compresslevel, ref=pref, cache_folder=self._app.cache_folder) assert tgz_path == package_tgz assert os.path.exists(package_tgz) @@ -257,6 +257,42 @@ def upload_package(self, pref, prev_bundle, remote): output.debug(f"Upload {pref} in {duration} time") +def gzopen_without_timestamps(name, fileobj, compresslevel=None): + """ !! Method overrided by laso to pass mtime=0 (!=None) to avoid time.time() was + setted in Gzip file causing md5 to change. Not possible using the + previous tarfile open because arguments are not passed to GzipFile constructor + """ + compresslevel = compresslevel if compresslevel is not None else 9 # default Gzip = 9 + fileobj = gzip.GzipFile(name, "w", compresslevel, fileobj, mtime=0) + # Format is forced because in Python3.8, it changed and it generates different tarfiles + # with different checksums, which break hashes of tgzs + # PAX_FORMAT is the default for Py38, lets make it explicit for older Python versions + t = tarfile.TarFile.taropen(name, "w", fileobj, format=tarfile.PAX_FORMAT) + t._extfileobj = False + return t + + +def compress_files(files, name, dest_dir, compresslevel=None, ref=None, cache_folder=None): + compress_plugin = load_compress_plugin(cache_folder) + if compress_plugin: + return compress_plugin.tar_compress(files, name, dest_dir, compresslevel, ref) + + t1 = time.time() + # FIXME, better write to disk sequentially and not keep tgz contents in memory + tgz_path = os.path.join(dest_dir, name) + ConanOutput(scope=str(ref)).info(f"Compressing {name}") + with set_dirty_context_manager(tgz_path), open(tgz_path, "wb") as tgz_handle: + tgz = gzopen_without_timestamps(name, fileobj=tgz_handle, compresslevel=compresslevel) + for filename, abs_path in sorted(files.items()): + # recursive is False in case it is a symlink to a folder + tgz.add(abs_path, filename, recursive=False) + tgz.close() + + duration = time.time() - t1 + ConanOutput().debug(f"{name} compressed in {duration} time") + return tgz_path + + def _total_size(cache_files): total_size = 0 for file in cache_files.values(): diff --git a/conan/internal/rest/remote_manager.py b/conan/internal/rest/remote_manager.py index 9ebff0a07ab..19da098dbe6 100644 --- a/conan/internal/rest/remote_manager.py +++ b/conan/internal/rest/remote_manager.py @@ -15,10 +15,10 @@ from conan.internal.model.info import load_binary_info from conan.api.model import PkgReference from conan.api.model import RecipeReference +from conan.internal.util.compression import load_compress_plugin from conan.internal.util.files import rmdir, human_size from conan.internal.paths import EXPORT_SOURCES_TGZ_NAME, EXPORT_TGZ_NAME, PACKAGE_TGZ_NAME -from conan.internal.util.files import mkdir -from conan.internal.util.compression import tar_extract +from conan.internal.util.files import mkdir, tar_extract class RemoteManager: @@ -289,7 +289,12 @@ def uncompress_file(src_path, dest_folder, scope=None, cache_folder=None): if big_file: hs = human_size(filesize) ConanOutput(scope=scope).info(f"Decompressing {hs} {os.path.basename(src_path)}") - tar_extract(src_path, dest_folder, cache_folder=cache_folder) + + compression_plugin = load_compress_plugin(cache_folder) + if compression_plugin: + compression_plugin.tar_extract(src_path, dest_folder) + else: + tar_extract(src_path, dest_folder) except Exception as e: error_msg = "Error while extracting downloaded file '%s' to %s\n%s\n"\ % (src_path, dest_folder, str(e)) diff --git a/conan/internal/util/compression.py b/conan/internal/util/compression.py index e237b39ce13..e811c84f46e 100644 --- a/conan/internal/util/compression.py +++ b/conan/internal/util/compression.py @@ -10,7 +10,7 @@ from conan.internal.util.files import set_dirty_context_manager def tar_extract(src_path, destination_dir, cache_folder=None): - compress_plugin = _load_compress_plugin(cache_folder) + compress_plugin = load_compress_plugin(cache_folder) if compress_plugin: return compress_plugin.tar_extract(src_path, destination_dir) @@ -25,7 +25,7 @@ def tar_extract(src_path, destination_dir, cache_folder=None): def tar_compress(files, name, dest_dir, compresslevel=None, ref=None, cache_folder=None): - compress_plugin = _load_compress_plugin(cache_folder) + compress_plugin = load_compress_plugin(cache_folder) if compress_plugin: return compress_plugin.tar_compress(files, name, dest_dir, compresslevel, ref) @@ -45,7 +45,7 @@ def tar_compress(files, name, dest_dir, compresslevel=None, ref=None, cache_fold return tgz_path def tar_compressor(name, fileobj, compresslevel, cache_path=None): - compress_plugin = _load_compress_plugin(cache_path) + compress_plugin = load_compress_plugin(cache_path) if compress_plugin: return compress_plugin.TarCompressor(name, fileobj, compresslevel) else: @@ -67,7 +67,7 @@ def gzopen_without_timestamps(name, fileobj, compresslevel=None): return t -def _load_compress_plugin(cache_folder): +def load_compress_plugin(cache_folder): if not cache_folder: return None compression_plugin_path = HomePaths(cache_folder).compression_plugin_path diff --git a/conan/internal/util/files.py b/conan/internal/util/files.py index 01b9a76fe64..98746479ae0 100644 --- a/conan/internal/util/files.py +++ b/conan/internal/util/files.py @@ -6,6 +6,7 @@ import shutil import stat import sys +import tarfile import time from contextlib import contextmanager @@ -255,6 +256,16 @@ def mkdir(path): os.makedirs(path) +def tar_extract(fileobj, destination_dir): + the_tar = tarfile.open(fileobj=fileobj) + # NOTE: The errorlevel=2 has been removed because it was failing in Win10, it didn't allow to + # "could not change modification time", with time=0 + # the_tar.errorlevel = 2 # raise exception if any error + the_tar.extraction_filter = (lambda member, path: member) # fully_trusted, avoid Py3.14 break + the_tar.extractall(path=destination_dir) + the_tar.close() + + def merge_directories(src, dst): from conan.tools.files import copy copy(None, pattern="*", src=src, dst=dst) diff --git a/conan/test/utils/test_files.py b/conan/test/utils/test_files.py index 55b7c173fab..7b3f011116d 100644 --- a/conan/test/utils/test_files.py +++ b/conan/test/utils/test_files.py @@ -8,7 +8,7 @@ import time from io import BytesIO -from conan.internal.util.compression import gzopen_without_timestamps +from conan.internal.api.uploader import gzopen_without_timestamps from conan.tools.files.files import untargz from conan.internal.subsystems import get_cased_path from conan.errors import ConanException diff --git a/test/integration/command/upload/upload_test.py b/test/integration/command/upload/upload_test.py index 1bb1044d534..63368208747 100644 --- a/test/integration/command/upload/upload_test.py +++ b/test/integration/command/upload/upload_test.py @@ -6,7 +6,6 @@ import unittest from collections import OrderedDict -from conan.internal.util.compression import gzopen_without_timestamps import pytest from mock import patch from requests import Response @@ -14,6 +13,7 @@ from conan.errors import ConanException from conan.api.model import PkgReference from conan.api.model import RecipeReference +from conan.internal.api.uploader import gzopen_without_timestamps from conan.internal.paths import EXPORT_SOURCES_TGZ_NAME, PACKAGE_TGZ_NAME from conan.test.utils.tools import NO_SETTINGS_PACKAGE_ID, TestClient, TestServer, \ GenConanfile, TestRequester, TestingResponse diff --git a/test/unittests/util/files/strip_root_extract_test.py b/test/unittests/util/files/strip_root_extract_test.py index 0f86c4b1165..1e219be7802 100644 --- a/test/unittests/util/files/strip_root_extract_test.py +++ b/test/unittests/util/files/strip_root_extract_test.py @@ -3,7 +3,7 @@ import unittest import zipfile -from conan.internal.util.compression import gzopen_without_timestamps +from conan.internal.api.uploader import gzopen_without_timestamps from conan.tools.files.files import untargz, unzip from conan.errors import ConanException from conan.internal.model.manifest import gather_files diff --git a/test/unittests/util/files/tar_extract_test.py b/test/unittests/util/files/tar_extract_test.py index f00b1b825a6..6f031a56a13 100644 --- a/test/unittests/util/files/tar_extract_test.py +++ b/test/unittests/util/files/tar_extract_test.py @@ -2,11 +2,12 @@ import platform import tarfile import unittest + import pytest -from conan.internal.util.compression import gzopen_without_timestamps, tar_extract +from conan.internal.api.uploader import gzopen_without_timestamps from conan.test.utils.test_files import temp_folder -from conan.internal.util.files import save, gather_files, chdir +from conan.internal.util.files import tar_extract, save, gather_files, chdir class TarExtractTest(unittest.TestCase): @@ -49,11 +50,13 @@ def check_files(destination_dir): with chdir(working_dir): # Unpack and check destination_dir = os.path.join(self.tmp_folder, "dest") - tar_extract(self.tgz_file, destination_dir) + with open(self.tgz_file, 'rb') as file_handler: + tar_extract(file_handler, destination_dir) check_files(destination_dir) # Unpack and check (now we have a symlinked local folder) os.symlink(temp_folder(), "folder") destination_dir = os.path.join(self.tmp_folder, "dest2") - tar_extract(self.tgz_file, destination_dir) + with open(self.tgz_file, 'rb') as file_handler: + tar_extract(file_handler, destination_dir) check_files(destination_dir) From 1295d4f02c2736267410dee51faba4c134b585f0 Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Mon, 19 May 2025 11:23:52 +0200 Subject: [PATCH 005/110] Cache the plugin load --- conan/api/subapi/cache.py | 89 +++++++++++++++--------------- conan/internal/util/compression.py | 21 ++----- 2 files changed, 50 insertions(+), 60 deletions(-) diff --git a/conan/api/subapi/cache.py b/conan/api/subapi/cache.py index 32e9759efca..d019477b511 100644 --- a/conan/api/subapi/cache.py +++ b/conan/api/subapi/cache.py @@ -1,6 +1,7 @@ import json import os import shutil +import tempfile from conan.api.model import PackagesList from conan.api.output import ConanOutput @@ -14,8 +15,8 @@ from conan.api.model import PkgReference from conan.api.model import RecipeReference from conan.internal.util.dates import revision_timestamp_now -from conan.internal.util.files import rmdir, mkdir, remove -from conan.internal.util.compression import tar_compressor, tar_extract +from conan.internal.util.files import rmdir, mkdir, remove, save +from conan.internal.util.compression import tar_compress, tar_extract class CacheAPI: @@ -129,52 +130,50 @@ def save(self, package_list, tgz_path, no_source=False): cache_folder = cache.store # Note, this is not the home, but the actual package cache out = ConanOutput() mkdir(os.path.dirname(tgz_path)) - name = os.path.basename(tgz_path) compresslevel = global_conf.get("core.gzip:compresslevel", check_type=int) + tar_files: dict[str,str] = {} # {path_in_tar: abs_path} - with open(tgz_path, "wb") as tgz_handle: - tgz = tar_compressor(name, fileobj=tgz_handle, compresslevel=compresslevel, - cache_path=self.conan_api.cache_folder) - for ref, ref_bundle in package_list.refs().items(): - ref_layout = cache.recipe_layout(ref) - recipe_folder = os.path.relpath(ref_layout.base_folder, cache_folder) - recipe_folder = recipe_folder.replace("\\", "/") # make win paths portable - ref_bundle["recipe_folder"] = recipe_folder - out.info(f"Saving {ref}: {recipe_folder}") - # Package only selected folders, not DOWNLOAD one - for f in (EXPORT_FOLDER, EXPORT_SRC_FOLDER, SRC_FOLDER): - if f == SRC_FOLDER and no_source: - continue - path = os.path.join(cache_folder, recipe_folder, f) - if os.path.exists(path): - tgz.add(path, f"{recipe_folder}/{f}", recursive=True) - path = os.path.join(cache_folder, recipe_folder, DOWNLOAD_EXPORT_FOLDER, METADATA) + for ref, ref_bundle in package_list.refs().items(): + ref_layout = cache.recipe_layout(ref) + recipe_folder = os.path.relpath(ref_layout.base_folder, cache_folder) + recipe_folder = recipe_folder.replace("\\", "/") # make win paths portable + ref_bundle["recipe_folder"] = recipe_folder + out.info(f"Saving {ref}: {recipe_folder}") + # Package only selected folders, not DOWNLOAD one + for f in (EXPORT_FOLDER, EXPORT_SRC_FOLDER, SRC_FOLDER): + if f == SRC_FOLDER and no_source: + continue + path = os.path.join(cache_folder, recipe_folder, f) if os.path.exists(path): - tgz.add(path, f"{recipe_folder}/{DOWNLOAD_EXPORT_FOLDER}/{METADATA}", - recursive=True) - - for pref, pref_bundle in package_list.prefs(ref, ref_bundle).items(): - pref_layout = cache.pkg_layout(pref) - pkg_folder = pref_layout.package() - folder = os.path.relpath(pkg_folder, cache_folder) - folder = folder.replace("\\", "/") # make win paths portable - pref_bundle["package_folder"] = folder - out.info(f"Saving {pref}: {folder}") - tgz.add(os.path.join(cache_folder, folder), folder, recursive=True) - if os.path.exists(pref_layout.metadata()): - metadata_folder = os.path.relpath(pref_layout.metadata(), cache_folder) - metadata_folder = metadata_folder.replace("\\", "/") # make paths portable - pref_bundle["metadata_folder"] = metadata_folder - out.info(f"Saving {pref} metadata: {metadata_folder}") - tgz.add(os.path.join(cache_folder, metadata_folder), metadata_folder, - recursive=True) - # Create pgklist.json to add it to the tgz - serialized = json.dumps(package_list.serialize(), indent=2) - pkglist_path = os.path.join(cache_folder, "pkglist.json") - with open(pkglist_path, "w") as file_handler: - file_handler.write(serialized) - tgz.add(pkglist_path, "pkglist.json", recursive=False) - tgz.close() + tar_files[f"{recipe_folder}/{f}"] = path + path = os.path.join(cache_folder, recipe_folder, DOWNLOAD_EXPORT_FOLDER, METADATA) + if os.path.exists(path): + tar_files[f"{recipe_folder}/{DOWNLOAD_EXPORT_FOLDER}/{METADATA}"] = path + + for pref, pref_bundle in package_list.prefs(ref, ref_bundle).items(): + pref_layout = cache.pkg_layout(pref) + pkg_folder = pref_layout.package() + folder = os.path.relpath(pkg_folder, cache_folder) + folder = folder.replace("\\", "/") # make win paths portable + pref_bundle["package_folder"] = folder + out.info(f"Saving {pref}: {folder}") + tar_files[folder] = os.path.join(cache_folder, folder) + + if os.path.exists(pref_layout.metadata()): + metadata_folder = os.path.relpath(pref_layout.metadata(), cache_folder) + metadata_folder = metadata_folder.replace("\\", "/") # make paths portable + pref_bundle["metadata_folder"] = metadata_folder + out.info(f"Saving {pref} metadata: {metadata_folder}") + tar_files[metadata_folder] = os.path.join(cache_folder, metadata_folder) + + # Create a temporary file in order to reuse compress_files functionality + serialized = json.dumps(package_list.serialize(), indent=2) + pkglist_path = os.path.join(tempfile.gettempdir(), "pkglist.json") + save(pkglist_path, serialized) + tar_files["pkglist.json"] = pkglist_path + tar_compress(tar_files, os.path.basename(tgz_path), os.path.dirname(tgz_path), compresslevel, + recursive=True, ref=None, cache_folder=self.conan_api.cache_folder) + remove(pkglist_path) def restore(self, path): if not os.path.isfile(path): diff --git a/conan/internal/util/compression.py b/conan/internal/util/compression.py index e811c84f46e..6a4f3e993d0 100644 --- a/conan/internal/util/compression.py +++ b/conan/internal/util/compression.py @@ -1,3 +1,4 @@ +from functools import lru_cache from conan.internal.cache.home_paths import HomePaths from conan.internal.loader import load_python_file from conan.internal.errors import ConanException @@ -24,7 +25,7 @@ def tar_extract(src_path, destination_dir, cache_folder=None): the_tar.close() -def tar_compress(files, name, dest_dir, compresslevel=None, ref=None, cache_folder=None): +def tar_compress(files, name, dest_dir, compresslevel=None, ref=None, recursive=False, cache_folder=None): compress_plugin = load_compress_plugin(cache_folder) if compress_plugin: return compress_plugin.tar_compress(files, name, dest_dir, compresslevel, ref) @@ -37,21 +38,13 @@ def tar_compress(files, name, dest_dir, compresslevel=None, ref=None, cache_fold tgz = gzopen_without_timestamps(name, fileobj=tgz_handle, compresslevel=compresslevel) for filename, abs_path in sorted(files.items()): # recursive is False in case it is a symlink to a folder - tgz.add(abs_path, filename, recursive=False) + tgz.add(abs_path, filename, recursive=recursive) tgz.close() duration = time.time() - t1 ConanOutput().debug(f"{name} compressed in {duration} time") return tgz_path -def tar_compressor(name, fileobj, compresslevel, cache_path=None): - compress_plugin = load_compress_plugin(cache_path) - if compress_plugin: - return compress_plugin.TarCompressor(name, fileobj, compresslevel) - else: - return gzopen_without_timestamps(name, fileobj, compresslevel) - - def gzopen_without_timestamps(name, fileobj, compresslevel=None): """ !! Method overrided by laso to pass mtime=0 (!=None) to avoid time.time() was setted in Gzip file causing md5 to change. Not possible using the @@ -67,6 +60,7 @@ def gzopen_without_timestamps(name, fileobj, compresslevel=None): return t +@lru_cache(maxsize=1) def load_compress_plugin(cache_folder): if not cache_folder: return None @@ -83,9 +77,6 @@ def load_compress_plugin(cache_folder): """ Plugin `compression.py` interface: - def tar_extract(src_path, destination_dir) -> None - def tar_compress(files, name, dest_dir, compresslevel=None, ref=None) -> str - class TarCompressor(name, fileobj, compresslevel) - def add(self, abs_path, filename, recursive=True) -> None - def close() -> None + def tar_extract(src_path: str, destination_dir: str) -> None + def tar_compress(files: List[str], name: str, dest_dir: str, compresslevel=None, ref: str=None, cache_folder:str, recursive: bool = False) -> str """ From cc4d3add4326f1de0d4e22579e3c9c34b46e177a Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Mon, 19 May 2025 17:23:54 +0200 Subject: [PATCH 006/110] WIP --- conan/api/subapi/config.py | 2 ++ conan/internal/util/compression.py | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/conan/api/subapi/config.py b/conan/api/subapi/config.py index f7342e475b2..5485181fcd4 100644 --- a/conan/api/subapi/config.py +++ b/conan/api/subapi/config.py @@ -3,6 +3,7 @@ import platform import textwrap import yaml +from conan.internal.util.compression import load_compress_plugin from jinja2 import Environment, FileSystemLoader from conan import conan_version @@ -31,6 +32,7 @@ def __init__(self, conan_api): self._new_config = None self._cli_core_confs = None self.hook_manager = HookManager(HomePaths(conan_api.home_folder).hooks_path) + self.compress_plugin = load_compress_plugin(conan_api.cache_folder) def home(self): return self.conan_api.cache_folder diff --git a/conan/internal/util/compression.py b/conan/internal/util/compression.py index 6a4f3e993d0..d1090beb299 100644 --- a/conan/internal/util/compression.py +++ b/conan/internal/util/compression.py @@ -60,7 +60,6 @@ def gzopen_without_timestamps(name, fileobj, compresslevel=None): return t -@lru_cache(maxsize=1) def load_compress_plugin(cache_folder): if not cache_folder: return None From f094d9aec5633d1caf77b584ac0ea8b5aff8b5e1 Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Tue, 20 May 2025 13:51:25 +0200 Subject: [PATCH 007/110] Moved plugin load to ConfigAPI --- conan/api/subapi/cache.py | 4 ++-- conan/api/subapi/config.py | 11 +++++++++-- conan/internal/api/uploader.py | 12 +++++++----- conan/internal/rest/remote_manager.py | 13 +++++++------ conan/internal/util/compression.py | 10 ++++------ 5 files changed, 29 insertions(+), 21 deletions(-) diff --git a/conan/api/subapi/cache.py b/conan/api/subapi/cache.py index d019477b511..a0a5fd99086 100644 --- a/conan/api/subapi/cache.py +++ b/conan/api/subapi/cache.py @@ -172,7 +172,7 @@ def save(self, package_list, tgz_path, no_source=False): save(pkglist_path, serialized) tar_files["pkglist.json"] = pkglist_path tar_compress(tar_files, os.path.basename(tgz_path), os.path.dirname(tgz_path), compresslevel, - recursive=True, ref=None, cache_folder=self.conan_api.cache_folder) + recursive=True, ref=None, compress_plugin=self.conan_api.config.compress_plugin) remove(pkglist_path) def restore(self, path): @@ -181,7 +181,7 @@ def restore(self, path): cache = PkgCache(self.conan_api.cache_folder, self.conan_api.config.global_conf) cache_folder = cache.store # Note, this is not the home, but the actual package cache - tar_extract(path, cache_folder, cache_folder=self.conan_api.cache_folder) + tar_extract(path, cache_folder, compress_plugin=self.conan_api.config.compress_plugin) # Retrieve the package list from the already extracted archive with open(os.path.join(cache_folder, "pkglist.json")) as file_handler: pkglist = file_handler.read() diff --git a/conan/api/subapi/config.py b/conan/api/subapi/config.py index 5485181fcd4..3ccb3b0545e 100644 --- a/conan/api/subapi/config.py +++ b/conan/api/subapi/config.py @@ -3,7 +3,7 @@ import platform import textwrap import yaml -from conan.internal.util.compression import load_compress_plugin +from conan.internal.util.compression import load_compression_plugin from jinja2 import Environment, FileSystemLoader from conan import conan_version @@ -32,7 +32,7 @@ def __init__(self, conan_api): self._new_config = None self._cli_core_confs = None self.hook_manager = HookManager(HomePaths(conan_api.home_folder).hooks_path) - self.compress_plugin = load_compress_plugin(conan_api.cache_folder) + self._compress_plugin = None def home(self): return self.conan_api.cache_folder @@ -238,4 +238,11 @@ def reinit(self): if self._new_config is not None: self._new_config.clear() self._populate_global_conf() + self._compress_plugin = None self.hook_manager = HookManager(HomePaths(self.conan_api.home_folder).hooks_path) + + @property + def compression_plugin(self): + if not self._compress_plugin: + self._compress_plugin = load_compression_plugin(self.conan_api.cache_folder) + return self._compress_plugin diff --git a/conan/internal/api/uploader.py b/conan/internal/api/uploader.py index 1a9bb70ab5c..3aaf93ca5bb 100644 --- a/conan/internal/api/uploader.py +++ b/conan/internal/api/uploader.py @@ -12,7 +12,7 @@ from conan.errors import ConanException from conan.internal.paths import (CONAN_MANIFEST, CONANFILE, EXPORT_SOURCES_TGZ_NAME, EXPORT_TGZ_NAME, PACKAGE_TGZ_NAME, CONANINFO) -from conan.internal.util.compression import load_compress_plugin +from conan.internal.util.compression import load_compression_plugin from conan.internal.util.files import (clean_dirty, is_dirty, gather_files, set_dirty_context_manager, mkdir, human_size) @@ -158,7 +158,8 @@ def add_tgz(tgz_name, tgz_files): elif tgz_files: compresslevel = self._global_conf.get("core.gzip:compresslevel", check_type=int) tgz = compress_files(tgz_files, tgz_name, download_export_folder, - compresslevel=compresslevel, ref=ref, cache_folder=self._app.cache_folder) + compresslevel=compresslevel, ref=ref, + compress_plugin=self._app.conan_api.config.compress_plugin) result[tgz_name] = tgz add_tgz(EXPORT_TGZ_NAME, files) @@ -205,7 +206,8 @@ def _compress_package_files(self, layout, pref): tgz_files = {f: path for f, path in files.items()} compresslevel = self._global_conf.get("core.gzip:compresslevel", check_type=int) tgz_path = compress_files(tgz_files, PACKAGE_TGZ_NAME, download_pkg_folder, - compresslevel=compresslevel, ref=pref, cache_folder=self._app.cache_folder) + compresslevel=compresslevel, ref=pref, + compress_plugin=self._app.conan_api.config.compress_plugin) assert tgz_path == package_tgz assert os.path.exists(package_tgz) @@ -272,8 +274,8 @@ def gzopen_without_timestamps(name, fileobj, compresslevel=None): return t -def compress_files(files, name, dest_dir, compresslevel=None, ref=None, recursive=False, cache_folder=None): - compress_plugin = load_compress_plugin(cache_folder) +def compress_files(files, name, dest_dir, compresslevel=None, ref=None, recursive=False, + compress_plugin=None): if compress_plugin: return compress_plugin.tar_compress(files, name, dest_dir, compresslevel, ref) diff --git a/conan/internal/rest/remote_manager.py b/conan/internal/rest/remote_manager.py index 19da098dbe6..4eda8e1b154 100644 --- a/conan/internal/rest/remote_manager.py +++ b/conan/internal/rest/remote_manager.py @@ -1,3 +1,4 @@ +from gzip import compress import os import shutil from typing import List @@ -15,7 +16,7 @@ from conan.internal.model.info import load_binary_info from conan.api.model import PkgReference from conan.api.model import RecipeReference -from conan.internal.util.compression import load_compress_plugin +from conan.internal.util.compression import load_compression_plugin from conan.internal.util.files import rmdir, human_size from conan.internal.paths import EXPORT_SOURCES_TGZ_NAME, EXPORT_TGZ_NAME, PACKAGE_TGZ_NAME from conan.internal.util.files import mkdir, tar_extract @@ -28,6 +29,7 @@ def __init__(self, cache, auth_manager, home_folder): self._auth_manager = auth_manager self._signer = PkgSignaturesPlugin(cache, home_folder) self._home_folder = home_folder + self._compression_plugin = load_compression_plugin(home_folder) # TODO: should use the instantiated one in ConfigAPI def _local_folder_remote(self, remote): if remote.remote_type == LOCAL_RECIPES_INDEX: @@ -82,7 +84,7 @@ def get_recipe(self, ref, remote, metadata=None): tgz_file = zipped_files.pop(EXPORT_TGZ_NAME, None) if tgz_file: - uncompress_file(tgz_file, export_folder, scope=str(ref), cache_folder=self._home_folder) + uncompress_file(tgz_file, export_folder, scope=str(ref), compression_plugin=self._compression_plugin) mkdir(export_folder) for file_name, file_path in zipped_files.items(): # copy CONANFILE shutil.move(file_path, os.path.join(export_folder, file_name)) @@ -124,7 +126,7 @@ def get_recipe_sources(self, ref, layout, remote): self._signer.verify(ref, download_folder, files=zipped_files) tgz_file = zipped_files[EXPORT_SOURCES_TGZ_NAME] - uncompress_file(tgz_file, export_sources_folder, scope=str(ref), cache_folder=self._home_folder) + uncompress_file(tgz_file, export_sources_folder, scope=str(ref), compression_plugin=self._compression_plugin) def get_package(self, pref, remote, metadata=None): output = ConanOutput(scope=str(pref.ref)) @@ -172,7 +174,7 @@ def _get_package(self, layout, pref, remote, scoped_output, metadata): tgz_file = zipped_files.pop(PACKAGE_TGZ_NAME, None) package_folder = layout.package() - uncompress_file(tgz_file, package_folder, scope=str(pref.ref), cache_folder=self._home_folder) + uncompress_file(tgz_file, package_folder, scope=str(pref.ref), compression_plugin=self._compression_plugin) mkdir(package_folder) # Just in case it doesn't exist, because uncompress did nothing for file_name, file_path in zipped_files.items(): # copy CONANINFO and CONANMANIFEST shutil.move(file_path, os.path.join(package_folder, file_name)) @@ -282,7 +284,7 @@ def _call_remote(self, remote, method, *args, **kwargs): raise ConanException(exc, remote=remote) -def uncompress_file(src_path, dest_folder, scope=None, cache_folder=None): +def uncompress_file(src_path, dest_folder, scope=None, compression_plugin=None): try: filesize = os.path.getsize(src_path) big_file = filesize > 10000000 # 10 MB @@ -290,7 +292,6 @@ def uncompress_file(src_path, dest_folder, scope=None, cache_folder=None): hs = human_size(filesize) ConanOutput(scope=scope).info(f"Decompressing {hs} {os.path.basename(src_path)}") - compression_plugin = load_compress_plugin(cache_folder) if compression_plugin: compression_plugin.tar_extract(src_path, dest_folder) else: diff --git a/conan/internal/util/compression.py b/conan/internal/util/compression.py index d1090beb299..f8ddedf55e5 100644 --- a/conan/internal/util/compression.py +++ b/conan/internal/util/compression.py @@ -10,8 +10,7 @@ from conan.api.output import ConanOutput from conan.internal.util.files import set_dirty_context_manager -def tar_extract(src_path, destination_dir, cache_folder=None): - compress_plugin = load_compress_plugin(cache_folder) +def tar_extract(src_path, destination_dir, compress_plugin=None): if compress_plugin: return compress_plugin.tar_extract(src_path, destination_dir) @@ -25,15 +24,14 @@ def tar_extract(src_path, destination_dir, cache_folder=None): the_tar.close() -def tar_compress(files, name, dest_dir, compresslevel=None, ref=None, recursive=False, cache_folder=None): - compress_plugin = load_compress_plugin(cache_folder) +def tar_compress(files, name, dest_dir, compresslevel=None, ref=None, recursive=False, compress_plugin=None): if compress_plugin: return compress_plugin.tar_compress(files, name, dest_dir, compresslevel, ref) t1 = time.time() # FIXME, better write to disk sequentially and not keep tgz contents in memory tgz_path = os.path.join(dest_dir, name) - ConanOutput(scope=str(ref)).info(f"Compressing {name}") + ConanOutput(scope=str(ref) if ref else "").info(f"Compressing {name}") with set_dirty_context_manager(tgz_path), open(tgz_path, "wb") as tgz_handle: tgz = gzopen_without_timestamps(name, fileobj=tgz_handle, compresslevel=compresslevel) for filename, abs_path in sorted(files.items()): @@ -60,7 +58,7 @@ def gzopen_without_timestamps(name, fileobj, compresslevel=None): return t -def load_compress_plugin(cache_folder): +def load_compression_plugin(cache_folder): if not cache_folder: return None compression_plugin_path = HomePaths(cache_folder).compression_plugin_path From fee5fab5cfeb655bf3dd83d91ad5211f2923097a Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Tue, 20 May 2025 16:23:01 +0200 Subject: [PATCH 008/110] Pass config_api to remote_manager and local_recipe_index --- conan/api/subapi/cache.py | 4 ++-- conan/api/subapi/config.py | 10 +++++----- conan/internal/api/uploader.py | 12 +++++------- conan/internal/conan_app.py | 6 +++--- conan/internal/rest/remote_manager.py | 13 ++++++------- .../internal/rest/rest_client_local_recipe_index.py | 4 ++-- conan/internal/util/compression.py | 12 ++++++------ 7 files changed, 29 insertions(+), 32 deletions(-) diff --git a/conan/api/subapi/cache.py b/conan/api/subapi/cache.py index a0a5fd99086..b78ecd8e196 100644 --- a/conan/api/subapi/cache.py +++ b/conan/api/subapi/cache.py @@ -172,7 +172,7 @@ def save(self, package_list, tgz_path, no_source=False): save(pkglist_path, serialized) tar_files["pkglist.json"] = pkglist_path tar_compress(tar_files, os.path.basename(tgz_path), os.path.dirname(tgz_path), compresslevel, - recursive=True, ref=None, compress_plugin=self.conan_api.config.compress_plugin) + recursive=True, ref=None, compression_plugin=self.conan_api.config.compression_plugin) remove(pkglist_path) def restore(self, path): @@ -181,7 +181,7 @@ def restore(self, path): cache = PkgCache(self.conan_api.cache_folder, self.conan_api.config.global_conf) cache_folder = cache.store # Note, this is not the home, but the actual package cache - tar_extract(path, cache_folder, compress_plugin=self.conan_api.config.compress_plugin) + tar_extract(path, cache_folder, compression_plugin=self.conan_api.config.compression_plugin) # Retrieve the package list from the already extracted archive with open(os.path.join(cache_folder, "pkglist.json")) as file_handler: pkglist = file_handler.read() diff --git a/conan/api/subapi/config.py b/conan/api/subapi/config.py index 3ccb3b0545e..bc4ab0be776 100644 --- a/conan/api/subapi/config.py +++ b/conan/api/subapi/config.py @@ -32,7 +32,7 @@ def __init__(self, conan_api): self._new_config = None self._cli_core_confs = None self.hook_manager = HookManager(HomePaths(conan_api.home_folder).hooks_path) - self._compress_plugin = None + self._compression_plugin = None def home(self): return self.conan_api.cache_folder @@ -238,11 +238,11 @@ def reinit(self): if self._new_config is not None: self._new_config.clear() self._populate_global_conf() - self._compress_plugin = None + self._compression_plugin = None self.hook_manager = HookManager(HomePaths(self.conan_api.home_folder).hooks_path) @property def compression_plugin(self): - if not self._compress_plugin: - self._compress_plugin = load_compression_plugin(self.conan_api.cache_folder) - return self._compress_plugin + if not self._compression_plugin: + self._compression_plugin = load_compression_plugin(self.conan_api.cache_folder) + return self._compression_plugin diff --git a/conan/internal/api/uploader.py b/conan/internal/api/uploader.py index 3aaf93ca5bb..e3cca0ea177 100644 --- a/conan/internal/api/uploader.py +++ b/conan/internal/api/uploader.py @@ -12,7 +12,6 @@ from conan.errors import ConanException from conan.internal.paths import (CONAN_MANIFEST, CONANFILE, EXPORT_SOURCES_TGZ_NAME, EXPORT_TGZ_NAME, PACKAGE_TGZ_NAME, CONANINFO) -from conan.internal.util.compression import load_compression_plugin from conan.internal.util.files import (clean_dirty, is_dirty, gather_files, set_dirty_context_manager, mkdir, human_size) @@ -159,7 +158,7 @@ def add_tgz(tgz_name, tgz_files): compresslevel = self._global_conf.get("core.gzip:compresslevel", check_type=int) tgz = compress_files(tgz_files, tgz_name, download_export_folder, compresslevel=compresslevel, ref=ref, - compress_plugin=self._app.conan_api.config.compress_plugin) + compression_plugin=self._app.conan_api.config.compression_plugin) result[tgz_name] = tgz add_tgz(EXPORT_TGZ_NAME, files) @@ -207,7 +206,7 @@ def _compress_package_files(self, layout, pref): compresslevel = self._global_conf.get("core.gzip:compresslevel", check_type=int) tgz_path = compress_files(tgz_files, PACKAGE_TGZ_NAME, download_pkg_folder, compresslevel=compresslevel, ref=pref, - compress_plugin=self._app.conan_api.config.compress_plugin) + compression_plugin=self._app.conan_api.config.compression_plugin) assert tgz_path == package_tgz assert os.path.exists(package_tgz) @@ -274,10 +273,9 @@ def gzopen_without_timestamps(name, fileobj, compresslevel=None): return t -def compress_files(files, name, dest_dir, compresslevel=None, ref=None, recursive=False, - compress_plugin=None): - if compress_plugin: - return compress_plugin.tar_compress(files, name, dest_dir, compresslevel, ref) +def compress_files(files, name, dest_dir, compresslevel=None, ref=None, recursive=False, compression_plugin=None): + if compression_plugin: + return compression_plugin.tar_compress(files, name, dest_dir, compresslevel, ref) t1 = time.time() # FIXME, better write to disk sequentially and not keep tgz contents in memory diff --git a/conan/internal/conan_app.py b/conan/internal/conan_app.py index 0ca4df974e2..9e1aa07a053 100644 --- a/conan/internal/conan_app.py +++ b/conan/internal/conan_app.py @@ -52,7 +52,7 @@ def __init__(self, conan_api): localdb = LocalDB(cache_folder) auth_manager = ConanApiAuthManager(conan_api.remotes.requester, cache_folder, localdb, global_conf) # Handle remote connections - self.remote_manager = RemoteManager(self.cache, auth_manager, cache_folder) + self.remote_manager = RemoteManager(self.cache, auth_manager, cache_folder, self.conan_api.config) global_editables = conan_api.local.editable_packages ws_editables = conan_api.workspace.editable_packages self.editable_packages = global_editables.update_copy(ws_editables) @@ -81,10 +81,10 @@ class LocalRecipesIndexApp: - loader (for the export phase of local-recipes-index) The others are internally use by other collaborators """ - def __init__(self, cache_folder): + def __init__(self, cache_folder, config_api): self.global_conf = ConfDefinition() self.cache = PkgCache(cache_folder, self.global_conf) - self.remote_manager = RemoteManager(self.cache, auth_manager=None, home_folder=cache_folder) + self.remote_manager = RemoteManager(self.cache, auth_manager=None, home_folder=cache_folder, config_api=config_api) editable_packages = EditablePackages() self.proxy = ConanProxy(self, editable_packages) self.range_resolver = RangeResolver(self, self.global_conf, editable_packages) diff --git a/conan/internal/rest/remote_manager.py b/conan/internal/rest/remote_manager.py index 4eda8e1b154..d3894accdb4 100644 --- a/conan/internal/rest/remote_manager.py +++ b/conan/internal/rest/remote_manager.py @@ -16,7 +16,6 @@ from conan.internal.model.info import load_binary_info from conan.api.model import PkgReference from conan.api.model import RecipeReference -from conan.internal.util.compression import load_compression_plugin from conan.internal.util.files import rmdir, human_size from conan.internal.paths import EXPORT_SOURCES_TGZ_NAME, EXPORT_TGZ_NAME, PACKAGE_TGZ_NAME from conan.internal.util.files import mkdir, tar_extract @@ -24,16 +23,16 @@ class RemoteManager: """ Will handle the remotes to get recipes, packages etc """ - def __init__(self, cache, auth_manager, home_folder): + def __init__(self, cache, auth_manager, home_folder, config_api): self._cache = cache self._auth_manager = auth_manager self._signer = PkgSignaturesPlugin(cache, home_folder) self._home_folder = home_folder - self._compression_plugin = load_compression_plugin(home_folder) # TODO: should use the instantiated one in ConfigAPI + self._config_api = config_api def _local_folder_remote(self, remote): if remote.remote_type == LOCAL_RECIPES_INDEX: - return RestApiClientLocalRecipesIndex(remote, self._home_folder) + return RestApiClientLocalRecipesIndex(remote, self._home_folder, self._config_api) def check_credentials(self, remote, force_auth=False): self._call_remote(remote, "check_credentials", force_auth) @@ -84,7 +83,7 @@ def get_recipe(self, ref, remote, metadata=None): tgz_file = zipped_files.pop(EXPORT_TGZ_NAME, None) if tgz_file: - uncompress_file(tgz_file, export_folder, scope=str(ref), compression_plugin=self._compression_plugin) + uncompress_file(tgz_file, export_folder, scope=str(ref), compression_plugin=self._config_api.compression_plugin) mkdir(export_folder) for file_name, file_path in zipped_files.items(): # copy CONANFILE shutil.move(file_path, os.path.join(export_folder, file_name)) @@ -126,7 +125,7 @@ def get_recipe_sources(self, ref, layout, remote): self._signer.verify(ref, download_folder, files=zipped_files) tgz_file = zipped_files[EXPORT_SOURCES_TGZ_NAME] - uncompress_file(tgz_file, export_sources_folder, scope=str(ref), compression_plugin=self._compression_plugin) + uncompress_file(tgz_file, export_sources_folder, scope=str(ref), compression_plugin=self._config_api.compression_plugin) def get_package(self, pref, remote, metadata=None): output = ConanOutput(scope=str(pref.ref)) @@ -174,7 +173,7 @@ def _get_package(self, layout, pref, remote, scoped_output, metadata): tgz_file = zipped_files.pop(PACKAGE_TGZ_NAME, None) package_folder = layout.package() - uncompress_file(tgz_file, package_folder, scope=str(pref.ref), compression_plugin=self._compression_plugin) + uncompress_file(tgz_file, package_folder, scope=str(pref.ref), compression_plugin=self._config_api.compression_plugin) mkdir(package_folder) # Just in case it doesn't exist, because uncompress did nothing for file_name, file_path in zipped_files.items(): # copy CONANINFO and CONANMANIFEST shutil.move(file_path, os.path.join(package_folder, file_name)) diff --git a/conan/internal/rest/rest_client_local_recipe_index.py b/conan/internal/rest/rest_client_local_recipe_index.py index 33082ca1356..83af84a263a 100644 --- a/conan/internal/rest/rest_client_local_recipe_index.py +++ b/conan/internal/rest/rest_client_local_recipe_index.py @@ -58,14 +58,14 @@ class RestApiClientLocalRecipesIndex: a local folder assuming the conan-center-index repo layout """ - def __init__(self, remote, home_folder): + def __init__(self, remote, home_folder, config_api): self._remote = remote local_recipes_index_path = HomePaths(home_folder).local_recipes_index_path local_recipes_index_path = os.path.join(local_recipes_index_path, remote.name, ".conan") repo_folder = self._remote.url from conan.internal.conan_app import LocalRecipesIndexApp - self._app = LocalRecipesIndexApp(local_recipes_index_path) + self._app = LocalRecipesIndexApp(local_recipes_index_path, config_api) self._hook_manager = HookManager(HomePaths(local_recipes_index_path).hooks_path) self._layout = _LocalRecipesIndexLayout(repo_folder) diff --git a/conan/internal/util/compression.py b/conan/internal/util/compression.py index f8ddedf55e5..9ebe0af96ce 100644 --- a/conan/internal/util/compression.py +++ b/conan/internal/util/compression.py @@ -10,9 +10,9 @@ from conan.api.output import ConanOutput from conan.internal.util.files import set_dirty_context_manager -def tar_extract(src_path, destination_dir, compress_plugin=None): - if compress_plugin: - return compress_plugin.tar_extract(src_path, destination_dir) +def tar_extract(src_path, destination_dir, compression_plugin=None): + if compression_plugin: + return compression_plugin.tar_extract(src_path, destination_dir) with open(src_path, mode='rb') as file_handler: the_tar = tarfile.open(fileobj=file_handler) @@ -24,9 +24,9 @@ def tar_extract(src_path, destination_dir, compress_plugin=None): the_tar.close() -def tar_compress(files, name, dest_dir, compresslevel=None, ref=None, recursive=False, compress_plugin=None): - if compress_plugin: - return compress_plugin.tar_compress(files, name, dest_dir, compresslevel, ref) +def tar_compress(files, name, dest_dir, compresslevel=None, ref=None, recursive=False, compression_plugin=None): + if compression_plugin: + return compression_plugin.tar_compress(files, name, dest_dir, compresslevel, ref) t1 = time.time() # FIXME, better write to disk sequentially and not keep tgz contents in memory From 452f7746b5d793cdd32e3f66e3c26dcaf73d45fa Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Tue, 20 May 2025 16:34:52 +0200 Subject: [PATCH 009/110] Restore previous tar_extract fixing tests --- conan/internal/rest/remote_manager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/conan/internal/rest/remote_manager.py b/conan/internal/rest/remote_manager.py index d3894accdb4..10f6617d068 100644 --- a/conan/internal/rest/remote_manager.py +++ b/conan/internal/rest/remote_manager.py @@ -294,7 +294,8 @@ def uncompress_file(src_path, dest_folder, scope=None, compression_plugin=None): if compression_plugin: compression_plugin.tar_extract(src_path, dest_folder) else: - tar_extract(src_path, dest_folder) + with open(src_path, mode='rb') as file_handler: + tar_extract(file_handler, dest_folder) except Exception as e: error_msg = "Error while extracting downloaded file '%s' to %s\n%s\n"\ % (src_path, dest_folder, str(e)) From c1a73204c088773d4f1f4b7be23bfc4336adb79b Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Thu, 22 May 2025 17:17:27 +0200 Subject: [PATCH 010/110] Removed compression.py module and minimize diff --- conan/api/subapi/cache.py | 18 ++- conan/api/subapi/config.py | 13 ++- conan/internal/api/uploader.py | 4 +- conan/internal/util/compression.py | 79 ------------- .../extensions/test_compression_plugin.py | 106 ++++++++++++++++++ 5 files changed, 132 insertions(+), 88 deletions(-) delete mode 100644 conan/internal/util/compression.py create mode 100644 test/integration/extensions/test_compression_plugin.py diff --git a/conan/api/subapi/cache.py b/conan/api/subapi/cache.py index b78ecd8e196..025f2b56d2a 100644 --- a/conan/api/subapi/cache.py +++ b/conan/api/subapi/cache.py @@ -5,6 +5,7 @@ from conan.api.model import PackagesList from conan.api.output import ConanOutput +from conan.internal.api.uploader import compress_files from conan.internal.cache.cache import PkgCache from conan.internal.cache.conan_reference_layout import EXPORT_SRC_FOLDER, EXPORT_FOLDER, SRC_FOLDER, \ METADATA, DOWNLOAD_EXPORT_FOLDER @@ -15,8 +16,7 @@ from conan.api.model import PkgReference from conan.api.model import RecipeReference from conan.internal.util.dates import revision_timestamp_now -from conan.internal.util.files import rmdir, mkdir, remove, save -from conan.internal.util.compression import tar_compress, tar_extract +from conan.internal.util.files import rmdir, mkdir, remove, save, tar_extract class CacheAPI: @@ -171,8 +171,9 @@ def save(self, package_list, tgz_path, no_source=False): pkglist_path = os.path.join(tempfile.gettempdir(), "pkglist.json") save(pkglist_path, serialized) tar_files["pkglist.json"] = pkglist_path - tar_compress(tar_files, os.path.basename(tgz_path), os.path.dirname(tgz_path), compresslevel, - recursive=True, ref=None, compression_plugin=self.conan_api.config.compression_plugin) + print(tar_files) + compress_files(tar_files, os.path.basename(tgz_path), os.path.dirname(tgz_path), compresslevel, + recursive=True, ref=None, compression_plugin=self.conan_api.config.compression_plugin) remove(pkglist_path) def restore(self, path): @@ -181,7 +182,14 @@ def restore(self, path): cache = PkgCache(self.conan_api.cache_folder, self.conan_api.config.global_conf) cache_folder = cache.store # Note, this is not the home, but the actual package cache - tar_extract(path, cache_folder, compression_plugin=self.conan_api.config.compression_plugin) + + compression_plugin = self.conan_api.config.compression_plugin + if compression_plugin: + compression_plugin.tar_extract(path, cache_folder) + else: + with open(path, mode='rb') as file_handler: + tar_extract(file_handler, cache_folder) + # Retrieve the package list from the already extracted archive with open(os.path.join(cache_folder, "pkglist.json")) as file_handler: pkglist = file_handler.read() diff --git a/conan/api/subapi/config.py b/conan/api/subapi/config.py index bc4ab0be776..9540da485d9 100644 --- a/conan/api/subapi/config.py +++ b/conan/api/subapi/config.py @@ -3,7 +3,7 @@ import platform import textwrap import yaml -from conan.internal.util.compression import load_compression_plugin +from conan.internal.loader import load_python_file from jinja2 import Environment, FileSystemLoader from conan import conan_version @@ -243,6 +243,15 @@ def reinit(self): @property def compression_plugin(self): + def load_compression_plugin(): + compression_plugin_path = HomePaths(self.conan_api.home_folder).compression_plugin_path + if not os.path.exists(compression_plugin_path): + return None + mod, _ = load_python_file(compression_plugin_path) + if not hasattr(mod, "tar_extract") or not hasattr(mod, "tar_compress"): + raise ConanException("The 'compression.py' plugin does not contain required `tar_extract` or `tar_compress` functions") + return mod if not self._compression_plugin: - self._compression_plugin = load_compression_plugin(self.conan_api.cache_folder) + self._compression_plugin = load_compression_plugin() return self._compression_plugin + diff --git a/conan/internal/api/uploader.py b/conan/internal/api/uploader.py index e3cca0ea177..31c14ef2a49 100644 --- a/conan/internal/api/uploader.py +++ b/conan/internal/api/uploader.py @@ -275,12 +275,12 @@ def gzopen_without_timestamps(name, fileobj, compresslevel=None): def compress_files(files, name, dest_dir, compresslevel=None, ref=None, recursive=False, compression_plugin=None): if compression_plugin: - return compression_plugin.tar_compress(files, name, dest_dir, compresslevel, ref) + return compression_plugin.tar_compress(files, name, dest_dir, compresslevel, ref, recursive) t1 = time.time() # FIXME, better write to disk sequentially and not keep tgz contents in memory tgz_path = os.path.join(dest_dir, name) - ConanOutput(scope=str(ref) if ref else "").info(f"Compressing {name}") + ConanOutput(scope=str(ref or "")).info(f"Compressing {name}") with set_dirty_context_manager(tgz_path), open(tgz_path, "wb") as tgz_handle: tgz = gzopen_without_timestamps(name, fileobj=tgz_handle, compresslevel=compresslevel) for filename, abs_path in sorted(files.items()): diff --git a/conan/internal/util/compression.py b/conan/internal/util/compression.py deleted file mode 100644 index 9ebe0af96ce..00000000000 --- a/conan/internal/util/compression.py +++ /dev/null @@ -1,79 +0,0 @@ -from functools import lru_cache -from conan.internal.cache.home_paths import HomePaths -from conan.internal.loader import load_python_file -from conan.internal.errors import ConanException - -import os -import gzip -import time -import tarfile -from conan.api.output import ConanOutput -from conan.internal.util.files import set_dirty_context_manager - -def tar_extract(src_path, destination_dir, compression_plugin=None): - if compression_plugin: - return compression_plugin.tar_extract(src_path, destination_dir) - - with open(src_path, mode='rb') as file_handler: - the_tar = tarfile.open(fileobj=file_handler) - # NOTE: The errorlevel=2 has been removed because it was failing in Win10, it didn't allow to - # "could not change modification time", with time=0 - # the_tar.errorlevel = 2 # raise exception if any error - the_tar.extraction_filter = (lambda member, path: member) # fully_trusted, avoid Py3.14 break - the_tar.extractall(path=destination_dir) - the_tar.close() - - -def tar_compress(files, name, dest_dir, compresslevel=None, ref=None, recursive=False, compression_plugin=None): - if compression_plugin: - return compression_plugin.tar_compress(files, name, dest_dir, compresslevel, ref) - - t1 = time.time() - # FIXME, better write to disk sequentially and not keep tgz contents in memory - tgz_path = os.path.join(dest_dir, name) - ConanOutput(scope=str(ref) if ref else "").info(f"Compressing {name}") - with set_dirty_context_manager(tgz_path), open(tgz_path, "wb") as tgz_handle: - tgz = gzopen_without_timestamps(name, fileobj=tgz_handle, compresslevel=compresslevel) - for filename, abs_path in sorted(files.items()): - # recursive is False in case it is a symlink to a folder - tgz.add(abs_path, filename, recursive=recursive) - tgz.close() - - duration = time.time() - t1 - ConanOutput().debug(f"{name} compressed in {duration} time") - return tgz_path - -def gzopen_without_timestamps(name, fileobj, compresslevel=None): - """ !! Method overrided by laso to pass mtime=0 (!=None) to avoid time.time() was - setted in Gzip file causing md5 to change. Not possible using the - previous tarfile open because arguments are not passed to GzipFile constructor - """ - compresslevel = compresslevel if compresslevel is not None else 9 # default Gzip = 9 - fileobj = gzip.GzipFile(name, "w", compresslevel, fileobj, mtime=0) - # Format is forced because in Python3.8, it changed and it generates different tarfiles - # with different checksums, which break hashes of tgzs - # PAX_FORMAT is the default for Py38, lets make it explicit for older Python versions - t = tarfile.TarFile.taropen(name, "w", fileobj, format=tarfile.PAX_FORMAT) - t._extfileobj = False - return t - - -def load_compression_plugin(cache_folder): - if not cache_folder: - return None - compression_plugin_path = HomePaths(cache_folder).compression_plugin_path - if not os.path.exists(compression_plugin_path): - return None - - mod, _ = load_python_file(compression_plugin_path) - if not hasattr(mod, "tar_extract") or not hasattr(mod, "tar_compress"): - raise ConanException("The 'compression.py' plugin does not contain required `tar_extract` or `tar_compress` functions") - return mod - - -""" -Plugin `compression.py` interface: - - def tar_extract(src_path: str, destination_dir: str) -> None - def tar_compress(files: List[str], name: str, dest_dir: str, compresslevel=None, ref: str=None, cache_folder:str, recursive: bool = False) -> str -""" diff --git a/test/integration/extensions/test_compression_plugin.py b/test/integration/extensions/test_compression_plugin.py new file mode 100644 index 00000000000..6dc5ec21b5c --- /dev/null +++ b/test/integration/extensions/test_compression_plugin.py @@ -0,0 +1,106 @@ +import os +import textwrap + +from conan.test.assets.genconanfile import GenConanfile +from conan.test.utils.tools import TestClient + + +def test_compression_plugin_not_existing(): + """Test that the compression plugin is not used if it does not exist""" + c = TestClient() + c.save({"conanfile.py": GenConanfile("pkg", "1.0")}) + c.run("create .") + c.run("cache save 'pkg/*'") + assert "Compressing conan_cache_save.tgz\n" in c.out + c.run("cache restore conan_cache_save.tgz") + # Default decompress does not have any output + assert "Decompressing conan_cache_save.tgz" not in c.out + + +def test_compression_plugin_not_valid(): + """Test an error is raised if the compression plugin is not valid""" + + c = TestClient() + compression_plugin = textwrap.dedent( + """ + def tar_compress(files, name, dest_dir, compresslevel=None, ref=None, recursive=False): + return None + """ + ) + + c.save( + { + os.path.join( + c.cache_folder, "extensions", "plugins", "compression.py" + ): compression_plugin, + "conanfile.py": GenConanfile("pkg", "1.0"), + } + ) + c.run("create .") + c.run("cache save 'pkg/*'", assert_error=True) + assert ( + "ERROR: The 'compression.py' plugin does not contain required `tar_extract` or `tar_compress` functions" + in c.out + ) + + +def test_compression_plugin_correctly_load(): + """Test that the compression plugin is correctly loaded and used on: + - cache save/restore + - remote upload/download + """ + c = TestClient(default_server_user=True) + + compression_plugin = textwrap.dedent( + """ + import os + import tarfile + from conan.api.output import ConanOutput + + # xz compression + def tar_compress(files, name, dest_dir, compresslevel=None, ref=None, recursive=False, *args, **kwargs): + tgz_path = os.path.join(dest_dir, name) + ConanOutput(scope=str(ref) if ref else "").info(f"Compressing {name} using compression plugin (xz)") + kwargs = {"preset": compresslevel} if compresslevel else {} + with tarfile.open(tgz_path, f"w:xz", **kwargs) as tgz: + for filename, abs_path in sorted(files.items()): + tgz.add(abs_path, filename, recursive=True) + return tgz_path + + def tar_extract(src_path, destination_dir, *args, **kwargs): + ConanOutput().info(f"Decompressing {os.path.basename(src_path)} using compression plugin (xz)") + with open(src_path, mode='rb') as file_handler: + the_tar = tarfile.open(fileobj=file_handler) + the_tar.extraction_filter = (lambda member, path: member) + the_tar.extractall(path=destination_dir) + the_tar.close() + """ + ) + + c.save( + { + os.path.join( + c.cache_folder, "extensions", "plugins", "compression.py" + ): compression_plugin, + "conanfile.py": GenConanfile("pkg", "1.0"), + } + ) + c.run("create .") + c.run("cache save 'pkg/*'") + assert "Compressing conan_cache_save.tgz using compression plugin (xz)" in c.out + c.run("remove pkg/* -c") + c.run("cache restore conan_cache_save.tgz") + assert "Decompressing conan_cache_save.tgz using compression plugin (xz)" in c.out + c.run("list pkg/1.0") + assert "Found 1 pkg/version recipes matching pkg/1.0 in local cache" in c.out + + # Remove pre existing tgz to force a recompression + c.run("remove pkg/* -c") + c.run("create .") + # Check the plugin is also used on remote interactions + c.run("upload * -r=default -c") + assert "Compressing conan_package.tgz using compression plugin (xz)" in c.out + assert "pkg/1.0: Uploading recipe" in c.out + c.run("remove pkg/* -c") + c.run("download 'pkg/*' -r=default") + assert "Decompressing conan_package.tgz using compression plugin (xz)" in c.out From a240c8c619766eb8a14d9f77343ce3821f604169 Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Thu, 22 May 2025 17:25:30 +0200 Subject: [PATCH 011/110] Remove debug print --- conan/api/subapi/cache.py | 1 - 1 file changed, 1 deletion(-) diff --git a/conan/api/subapi/cache.py b/conan/api/subapi/cache.py index 025f2b56d2a..1198ebe5924 100644 --- a/conan/api/subapi/cache.py +++ b/conan/api/subapi/cache.py @@ -171,7 +171,6 @@ def save(self, package_list, tgz_path, no_source=False): pkglist_path = os.path.join(tempfile.gettempdir(), "pkglist.json") save(pkglist_path, serialized) tar_files["pkglist.json"] = pkglist_path - print(tar_files) compress_files(tar_files, os.path.basename(tgz_path), os.path.dirname(tgz_path), compresslevel, recursive=True, ref=None, compression_plugin=self.conan_api.config.compression_plugin) remove(pkglist_path) From cf7de747bf3f66ce25ae95e7f9accfd4af8654ff Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Fri, 23 May 2025 10:03:37 +0200 Subject: [PATCH 012/110] Applied thread suggestions --- conan/api/subapi/config.py | 6 ++---- .../command/cache/test_cache_save_restore.py | 4 ++++ .../extensions/test_compression_plugin.py | 12 ------------ 3 files changed, 6 insertions(+), 16 deletions(-) diff --git a/conan/api/subapi/config.py b/conan/api/subapi/config.py index 9540da485d9..9577d86c962 100644 --- a/conan/api/subapi/config.py +++ b/conan/api/subapi/config.py @@ -243,15 +243,13 @@ def reinit(self): @property def compression_plugin(self): - def load_compression_plugin(): + if not self._compression_plugin: compression_plugin_path = HomePaths(self.conan_api.home_folder).compression_plugin_path if not os.path.exists(compression_plugin_path): return None mod, _ = load_python_file(compression_plugin_path) if not hasattr(mod, "tar_extract") or not hasattr(mod, "tar_compress"): raise ConanException("The 'compression.py' plugin does not contain required `tar_extract` or `tar_compress` functions") - return mod - if not self._compression_plugin: - self._compression_plugin = load_compression_plugin() + self._compression_plugin = mod return self._compression_plugin diff --git a/test/integration/command/cache/test_cache_save_restore.py b/test/integration/command/cache/test_cache_save_restore.py index a6bc91b495d..113a0dd0ac5 100644 --- a/test/integration/command/cache/test_cache_save_restore.py +++ b/test/integration/command/cache/test_cache_save_restore.py @@ -158,9 +158,13 @@ def test_cache_save_excluded_folders(): # exclude source c.run("cache save * --no-source") + # Check default compression function is being used and not compression.py plugin one + assert "Compressing conan_cache_save.tgz\n" in c.out c3 = TestClient() shutil.copy2(cache_path, c3.current_folder) c3.run("cache restore conan_cache_save.tgz") + # Default decompress does not have any output + assert "Decompressing conan_cache_save.tgz" not in c3.out ref_layout = c3.get_latest_ref_layout(ref) assert not os.path.exists(os.path.join(ref_layout.source(), "mysrc.c")) diff --git a/test/integration/extensions/test_compression_plugin.py b/test/integration/extensions/test_compression_plugin.py index 6dc5ec21b5c..cee4c3b7079 100644 --- a/test/integration/extensions/test_compression_plugin.py +++ b/test/integration/extensions/test_compression_plugin.py @@ -5,18 +5,6 @@ from conan.test.utils.tools import TestClient -def test_compression_plugin_not_existing(): - """Test that the compression plugin is not used if it does not exist""" - c = TestClient() - c.save({"conanfile.py": GenConanfile("pkg", "1.0")}) - c.run("create .") - c.run("cache save 'pkg/*'") - assert "Compressing conan_cache_save.tgz\n" in c.out - c.run("cache restore conan_cache_save.tgz") - # Default decompress does not have any output - assert "Decompressing conan_cache_save.tgz" not in c.out - - def test_compression_plugin_not_valid(): """Test an error is raised if the compression plugin is not valid""" From 557f8e001f42110087dbf760d1ffd2a64783f35e Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Fri, 23 May 2025 11:31:38 +0200 Subject: [PATCH 013/110] Remove created pkglist.json on cache restore after usage --- conan/api/subapi/cache.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/conan/api/subapi/cache.py b/conan/api/subapi/cache.py index 1198ebe5924..024fa6de68b 100644 --- a/conan/api/subapi/cache.py +++ b/conan/api/subapi/cache.py @@ -190,8 +190,11 @@ def restore(self, path): tar_extract(file_handler, cache_folder) # Retrieve the package list from the already extracted archive - with open(os.path.join(cache_folder, "pkglist.json")) as file_handler: + pkglist_path = os.path.join(cache_folder, "pkglist.json") + with open(pkglist_path) as file_handler: pkglist = file_handler.read() + # Delete the pkglist.json file to keep cache clean + remove(pkglist_path) # After unzipping the files, we need to update the DB that references these files out = ConanOutput() From f82d764a2d86b4af76b105488de7834893782aa3 Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Mon, 26 May 2025 11:09:27 +0200 Subject: [PATCH 014/110] Remove unused and avoid rechecking FS --- conan/api/subapi/config.py | 7 ++++--- conan/internal/rest/remote_manager.py | 1 - 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/conan/api/subapi/config.py b/conan/api/subapi/config.py index 9577d86c962..c509753e832 100644 --- a/conan/api/subapi/config.py +++ b/conan/api/subapi/config.py @@ -32,7 +32,6 @@ def __init__(self, conan_api): self._new_config = None self._cli_core_confs = None self.hook_manager = HookManager(HomePaths(conan_api.home_folder).hooks_path) - self._compression_plugin = None def home(self): return self.conan_api.cache_folder @@ -238,14 +237,16 @@ def reinit(self): if self._new_config is not None: self._new_config.clear() self._populate_global_conf() - self._compression_plugin = None + if hasattr(self, "_compression_plugin"): + del self._compression_plugin self.hook_manager = HookManager(HomePaths(self.conan_api.home_folder).hooks_path) @property def compression_plugin(self): - if not self._compression_plugin: + if not hasattr(self, "_compression_plugin"): compression_plugin_path = HomePaths(self.conan_api.home_folder).compression_plugin_path if not os.path.exists(compression_plugin_path): + self._compression_plugin = None return None mod, _ = load_python_file(compression_plugin_path) if not hasattr(mod, "tar_extract") or not hasattr(mod, "tar_compress"): diff --git a/conan/internal/rest/remote_manager.py b/conan/internal/rest/remote_manager.py index 10f6617d068..3dce1518b02 100644 --- a/conan/internal/rest/remote_manager.py +++ b/conan/internal/rest/remote_manager.py @@ -1,4 +1,3 @@ -from gzip import compress import os import shutil from typing import List From ac1fc45695fec2acd78f30588448c25a9c9a19fa Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Mon, 26 May 2025 19:47:44 +0200 Subject: [PATCH 015/110] Added test to check extract failure and issue #18259 test --- conan/api/subapi/config.py | 10 +- conan/internal/rest/remote_manager.py | 2 +- conan/internal/util/files.py | 19 +-- .../extensions/test_compression_plugin.py | 108 ++++++++++++++++++ test/unittests/util/files/tar_extract_test.py | 2 + 5 files changed, 128 insertions(+), 13 deletions(-) diff --git a/conan/api/subapi/config.py b/conan/api/subapi/config.py index c509753e832..d03c275b0c2 100644 --- a/conan/api/subapi/config.py +++ b/conan/api/subapi/config.py @@ -32,6 +32,7 @@ def __init__(self, conan_api): self._new_config = None self._cli_core_confs = None self.hook_manager = HookManager(HomePaths(conan_api.home_folder).hooks_path) + self._compression_plugin = None def home(self): return self.conan_api.cache_folder @@ -237,17 +238,16 @@ def reinit(self): if self._new_config is not None: self._new_config.clear() self._populate_global_conf() - if hasattr(self, "_compression_plugin"): - del self._compression_plugin + self._compression_plugin = None self.hook_manager = HookManager(HomePaths(self.conan_api.home_folder).hooks_path) @property def compression_plugin(self): - if not hasattr(self, "_compression_plugin"): + if self._compression_plugin is None: compression_plugin_path = HomePaths(self.conan_api.home_folder).compression_plugin_path if not os.path.exists(compression_plugin_path): - self._compression_plugin = None - return None + self._compression_plugin = False # Avoid FS re-check + return False mod, _ = load_python_file(compression_plugin_path) if not hasattr(mod, "tar_extract") or not hasattr(mod, "tar_compress"): raise ConanException("The 'compression.py' plugin does not contain required `tar_extract` or `tar_compress` functions") diff --git a/conan/internal/rest/remote_manager.py b/conan/internal/rest/remote_manager.py index 3dce1518b02..14b45d089d0 100644 --- a/conan/internal/rest/remote_manager.py +++ b/conan/internal/rest/remote_manager.py @@ -282,7 +282,7 @@ def _call_remote(self, remote, method, *args, **kwargs): raise ConanException(exc, remote=remote) -def uncompress_file(src_path, dest_folder, scope=None, compression_plugin=None): +def uncompress_file(src_path, dest_folder, scope="", compression_plugin=None): try: filesize = os.path.getsize(src_path) big_file = filesize > 10000000 # 10 MB diff --git a/conan/internal/util/files.py b/conan/internal/util/files.py index 98746479ae0..2cd89b90cb5 100644 --- a/conan/internal/util/files.py +++ b/conan/internal/util/files.py @@ -257,13 +257,18 @@ def mkdir(path): def tar_extract(fileobj, destination_dir): - the_tar = tarfile.open(fileobj=fileobj) - # NOTE: The errorlevel=2 has been removed because it was failing in Win10, it didn't allow to - # "could not change modification time", with time=0 - # the_tar.errorlevel = 2 # raise exception if any error - the_tar.extraction_filter = (lambda member, path: member) # fully_trusted, avoid Py3.14 break - the_tar.extractall(path=destination_dir) - the_tar.close() + try: + the_tar = tarfile.open(fileobj=fileobj) + # NOTE: The errorlevel=2 has been removed because it was failing in Win10, it didn't allow to + # "could not change modification time", with time=0 + # the_tar.errorlevel = 2 # raise exception if any error + the_tar.extraction_filter = (lambda member, path: member) # fully_trusted, avoid Py3.14 break + the_tar.extractall(path=destination_dir) + the_tar.close() + except tarfile.ReadError: + raise ConanException(f"Error while extracting {os.path.basename(fileobj.name)}. The file compression is not recogniced.\n" + "This file could have been compressed using a `compression` plugin.\n" + "If your organization uses this plugin, ensure it is correctly installed on your environment.") def merge_directories(src, dst): diff --git a/test/integration/extensions/test_compression_plugin.py b/test/integration/extensions/test_compression_plugin.py index cee4c3b7079..31a1e206e9e 100644 --- a/test/integration/extensions/test_compression_plugin.py +++ b/test/integration/extensions/test_compression_plugin.py @@ -1,6 +1,7 @@ import os import textwrap +from conan.internal.util.files import tar_extract from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient @@ -92,3 +93,110 @@ def tar_extract(src_path, destination_dir, *args, **kwargs): c.run("remove pkg/* -c") c.run("download 'pkg/*' -r=default") assert "Decompressing conan_package.tgz using compression plugin (xz)" in c.out + + +def test_compression_plugin_tar_not_compatible_with_builtin(): + """ + Test that built in tar_extract function fails when uncompressing a non compatible file (a file + which has been compressed using the compression plugin with a different algorithm than the built-in one). + """ + c = TestClient(default_server_user=True) + + compression_plugin = textwrap.dedent( + """ + import os + import zipfile + from conan.api.output import ConanOutput + + # zip compression + def tar_extract(src_path, destination_dir): + # extract tar using zipfile library + ConanOutput().info(f"Decompressing {src_path} using compression plugin (zip)") + with zipfile.ZipFile(src_path, 'r') as zip_ref: + zip_ref.extractall(destination_dir) + + def tar_compress(files, name, dest_dir, compresslevel=None, ref=None, recursive=False): + # compress files using zipfile library taking into account recursive + zip_path = os.path.join(dest_dir, name) + ConanOutput(scope=str(ref) if ref else "").info(f"Compressing {name} using compression plugin (zip)") + with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED, compresslevel=compresslevel) as zipf: + for filename, abs_path in sorted(files.items()): + if recursive: + arcname = os.path.relpath(abs_path, start=os.path.dirname(abs_path)) + zipf.write(abs_path, arcname) + else: + zipf.write(abs_path, filename) + return zip_path + """ + ) + + c.save( + { + os.path.join( + c.cache_folder, "extensions", "plugins", "compression.py" + ): compression_plugin, + "conanfile.py": GenConanfile("pkg", "1.0"), + } + ) + c.run("create .") + c.run("cache save 'pkg/*'") + c.run("remove pkg/* -c") + os.unlink(os.path.join(c.cache_folder, "extensions", "plugins", "compression.py")) + c.run("cache restore conan_cache_save.tgz", assert_error=True) + assert ( + "Error while extracting conan_cache_save.tgz. The file compression is not recogniced.\n" + "This file could have been compressed using a `compression` plugin.\n" + "If your organization uses this plugin, ensure it is correctly installed on your environment." + ) in c.out + + +# https://github.com/conan-io/conan/issues/18259 +def test_compress_in_subdirectory(): + c = TestClient(default_server_user=True) + compression_plugin = textwrap.dedent( + """ + import os + import tarfile + from conan.api.output import ConanOutput + def tar_compress(files, name, dest_dir, compresslevel=None, ref=None, recursive=False, *args, **kwargs): + # compress files using tarfile putting all content in a `conan/` subfolder + tgz_path = os.path.join(dest_dir, name) + ConanOutput(scope=str(ref or "")).info(f"Compressing {name} in conan subfolder") + with open(tgz_path, "wb") as tgz_handle: + tgz = tarfile.open(name, "w", fileobj=tgz_handle) + for filename, abs_path in sorted(files.items()): + tgz.add(abs_path, os.path.join("conan", filename), recursive=recursive) + tgz.close() + return tgz_path + + def tar_extract(src_path, destination_dir, *args, **kwargs): + ConanOutput().info(f"Decompressing {src_path} in conan subfolder") + with open(src_path, mode="rb") as file_handler: + the_tar = tarfile.open(fileobj=file_handler) + the_tar.extraction_filter = (lambda member, path: member) + for member in the_tar.getmembers(): + if member.name.startswith("conan/"): + member.name = member.name[len("conan/"):] # Strip 'conan/' prefix + the_tar.extract(member, path=destination_dir) + the_tar.close() + """ + ) + c.save( + { + os.path.join( + c.cache_folder, "extensions", "plugins", "compression.py" + ): compression_plugin, + "conanfile.py": GenConanfile("pkg", "1.0"), + } + ) + c.run("create .") + c.run("cache save 'pkg/*'") + c.run("remove pkg/* -c") + c.run("cache restore conan_cache_save.tgz") + with open(os.path.join(c.current_folder, "conan_cache_save.tgz"), 'rb') as file_handler: + destination_dir = os.path.join(c.cache_folder, "extracted") + tar_extract(file_handler, destination_dir) + assert os.listdir(destination_dir) == ["conan"] + assert os.path.exists(os.path.join(destination_dir, "conan", "pkglist.json")) + + diff --git a/test/unittests/util/files/tar_extract_test.py b/test/unittests/util/files/tar_extract_test.py index 6f031a56a13..9ca622dbb9d 100644 --- a/test/unittests/util/files/tar_extract_test.py +++ b/test/unittests/util/files/tar_extract_test.py @@ -60,3 +60,5 @@ def check_files(destination_dir): with open(self.tgz_file, 'rb') as file_handler: tar_extract(file_handler, destination_dir) check_files(destination_dir) + + From 181a7366336882124b7a333eae689f0bcc92d611 Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Tue, 27 May 2025 11:24:54 +0200 Subject: [PATCH 016/110] Added config to compression.py interface and renamed parameters --- conan/api/subapi/cache.py | 3 +- conan/api/subapi/config.py | 5 +- conan/internal/api/uploader.py | 20 ++++-- conan/internal/rest/remote_manager.py | 13 ++-- .../extensions/test_compression_plugin.py | 70 +++++++++---------- 5 files changed, 58 insertions(+), 53 deletions(-) diff --git a/conan/api/subapi/cache.py b/conan/api/subapi/cache.py index 024fa6de68b..ca2cf67c907 100644 --- a/conan/api/subapi/cache.py +++ b/conan/api/subapi/cache.py @@ -184,7 +184,8 @@ def restore(self, path): compression_plugin = self.conan_api.config.compression_plugin if compression_plugin: - compression_plugin.tar_extract(path, cache_folder) + compression_plugin.tar_extract(archive_path=path, dest_dir=cache_folder, + config=self.conan_api.config.global_conf) else: with open(path, mode='rb') as file_handler: tar_extract(file_handler, cache_folder) diff --git a/conan/api/subapi/config.py b/conan/api/subapi/config.py index d03c275b0c2..7b8aba3eb2c 100644 --- a/conan/api/subapi/config.py +++ b/conan/api/subapi/config.py @@ -247,10 +247,9 @@ def compression_plugin(self): compression_plugin_path = HomePaths(self.conan_api.home_folder).compression_plugin_path if not os.path.exists(compression_plugin_path): self._compression_plugin = False # Avoid FS re-check - return False + return None mod, _ = load_python_file(compression_plugin_path) if not hasattr(mod, "tar_extract") or not hasattr(mod, "tar_compress"): raise ConanException("The 'compression.py' plugin does not contain required `tar_extract` or `tar_compress` functions") self._compression_plugin = mod - return self._compression_plugin - + return self._compression_plugin or None diff --git a/conan/internal/api/uploader.py b/conan/internal/api/uploader.py index 31c14ef2a49..44585516704 100644 --- a/conan/internal/api/uploader.py +++ b/conan/internal/api/uploader.py @@ -155,9 +155,8 @@ def add_tgz(tgz_name, tgz_files): if os.path.isfile(tgz): result[tgz_name] = tgz elif tgz_files: - compresslevel = self._global_conf.get("core.gzip:compresslevel", check_type=int) tgz = compress_files(tgz_files, tgz_name, download_export_folder, - compresslevel=compresslevel, ref=ref, + config=self._global_conf, ref=ref, compression_plugin=self._app.conan_api.config.compression_plugin) result[tgz_name] = tgz @@ -203,9 +202,8 @@ def _compress_package_files(self, layout, pref): if not os.path.isfile(package_tgz): tgz_files = {f: path for f, path in files.items()} - compresslevel = self._global_conf.get("core.gzip:compresslevel", check_type=int) tgz_path = compress_files(tgz_files, PACKAGE_TGZ_NAME, download_pkg_folder, - compresslevel=compresslevel, ref=pref, + config=self._global_conf, ref=pref, compression_plugin=self._app.conan_api.config.compression_plugin) assert tgz_path == package_tgz assert os.path.exists(package_tgz) @@ -273,15 +271,23 @@ def gzopen_without_timestamps(name, fileobj, compresslevel=None): return t -def compress_files(files, name, dest_dir, compresslevel=None, ref=None, recursive=False, compression_plugin=None): +def compress_files(files, name, dest_dir, config=None, ref=None, recursive=False, compression_plugin=None): + tgz_path = os.path.join(dest_dir, name) if compression_plugin: - return compression_plugin.tar_compress(files, name, dest_dir, compresslevel, ref, recursive) + compression_plugin.tar_compress( + archive_path=tgz_path, + files=files, + recursive=recursive, + config=config, + ref=ref, + ) + return tgz_path t1 = time.time() # FIXME, better write to disk sequentially and not keep tgz contents in memory - tgz_path = os.path.join(dest_dir, name) ConanOutput(scope=str(ref or "")).info(f"Compressing {name}") with set_dirty_context_manager(tgz_path), open(tgz_path, "wb") as tgz_handle: + compresslevel = config.get("core.gzip:compresslevel", check_type=int) if config else None tgz = gzopen_without_timestamps(name, fileobj=tgz_handle, compresslevel=compresslevel) for filename, abs_path in sorted(files.items()): # recursive is False by default in case it is a symlink to a folder diff --git a/conan/internal/rest/remote_manager.py b/conan/internal/rest/remote_manager.py index 14b45d089d0..01e7bfaa552 100644 --- a/conan/internal/rest/remote_manager.py +++ b/conan/internal/rest/remote_manager.py @@ -82,7 +82,7 @@ def get_recipe(self, ref, remote, metadata=None): tgz_file = zipped_files.pop(EXPORT_TGZ_NAME, None) if tgz_file: - uncompress_file(tgz_file, export_folder, scope=str(ref), compression_plugin=self._config_api.compression_plugin) + uncompress_file(tgz_file, export_folder, scope=str(ref), config_api=self._config_api) mkdir(export_folder) for file_name, file_path in zipped_files.items(): # copy CONANFILE shutil.move(file_path, os.path.join(export_folder, file_name)) @@ -124,7 +124,7 @@ def get_recipe_sources(self, ref, layout, remote): self._signer.verify(ref, download_folder, files=zipped_files) tgz_file = zipped_files[EXPORT_SOURCES_TGZ_NAME] - uncompress_file(tgz_file, export_sources_folder, scope=str(ref), compression_plugin=self._config_api.compression_plugin) + uncompress_file(tgz_file, export_sources_folder, scope=str(ref), config_api=self._config_api) def get_package(self, pref, remote, metadata=None): output = ConanOutput(scope=str(pref.ref)) @@ -172,7 +172,7 @@ def _get_package(self, layout, pref, remote, scoped_output, metadata): tgz_file = zipped_files.pop(PACKAGE_TGZ_NAME, None) package_folder = layout.package() - uncompress_file(tgz_file, package_folder, scope=str(pref.ref), compression_plugin=self._config_api.compression_plugin) + uncompress_file(tgz_file, package_folder, scope=str(pref.ref), config_api=self._config_api) mkdir(package_folder) # Just in case it doesn't exist, because uncompress did nothing for file_name, file_path in zipped_files.items(): # copy CONANINFO and CONANMANIFEST shutil.move(file_path, os.path.join(package_folder, file_name)) @@ -282,7 +282,7 @@ def _call_remote(self, remote, method, *args, **kwargs): raise ConanException(exc, remote=remote) -def uncompress_file(src_path, dest_folder, scope="", compression_plugin=None): +def uncompress_file(src_path, dest_folder, scope="", config_api=None): try: filesize = os.path.getsize(src_path) big_file = filesize > 10000000 # 10 MB @@ -290,8 +290,9 @@ def uncompress_file(src_path, dest_folder, scope="", compression_plugin=None): hs = human_size(filesize) ConanOutput(scope=scope).info(f"Decompressing {hs} {os.path.basename(src_path)}") - if compression_plugin: - compression_plugin.tar_extract(src_path, dest_folder) + if config_api and config_api.compression_plugin: + config_api.compression_plugin.tar_extract(archive_path=src_path, dest_dir=dest_folder, + config=config_api.global_conf) else: with open(src_path, mode='rb') as file_handler: tar_extract(file_handler, dest_folder) diff --git a/test/integration/extensions/test_compression_plugin.py b/test/integration/extensions/test_compression_plugin.py index 31a1e206e9e..0bb21e3414b 100644 --- a/test/integration/extensions/test_compression_plugin.py +++ b/test/integration/extensions/test_compression_plugin.py @@ -12,8 +12,8 @@ def test_compression_plugin_not_valid(): c = TestClient() compression_plugin = textwrap.dedent( """ - def tar_compress(files, name, dest_dir, compresslevel=None, ref=None, recursive=False): - return None + def tar_compress(archive_path, files, recursive, config=None, *args, **kwargs): + pass """ ) @@ -47,21 +47,21 @@ def test_compression_plugin_correctly_load(): from conan.api.output import ConanOutput # xz compression - def tar_compress(files, name, dest_dir, compresslevel=None, ref=None, recursive=False, *args, **kwargs): - tgz_path = os.path.join(dest_dir, name) - ConanOutput(scope=str(ref) if ref else "").info(f"Compressing {name} using compression plugin (xz)") + def tar_compress(archive_path, files, recursive, config=None, *args, **kwargs): + name = os.path.basename(archive_path) + ConanOutput().info(f"Compressing {name} using compression plugin (xz)") + compresslevel = config.get("core.gzip:compresslevel", check_type=int) if config else None kwargs = {"preset": compresslevel} if compresslevel else {} - with tarfile.open(tgz_path, f"w:xz", **kwargs) as tgz: + with tarfile.open(archive_path, f"w:xz", **kwargs) as tgz: for filename, abs_path in sorted(files.items()): tgz.add(abs_path, filename, recursive=True) - return tgz_path - def tar_extract(src_path, destination_dir, *args, **kwargs): - ConanOutput().info(f"Decompressing {os.path.basename(src_path)} using compression plugin (xz)") - with open(src_path, mode='rb') as file_handler: + def tar_extract(archive_path, dest_dir, config=None, *args, **kwargs): + ConanOutput().info(f"Decompressing {os.path.basename(archive_path)} using compression plugin (xz)") + with open(archive_path, mode='rb') as file_handler: the_tar = tarfile.open(fileobj=file_handler) the_tar.extraction_filter = (lambda member, path: member) - the_tar.extractall(path=destination_dir) + the_tar.extractall(path=dest_dir) the_tar.close() """ ) @@ -109,24 +109,24 @@ def test_compression_plugin_tar_not_compatible_with_builtin(): from conan.api.output import ConanOutput # zip compression - def tar_extract(src_path, destination_dir): - # extract tar using zipfile library - ConanOutput().info(f"Decompressing {src_path} using compression plugin (zip)") - with zipfile.ZipFile(src_path, 'r') as zip_ref: - zip_ref.extractall(destination_dir) - - def tar_compress(files, name, dest_dir, compresslevel=None, ref=None, recursive=False): + def tar_compress(archive_path, files, recursive, config=None, *args, **kwargs): # compress files using zipfile library taking into account recursive - zip_path = os.path.join(dest_dir, name) - ConanOutput(scope=str(ref) if ref else "").info(f"Compressing {name} using compression plugin (zip)") - with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED, compresslevel=compresslevel) as zipf: + name = os.path.basename(archive_path) + compresslevel = config.get("core.gzip:compresslevel", check_type=int) if config else None + ConanOutput().info(f"Compressing {name} using compression plugin (zip)") + with zipfile.ZipFile(archive_path, 'w', zipfile.ZIP_DEFLATED, compresslevel=compresslevel) as zipf: for filename, abs_path in sorted(files.items()): if recursive: arcname = os.path.relpath(abs_path, start=os.path.dirname(abs_path)) zipf.write(abs_path, arcname) else: zipf.write(abs_path, filename) - return zip_path + + def tar_extract(archive_path, dest_dir, config=None, *args, **kwargs): + # extract tar using zipfile library + ConanOutput().info(f"Decompressing {os.path.basename(archive_path)} using compression plugin (zip)") + with zipfile.ZipFile(archive_path, 'r') as zip_ref: + zip_ref.extractall(dest_dir) """ ) @@ -158,26 +158,25 @@ def test_compress_in_subdirectory(): import os import tarfile from conan.api.output import ConanOutput - def tar_compress(files, name, dest_dir, compresslevel=None, ref=None, recursive=False, *args, **kwargs): + def tar_compress(archive_path, files, recursive, *args, **kwargs): # compress files using tarfile putting all content in a `conan/` subfolder - tgz_path = os.path.join(dest_dir, name) - ConanOutput(scope=str(ref or "")).info(f"Compressing {name} in conan subfolder") - with open(tgz_path, "wb") as tgz_handle: + name = os.path.basename(archive_path) + ConanOutput().info(f"Compressing {os.path.basename(name)} in conan subfolder") + with open(archive_path, "wb") as tgz_handle: tgz = tarfile.open(name, "w", fileobj=tgz_handle) for filename, abs_path in sorted(files.items()): tgz.add(abs_path, os.path.join("conan", filename), recursive=recursive) tgz.close() - return tgz_path - def tar_extract(src_path, destination_dir, *args, **kwargs): - ConanOutput().info(f"Decompressing {src_path} in conan subfolder") - with open(src_path, mode="rb") as file_handler: + def tar_extract(archive_path, dest_dir, *args, **kwargs): + ConanOutput().info(f"Decompressing {archive_path} in conan subfolder") + with open(archive_path, mode="rb") as file_handler: the_tar = tarfile.open(fileobj=file_handler) the_tar.extraction_filter = (lambda member, path: member) for member in the_tar.getmembers(): if member.name.startswith("conan/"): member.name = member.name[len("conan/"):] # Strip 'conan/' prefix - the_tar.extract(member, path=destination_dir) + the_tar.extract(member, path=dest_dir) the_tar.close() """ ) @@ -194,9 +193,8 @@ def tar_extract(src_path, destination_dir, *args, **kwargs): c.run("remove pkg/* -c") c.run("cache restore conan_cache_save.tgz") with open(os.path.join(c.current_folder, "conan_cache_save.tgz"), 'rb') as file_handler: - destination_dir = os.path.join(c.cache_folder, "extracted") - tar_extract(file_handler, destination_dir) - assert os.listdir(destination_dir) == ["conan"] - assert os.path.exists(os.path.join(destination_dir, "conan", "pkglist.json")) - + dest_dir = os.path.join(c.cache_folder, "extracted") + tar_extract(file_handler, dest_dir) + assert os.listdir(dest_dir) == ["conan"] + assert os.path.exists(os.path.join(dest_dir, "conan", "pkglist.json")) From 3b32ec20f991b1bee1c5b6e9f4e4c606062e241f Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Tue, 27 May 2025 11:54:31 +0200 Subject: [PATCH 017/110] Fix invokation --- conan/api/subapi/cache.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/conan/api/subapi/cache.py b/conan/api/subapi/cache.py index ca2cf67c907..afa6d1195de 100644 --- a/conan/api/subapi/cache.py +++ b/conan/api/subapi/cache.py @@ -130,7 +130,6 @@ def save(self, package_list, tgz_path, no_source=False): cache_folder = cache.store # Note, this is not the home, but the actual package cache out = ConanOutput() mkdir(os.path.dirname(tgz_path)) - compresslevel = global_conf.get("core.gzip:compresslevel", check_type=int) tar_files: dict[str,str] = {} # {path_in_tar: abs_path} for ref, ref_bundle in package_list.refs().items(): @@ -171,7 +170,7 @@ def save(self, package_list, tgz_path, no_source=False): pkglist_path = os.path.join(tempfile.gettempdir(), "pkglist.json") save(pkglist_path, serialized) tar_files["pkglist.json"] = pkglist_path - compress_files(tar_files, os.path.basename(tgz_path), os.path.dirname(tgz_path), compresslevel, + compress_files(tar_files, os.path.basename(tgz_path), os.path.dirname(tgz_path), config=self.conan_api.config, recursive=True, ref=None, compression_plugin=self.conan_api.config.compression_plugin) remove(pkglist_path) From b52bce2ffd0be2376a427dd06e7ec643ff9226a4 Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Tue, 27 May 2025 12:24:20 +0200 Subject: [PATCH 018/110] Rename config for conf --- conan/api/subapi/cache.py | 4 ++-- conan/internal/api/uploader.py | 10 +++++----- conan/internal/rest/remote_manager.py | 2 +- .../extensions/test_compression_plugin.py | 14 +++++++------- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/conan/api/subapi/cache.py b/conan/api/subapi/cache.py index afa6d1195de..753229ae15f 100644 --- a/conan/api/subapi/cache.py +++ b/conan/api/subapi/cache.py @@ -170,7 +170,7 @@ def save(self, package_list, tgz_path, no_source=False): pkglist_path = os.path.join(tempfile.gettempdir(), "pkglist.json") save(pkglist_path, serialized) tar_files["pkglist.json"] = pkglist_path - compress_files(tar_files, os.path.basename(tgz_path), os.path.dirname(tgz_path), config=self.conan_api.config, + compress_files(tar_files, os.path.basename(tgz_path), os.path.dirname(tgz_path), conf=self.conan_api.config, recursive=True, ref=None, compression_plugin=self.conan_api.config.compression_plugin) remove(pkglist_path) @@ -184,7 +184,7 @@ def restore(self, path): compression_plugin = self.conan_api.config.compression_plugin if compression_plugin: compression_plugin.tar_extract(archive_path=path, dest_dir=cache_folder, - config=self.conan_api.config.global_conf) + conf=self.conan_api.config.global_conf) else: with open(path, mode='rb') as file_handler: tar_extract(file_handler, cache_folder) diff --git a/conan/internal/api/uploader.py b/conan/internal/api/uploader.py index 44585516704..5b0738ca509 100644 --- a/conan/internal/api/uploader.py +++ b/conan/internal/api/uploader.py @@ -156,7 +156,7 @@ def add_tgz(tgz_name, tgz_files): result[tgz_name] = tgz elif tgz_files: tgz = compress_files(tgz_files, tgz_name, download_export_folder, - config=self._global_conf, ref=ref, + conf=self._global_conf, ref=ref, compression_plugin=self._app.conan_api.config.compression_plugin) result[tgz_name] = tgz @@ -203,7 +203,7 @@ def _compress_package_files(self, layout, pref): if not os.path.isfile(package_tgz): tgz_files = {f: path for f, path in files.items()} tgz_path = compress_files(tgz_files, PACKAGE_TGZ_NAME, download_pkg_folder, - config=self._global_conf, ref=pref, + conf=self._global_conf, ref=pref, compression_plugin=self._app.conan_api.config.compression_plugin) assert tgz_path == package_tgz assert os.path.exists(package_tgz) @@ -271,14 +271,14 @@ def gzopen_without_timestamps(name, fileobj, compresslevel=None): return t -def compress_files(files, name, dest_dir, config=None, ref=None, recursive=False, compression_plugin=None): +def compress_files(files, name, dest_dir, conf=None, ref=None, recursive=False, compression_plugin=None): tgz_path = os.path.join(dest_dir, name) if compression_plugin: compression_plugin.tar_compress( archive_path=tgz_path, files=files, recursive=recursive, - config=config, + conf=conf, ref=ref, ) return tgz_path @@ -287,7 +287,7 @@ def compress_files(files, name, dest_dir, config=None, ref=None, recursive=False # FIXME, better write to disk sequentially and not keep tgz contents in memory ConanOutput(scope=str(ref or "")).info(f"Compressing {name}") with set_dirty_context_manager(tgz_path), open(tgz_path, "wb") as tgz_handle: - compresslevel = config.get("core.gzip:compresslevel", check_type=int) if config else None + compresslevel = conf.get("core.gzip:compresslevel", check_type=int) if conf else None tgz = gzopen_without_timestamps(name, fileobj=tgz_handle, compresslevel=compresslevel) for filename, abs_path in sorted(files.items()): # recursive is False by default in case it is a symlink to a folder diff --git a/conan/internal/rest/remote_manager.py b/conan/internal/rest/remote_manager.py index 01e7bfaa552..a95214826a7 100644 --- a/conan/internal/rest/remote_manager.py +++ b/conan/internal/rest/remote_manager.py @@ -292,7 +292,7 @@ def uncompress_file(src_path, dest_folder, scope="", config_api=None): if config_api and config_api.compression_plugin: config_api.compression_plugin.tar_extract(archive_path=src_path, dest_dir=dest_folder, - config=config_api.global_conf) + conf=config_api.global_conf) else: with open(src_path, mode='rb') as file_handler: tar_extract(file_handler, dest_folder) diff --git a/test/integration/extensions/test_compression_plugin.py b/test/integration/extensions/test_compression_plugin.py index 0bb21e3414b..cb179d020fd 100644 --- a/test/integration/extensions/test_compression_plugin.py +++ b/test/integration/extensions/test_compression_plugin.py @@ -12,7 +12,7 @@ def test_compression_plugin_not_valid(): c = TestClient() compression_plugin = textwrap.dedent( """ - def tar_compress(archive_path, files, recursive, config=None, *args, **kwargs): + def tar_compress(archive_path, files, recursive, conf=None, *args, **kwargs): pass """ ) @@ -47,16 +47,16 @@ def test_compression_plugin_correctly_load(): from conan.api.output import ConanOutput # xz compression - def tar_compress(archive_path, files, recursive, config=None, *args, **kwargs): + def tar_compress(archive_path, files, recursive, conf=None, *args, **kwargs): name = os.path.basename(archive_path) ConanOutput().info(f"Compressing {name} using compression plugin (xz)") - compresslevel = config.get("core.gzip:compresslevel", check_type=int) if config else None + compresslevel = conf.get("core.gzip:compresslevel", check_type=int) if conf else None kwargs = {"preset": compresslevel} if compresslevel else {} with tarfile.open(archive_path, f"w:xz", **kwargs) as tgz: for filename, abs_path in sorted(files.items()): tgz.add(abs_path, filename, recursive=True) - def tar_extract(archive_path, dest_dir, config=None, *args, **kwargs): + def tar_extract(archive_path, dest_dir, conf=None, *args, **kwargs): ConanOutput().info(f"Decompressing {os.path.basename(archive_path)} using compression plugin (xz)") with open(archive_path, mode='rb') as file_handler: the_tar = tarfile.open(fileobj=file_handler) @@ -109,10 +109,10 @@ def test_compression_plugin_tar_not_compatible_with_builtin(): from conan.api.output import ConanOutput # zip compression - def tar_compress(archive_path, files, recursive, config=None, *args, **kwargs): + def tar_compress(archive_path, files, recursive, conf=None, *args, **kwargs): # compress files using zipfile library taking into account recursive name = os.path.basename(archive_path) - compresslevel = config.get("core.gzip:compresslevel", check_type=int) if config else None + compresslevel = conf.get("core.gzip:compresslevel", check_type=int) if conf else None ConanOutput().info(f"Compressing {name} using compression plugin (zip)") with zipfile.ZipFile(archive_path, 'w', zipfile.ZIP_DEFLATED, compresslevel=compresslevel) as zipf: for filename, abs_path in sorted(files.items()): @@ -122,7 +122,7 @@ def tar_compress(archive_path, files, recursive, config=None, *args, **kwargs): else: zipf.write(abs_path, filename) - def tar_extract(archive_path, dest_dir, config=None, *args, **kwargs): + def tar_extract(archive_path, dest_dir, conf=None, *args, **kwargs): # extract tar using zipfile library ConanOutput().info(f"Decompressing {os.path.basename(archive_path)} using compression plugin (zip)") with zipfile.ZipFile(archive_path, 'r') as zip_ref: From 91da7a4b5e5b8478201d05273fcf23ddb9f3a433 Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Tue, 27 May 2025 14:23:29 +0200 Subject: [PATCH 019/110] Added ref on test --- test/integration/extensions/test_compression_plugin.py | 4 ++-- test/unittests/util/files/tar_extract_test.py | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/test/integration/extensions/test_compression_plugin.py b/test/integration/extensions/test_compression_plugin.py index cb179d020fd..43b64b369dd 100644 --- a/test/integration/extensions/test_compression_plugin.py +++ b/test/integration/extensions/test_compression_plugin.py @@ -47,9 +47,9 @@ def test_compression_plugin_correctly_load(): from conan.api.output import ConanOutput # xz compression - def tar_compress(archive_path, files, recursive, conf=None, *args, **kwargs): + def tar_compress(archive_path, files, recursive, conf=None, ref=None, *args, **kwargs): name = os.path.basename(archive_path) - ConanOutput().info(f"Compressing {name} using compression plugin (xz)") + ConanOutput(scope=ref).info(f"Compressing {name} using compression plugin (xz)") compresslevel = conf.get("core.gzip:compresslevel", check_type=int) if conf else None kwargs = {"preset": compresslevel} if compresslevel else {} with tarfile.open(archive_path, f"w:xz", **kwargs) as tgz: diff --git a/test/unittests/util/files/tar_extract_test.py b/test/unittests/util/files/tar_extract_test.py index 9ca622dbb9d..6f031a56a13 100644 --- a/test/unittests/util/files/tar_extract_test.py +++ b/test/unittests/util/files/tar_extract_test.py @@ -60,5 +60,3 @@ def check_files(destination_dir): with open(self.tgz_file, 'rb') as file_handler: tar_extract(file_handler, destination_dir) check_files(destination_dir) - - From 989fde99776b773973b79e2a3ad6b2bd8a5e0679 Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Fri, 20 Jun 2025 09:47:50 +0200 Subject: [PATCH 020/110] Move to different approach: tar encapsulation respecting extensions --- conan/api/subapi/cache.py | 14 ++--- conan/internal/api/uploader.py | 20 +++++-- conan/internal/rest/remote_manager.py | 13 ++--- conan/internal/util/files.py | 52 ++++++++++++++----- .../extensions/test_compression_plugin.py | 37 ++++++------- 5 files changed, 90 insertions(+), 46 deletions(-) diff --git a/conan/api/subapi/cache.py b/conan/api/subapi/cache.py index 753229ae15f..addab4b6a85 100644 --- a/conan/api/subapi/cache.py +++ b/conan/api/subapi/cache.py @@ -181,13 +181,13 @@ def restore(self, path): cache = PkgCache(self.conan_api.cache_folder, self.conan_api.config.global_conf) cache_folder = cache.store # Note, this is not the home, but the actual package cache - compression_plugin = self.conan_api.config.compression_plugin - if compression_plugin: - compression_plugin.tar_extract(archive_path=path, dest_dir=cache_folder, - conf=self.conan_api.config.global_conf) - else: - with open(path, mode='rb') as file_handler: - tar_extract(file_handler, cache_folder) + with open(path, mode="rb") as file_handler: + tar_extract( + fileobj=file_handler, + destination_dir=cache_folder, + compression_plugin=self.conan_api.config.compression_plugin, + conf=self.conan_api.config.global_conf, + ) # Retrieve the package list from the already extracted archive pkglist_path = os.path.join(cache_folder, "pkglist.json") diff --git a/conan/internal/api/uploader.py b/conan/internal/api/uploader.py index 5b0738ca509..abd1ce3322d 100644 --- a/conan/internal/api/uploader.py +++ b/conan/internal/api/uploader.py @@ -4,6 +4,7 @@ import shutil import tarfile import time +from pathlib import Path from conan.internal.conan_app import ConanApp from conan.api.output import ConanOutput @@ -12,7 +13,7 @@ from conan.errors import ConanException from conan.internal.paths import (CONAN_MANIFEST, CONANFILE, EXPORT_SOURCES_TGZ_NAME, EXPORT_TGZ_NAME, PACKAGE_TGZ_NAME, CONANINFO) -from conan.internal.util.files import (clean_dirty, is_dirty, gather_files, +from conan.internal.util.files import (COMPRESSED_PLUGIN_TAR_NAME, clean_dirty, is_dirty, gather_files, remove, set_dirty_context_manager, mkdir, human_size) UPLOAD_POLICY_FORCE = "force-upload" @@ -274,13 +275,26 @@ def gzopen_without_timestamps(name, fileobj, compresslevel=None): def compress_files(files, name, dest_dir, conf=None, ref=None, recursive=False, compression_plugin=None): tgz_path = os.path.join(dest_dir, name) if compression_plugin: - compression_plugin.tar_compress( - archive_path=tgz_path, + t1 = time.time() + compressed_path = compression_plugin.tar_compress( + archive_path=os.path.join(dest_dir, COMPRESSED_PLUGIN_TAR_NAME), files=files, recursive=recursive, conf=conf, ref=ref, ) + ConanOutput().debug(f"{name} compressed in {time.time() - t1} time in plugin") + ConanOutput(scope=str(ref or "")).info(f"Compressing {compressed_path}") + t1 = time.time() + ConanOutput().debug(f"Wrapping {compressed_path} in {name}") + with set_dirty_context_manager(tgz_path), open(tgz_path, "wb") as tgz_handle: + tgz = gzopen_without_timestamps(name, fileobj=tgz_handle, compresslevel=0) + tgz.add(compressed_path, arcname=os.path.basename(compressed_path), recursive=recursive) + tgz.close() + ConanOutput().debug(f"{name} wrapped in {time.time() - t1} time") + # Only remove wrapped if it is different from the tgz_path + if compressed_path != os.path.basename(tgz_path): + remove(compressed_path) return tgz_path t1 = time.time() diff --git a/conan/internal/rest/remote_manager.py b/conan/internal/rest/remote_manager.py index 13ccf6e5efe..9d4d70bf817 100644 --- a/conan/internal/rest/remote_manager.py +++ b/conan/internal/rest/remote_manager.py @@ -304,12 +304,13 @@ def uncompress_file(src_path, dest_folder, scope="", config_api=None): hs = human_size(filesize) ConanOutput(scope=scope).info(f"Decompressing {hs} {os.path.basename(src_path)}") - if config_api and config_api.compression_plugin: - config_api.compression_plugin.tar_extract(archive_path=src_path, dest_dir=dest_folder, - conf=config_api.global_conf) - else: - with open(src_path, mode='rb') as file_handler: - tar_extract(file_handler, dest_folder) + with open(src_path, mode='rb') as file_handler: + tar_extract( + fileobj=file_handler, + destination_dir=dest_folder, + compression_plugin=config_api.compression_plugin if config_api and config_api.compression_plugin else None, + conf=config_api.global_conf if config_api else None + ) except Exception as e: error_msg = "Error while extracting downloaded file '%s' to %s\n%s\n"\ % (src_path, dest_folder, str(e)) diff --git a/conan/internal/util/files.py b/conan/internal/util/files.py index 2cd89b90cb5..a5c172aae20 100644 --- a/conan/internal/util/files.py +++ b/conan/internal/util/files.py @@ -1,4 +1,6 @@ import errno +from pathlib import Path +import tempfile import gzip import hashlib import os @@ -11,10 +13,13 @@ from contextlib import contextmanager +from conan.api.output import ConanOutput from conan.errors import ConanException _DIRTY_FOLDER = ".dirty" +# Name (without extension) of the tar file to be created by the compression plugin +COMPRESSED_PLUGIN_TAR_NAME = "__conan_plugin_compressed_contents__" def set_dirty(folder): dirty_file = os.path.normpath(folder) + _DIRTY_FOLDER @@ -256,20 +261,43 @@ def mkdir(path): os.makedirs(path) -def tar_extract(fileobj, destination_dir): - try: - the_tar = tarfile.open(fileobj=fileobj) - # NOTE: The errorlevel=2 has been removed because it was failing in Win10, it didn't allow to - # "could not change modification time", with time=0 - # the_tar.errorlevel = 2 # raise exception if any error - the_tar.extraction_filter = (lambda member, path: member) # fully_trusted, avoid Py3.14 break - the_tar.extractall(path=destination_dir) - the_tar.close() - except tarfile.ReadError: - raise ConanException(f"Error while extracting {os.path.basename(fileobj.name)}. The file compression is not recogniced.\n" - "This file could have been compressed using a `compression` plugin.\n" +def tar_extract(fileobj, destination_dir, compression_plugin=None, conf=None): + if compression_plugin: + _tar_extract_with_plugin(fileobj, destination_dir, compression_plugin, conf) + return + the_tar = tarfile.open(fileobj=fileobj) + # NOTE: The errorlevel=2 has been removed because it was failing in Win10, it didn't allow to + # "could not change modification time", with time=0 + # the_tar.errorlevel = 2 # raise exception if any error + the_tar.extraction_filter = (lambda member, path: member) # fully_trusted, avoid Py3.14 break + the_tar.extractall(path=destination_dir) + the_tar.close() + if Path(destination_dir).glob(f"{COMPRESSED_PLUGIN_TAR_NAME}.*"): + raise ConanException(f"Error while extracting {os.path.basename(fileobj.name)}.\n" + "This file has been compressed using a `compression` plugin.\n" "If your organization uses this plugin, ensure it is correctly installed on your environment.") +def _tar_extract_with_plugin(fileobj, destination_dir, compression_plugin, conf): + """First remove tar.gz wrapper and then call the plugin to extract""" + with tempfile.TemporaryDirectory() as temp_dir: + t1 = time.time() + the_tar = tarfile.open(fileobj=fileobj) + the_tar.extraction_filter = (lambda member, path: member) # fully_trusted, avoid Py3.14 break + the_tar.extractall(path=temp_dir) + # Check if the tar was compressed with the compression plugin by checking the existence of + # our constant COMPRESSED_PLUGIN_TAR_NAME (without extension as extension is added by the plugin) + if list(Path(temp_dir).glob(f"{COMPRESSED_PLUGIN_TAR_NAME}.*")): + # Get the only extracted file: the plugin tar + plugin_tar_path = os.path.join(temp_dir, the_tar.getnames()[0]) + the_tar.close() + ConanOutput().debug(f"Unwrapped in {time.time() - t1} time") + t1 = time.time() + compression_plugin.tar_extract(archive_path=plugin_tar_path, dest_dir=destination_dir, conf=conf) + ConanOutput().debug(f"Extracted in {time.time() - t1} time on plugin") + else: + # The tar was not compressed using the plugin, copy files to destination + from conan.tools.files import copy + copy(None, pattern="*", src=temp_dir, dst=destination_dir) def merge_directories(src, dst): from conan.tools.files import copy diff --git a/test/integration/extensions/test_compression_plugin.py b/test/integration/extensions/test_compression_plugin.py index 43b64b369dd..82900518c22 100644 --- a/test/integration/extensions/test_compression_plugin.py +++ b/test/integration/extensions/test_compression_plugin.py @@ -1,7 +1,7 @@ import os import textwrap -from conan.internal.util.files import tar_extract +from conan.internal.util.files import COMPRESSED_PLUGIN_TAR_NAME, tar_extract from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient @@ -26,7 +26,7 @@ def tar_compress(archive_path, files, recursive, conf=None, *args, **kwargs): } ) c.run("create .") - c.run("cache save 'pkg/*'", assert_error=True) + c.run("cache save 'pkg/*:*'", assert_error=True) assert ( "ERROR: The 'compression.py' plugin does not contain required `tar_extract` or `tar_compress` functions" in c.out @@ -48,6 +48,7 @@ def test_compression_plugin_correctly_load(): # xz compression def tar_compress(archive_path, files, recursive, conf=None, ref=None, *args, **kwargs): + archive_path += ".xz" name = os.path.basename(archive_path) ConanOutput(scope=ref).info(f"Compressing {name} using compression plugin (xz)") compresslevel = conf.get("core.gzip:compresslevel", check_type=int) if conf else None @@ -55,6 +56,7 @@ def tar_compress(archive_path, files, recursive, conf=None, ref=None, *args, **k with tarfile.open(archive_path, f"w:xz", **kwargs) as tgz: for filename, abs_path in sorted(files.items()): tgz.add(abs_path, filename, recursive=True) + return archive_path def tar_extract(archive_path, dest_dir, conf=None, *args, **kwargs): ConanOutput().info(f"Decompressing {os.path.basename(archive_path)} using compression plugin (xz)") @@ -75,11 +77,12 @@ def tar_extract(archive_path, dest_dir, conf=None, *args, **kwargs): } ) c.run("create .") - c.run("cache save 'pkg/*'") - assert "Compressing conan_cache_save.tgz using compression plugin (xz)" in c.out + c.run("cache save 'pkg/*:*'") + print(c.out) + assert f"Compressing {COMPRESSED_PLUGIN_TAR_NAME}.xz using compression plugin (xz)" in c.out c.run("remove pkg/* -c") c.run("cache restore conan_cache_save.tgz") - assert "Decompressing conan_cache_save.tgz using compression plugin (xz)" in c.out + assert f"Decompressing {COMPRESSED_PLUGIN_TAR_NAME}.xz using compression plugin (xz)" in c.out c.run("list pkg/1.0") assert "Found 1 pkg/version recipes matching pkg/1.0 in local cache" in c.out @@ -87,12 +90,12 @@ def tar_extract(archive_path, dest_dir, conf=None, *args, **kwargs): c.run("remove pkg/* -c") c.run("create .") # Check the plugin is also used on remote interactions - c.run("upload * -r=default -c") - assert "Compressing conan_package.tgz using compression plugin (xz)" in c.out + c.run("upload *:* -r=default -c") + assert f"Compressing {COMPRESSED_PLUGIN_TAR_NAME}.xz using compression plugin (xz)" in c.out assert "pkg/1.0: Uploading recipe" in c.out c.run("remove pkg/* -c") c.run("download 'pkg/*' -r=default") - assert "Decompressing conan_package.tgz using compression plugin (xz)" in c.out + assert f"Decompressing {COMPRESSED_PLUGIN_TAR_NAME}.xz using compression plugin (xz)" in c.out def test_compression_plugin_tar_not_compatible_with_builtin(): @@ -111,6 +114,7 @@ def test_compression_plugin_tar_not_compatible_with_builtin(): # zip compression def tar_compress(archive_path, files, recursive, conf=None, *args, **kwargs): # compress files using zipfile library taking into account recursive + archive_path += ".zip" name = os.path.basename(archive_path) compresslevel = conf.get("core.gzip:compresslevel", check_type=int) if conf else None ConanOutput().info(f"Compressing {name} using compression plugin (zip)") @@ -121,6 +125,7 @@ def tar_compress(archive_path, files, recursive, conf=None, *args, **kwargs): zipf.write(abs_path, arcname) else: zipf.write(abs_path, filename) + return archive_path def tar_extract(archive_path, dest_dir, conf=None, *args, **kwargs): # extract tar using zipfile library @@ -139,13 +144,13 @@ def tar_extract(archive_path, dest_dir, conf=None, *args, **kwargs): } ) c.run("create .") - c.run("cache save 'pkg/*'") + c.run("cache save 'pkg/*:*'") c.run("remove pkg/* -c") os.unlink(os.path.join(c.cache_folder, "extensions", "plugins", "compression.py")) c.run("cache restore conan_cache_save.tgz", assert_error=True) assert ( - "Error while extracting conan_cache_save.tgz. The file compression is not recogniced.\n" - "This file could have been compressed using a `compression` plugin.\n" + "Error while extracting conan_cache_save.tgz.\n" + "This file has been compressed using a `compression` plugin.\n" "If your organization uses this plugin, ensure it is correctly installed on your environment." ) in c.out @@ -160,6 +165,7 @@ def test_compress_in_subdirectory(): from conan.api.output import ConanOutput def tar_compress(archive_path, files, recursive, *args, **kwargs): # compress files using tarfile putting all content in a `conan/` subfolder + archive_path += ".tgz" name = os.path.basename(archive_path) ConanOutput().info(f"Compressing {os.path.basename(name)} in conan subfolder") with open(archive_path, "wb") as tgz_handle: @@ -167,6 +173,7 @@ def tar_compress(archive_path, files, recursive, *args, **kwargs): for filename, abs_path in sorted(files.items()): tgz.add(abs_path, os.path.join("conan", filename), recursive=recursive) tgz.close() + return archive_path def tar_extract(archive_path, dest_dir, *args, **kwargs): ConanOutput().info(f"Decompressing {archive_path} in conan subfolder") @@ -189,12 +196,6 @@ def tar_extract(archive_path, dest_dir, *args, **kwargs): } ) c.run("create .") - c.run("cache save 'pkg/*'") + c.run("cache save 'pkg/*:*'") c.run("remove pkg/* -c") c.run("cache restore conan_cache_save.tgz") - with open(os.path.join(c.current_folder, "conan_cache_save.tgz"), 'rb') as file_handler: - dest_dir = os.path.join(c.cache_folder, "extracted") - tar_extract(file_handler, dest_dir) - assert os.listdir(dest_dir) == ["conan"] - assert os.path.exists(os.path.join(dest_dir, "conan", "pkglist.json")) - From f986651d0afe3867afd27d8376b469e43e315db9 Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Mon, 23 Jun 2025 17:50:41 +0200 Subject: [PATCH 021/110] Fix condition error --- conan/internal/api/uploader.py | 1 - conan/internal/util/files.py | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/conan/internal/api/uploader.py b/conan/internal/api/uploader.py index abd1ce3322d..c1f945fcf31 100644 --- a/conan/internal/api/uploader.py +++ b/conan/internal/api/uploader.py @@ -4,7 +4,6 @@ import shutil import tarfile import time -from pathlib import Path from conan.internal.conan_app import ConanApp from conan.api.output import ConanOutput diff --git a/conan/internal/util/files.py b/conan/internal/util/files.py index a5c172aae20..b7985c733fe 100644 --- a/conan/internal/util/files.py +++ b/conan/internal/util/files.py @@ -272,7 +272,7 @@ def tar_extract(fileobj, destination_dir, compression_plugin=None, conf=None): the_tar.extraction_filter = (lambda member, path: member) # fully_trusted, avoid Py3.14 break the_tar.extractall(path=destination_dir) the_tar.close() - if Path(destination_dir).glob(f"{COMPRESSED_PLUGIN_TAR_NAME}.*"): + if list(Path(destination_dir).glob(f"{COMPRESSED_PLUGIN_TAR_NAME}.*")): raise ConanException(f"Error while extracting {os.path.basename(fileobj.name)}.\n" "This file has been compressed using a `compression` plugin.\n" "If your organization uses this plugin, ensure it is correctly installed on your environment.") @@ -297,6 +297,7 @@ def _tar_extract_with_plugin(fileobj, destination_dir, compression_plugin, conf) else: # The tar was not compressed using the plugin, copy files to destination from conan.tools.files import copy + ConanOutput().debug(f"Extracted in {time.time() - t1} time built in") copy(None, pattern="*", src=temp_dir, dst=destination_dir) def merge_directories(src, dst): From 8009bed44ed708357e4b6e5f915e45f6260aed12 Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Wed, 25 Jun 2025 10:39:15 +0200 Subject: [PATCH 022/110] Addressed some issues --- conan/internal/api/uploader.py | 53 +++++++++++-------- conan/internal/util/files.py | 6 ++- .../extensions/test_compression_plugin.py | 31 ++++++++++- 3 files changed, 64 insertions(+), 26 deletions(-) diff --git a/conan/internal/api/uploader.py b/conan/internal/api/uploader.py index c1f945fcf31..24a49a6c911 100644 --- a/conan/internal/api/uploader.py +++ b/conan/internal/api/uploader.py @@ -272,30 +272,10 @@ def gzopen_without_timestamps(name, fileobj, compresslevel=None): def compress_files(files, name, dest_dir, conf=None, ref=None, recursive=False, compression_plugin=None): - tgz_path = os.path.join(dest_dir, name) if compression_plugin: - t1 = time.time() - compressed_path = compression_plugin.tar_compress( - archive_path=os.path.join(dest_dir, COMPRESSED_PLUGIN_TAR_NAME), - files=files, - recursive=recursive, - conf=conf, - ref=ref, - ) - ConanOutput().debug(f"{name} compressed in {time.time() - t1} time in plugin") - ConanOutput(scope=str(ref or "")).info(f"Compressing {compressed_path}") - t1 = time.time() - ConanOutput().debug(f"Wrapping {compressed_path} in {name}") - with set_dirty_context_manager(tgz_path), open(tgz_path, "wb") as tgz_handle: - tgz = gzopen_without_timestamps(name, fileobj=tgz_handle, compresslevel=0) - tgz.add(compressed_path, arcname=os.path.basename(compressed_path), recursive=recursive) - tgz.close() - ConanOutput().debug(f"{name} wrapped in {time.time() - t1} time") - # Only remove wrapped if it is different from the tgz_path - if compressed_path != os.path.basename(tgz_path): - remove(compressed_path) - return tgz_path + return _compress_files_with_plugin(files, name, dest_dir, conf, ref, recursive, compression_plugin) + tgz_path = os.path.join(dest_dir, name) t1 = time.time() # FIXME, better write to disk sequentially and not keep tgz contents in memory ConanOutput(scope=str(ref or "")).info(f"Compressing {name}") @@ -311,6 +291,35 @@ def compress_files(files, name, dest_dir, conf=None, ref=None, recursive=False, ConanOutput().debug(f"{name} compressed in {duration} time") return tgz_path +def _compress_files_with_plugin(files, name, dest_dir, conf, ref, recursive, compression_plugin): + t1 = time.time() + abs_path_without_extension = os.path.join(dest_dir, COMPRESSED_PLUGIN_TAR_NAME) + ConanOutput(scope=str(ref or "")).info(f"Compressing {name} using compression plugin") + compressed_path = compression_plugin.tar_compress( + archive_path=abs_path_without_extension, + files=files, + recursive=recursive, + conf=conf, + ref=ref, + ) + ConanOutput().debug(f"Compressed {compressed_path} in {time.time() - t1} time") + # Check if compressed_path == abs_path_without_extension + .* (any extension) + path, extension = os.path.splitext(compressed_path) + if path != abs_path_without_extension or not extension: + raise ConanException("The 'compression.py' plugin returned an unexpected path. Plugin should return the 'archive_path' with an extra extension") + + t1 = time.time() + ConanOutput().debug(f"Wrapping {compressed_path} in {name}") + tgz_path = os.path.join(dest_dir, name) + with set_dirty_context_manager(tgz_path), open(tgz_path, "wb") as tgz_handle: + tgz = gzopen_without_timestamps(name, fileobj=tgz_handle, compresslevel=0) + tgz.add(compressed_path, arcname=os.path.basename(compressed_path), recursive=recursive) + tgz.close() + ConanOutput().debug(f"{name} wrapped in {time.time() - t1} time") + # Only remove wrapped if it is different from the tgz_path + if compressed_path != os.path.basename(tgz_path): + remove(compressed_path) + return tgz_path def _total_size(cache_files): total_size = 0 diff --git a/conan/internal/util/files.py b/conan/internal/util/files.py index b7985c733fe..36bbe80daaa 100644 --- a/conan/internal/util/files.py +++ b/conan/internal/util/files.py @@ -265,6 +265,7 @@ def tar_extract(fileobj, destination_dir, compression_plugin=None, conf=None): if compression_plugin: _tar_extract_with_plugin(fileobj, destination_dir, compression_plugin, conf) return + the_tar = tarfile.open(fileobj=fileobj) # NOTE: The errorlevel=2 has been removed because it was failing in Win10, it didn't allow to # "could not change modification time", with time=0 @@ -284,12 +285,13 @@ def _tar_extract_with_plugin(fileobj, destination_dir, compression_plugin, conf) the_tar = tarfile.open(fileobj=fileobj) the_tar.extraction_filter = (lambda member, path: member) # fully_trusted, avoid Py3.14 break the_tar.extractall(path=temp_dir) + extracted_file = the_tar.getnames()[0] + the_tar.close() # Check if the tar was compressed with the compression plugin by checking the existence of # our constant COMPRESSED_PLUGIN_TAR_NAME (without extension as extension is added by the plugin) if list(Path(temp_dir).glob(f"{COMPRESSED_PLUGIN_TAR_NAME}.*")): # Get the only extracted file: the plugin tar - plugin_tar_path = os.path.join(temp_dir, the_tar.getnames()[0]) - the_tar.close() + plugin_tar_path = os.path.join(temp_dir, extracted_file) ConanOutput().debug(f"Unwrapped in {time.time() - t1} time") t1 = time.time() compression_plugin.tar_extract(archive_path=plugin_tar_path, dest_dir=destination_dir, conf=conf) diff --git a/test/integration/extensions/test_compression_plugin.py b/test/integration/extensions/test_compression_plugin.py index 82900518c22..96144d95aa9 100644 --- a/test/integration/extensions/test_compression_plugin.py +++ b/test/integration/extensions/test_compression_plugin.py @@ -1,7 +1,7 @@ import os import textwrap -from conan.internal.util.files import COMPRESSED_PLUGIN_TAR_NAME, tar_extract +from conan.internal.util.files import COMPRESSED_PLUGIN_TAR_NAME from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient @@ -32,6 +32,34 @@ def tar_compress(archive_path, files, recursive, conf=None, *args, **kwargs): in c.out ) +def test_compression_plugin_returning_invalid_path(): + """Test an error is raised if the compression plugin does not return expected path""" + + c = TestClient() + compression_plugin = textwrap.dedent( + """ + def tar_compress(archive_path, files, recursive, conf=None, *args, **kwargs): + return archive_path + def tar_extract(archive_path, dest_dir, conf=None, *args, **kwargs): + pass + """ + ) + + c.save( + { + os.path.join( + c.cache_folder, "extensions", "plugins", "compression.py" + ): compression_plugin, + "conanfile.py": GenConanfile("pkg", "1.0"), + } + ) + c.run("create .") + c.run("cache save 'pkg/*:*'", assert_error=True) + assert ( + "ERROR: The 'compression.py' plugin returned an unexpected path. Plugin should return the 'archive_path' with an extra extension" + in c.out + ) + def test_compression_plugin_correctly_load(): """Test that the compression plugin is correctly loaded and used on: @@ -78,7 +106,6 @@ def tar_extract(archive_path, dest_dir, conf=None, *args, **kwargs): ) c.run("create .") c.run("cache save 'pkg/*:*'") - print(c.out) assert f"Compressing {COMPRESSED_PLUGIN_TAR_NAME}.xz using compression plugin (xz)" in c.out c.run("remove pkg/* -c") c.run("cache restore conan_cache_save.tgz") From 0db1a7b2e38737ca398ab2f8e530775d35394ed2 Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Wed, 25 Jun 2025 11:24:13 +0200 Subject: [PATCH 023/110] Make plugin return compressed extension --- conan/internal/api/uploader.py | 16 ++++++---------- .../extensions/test_compression_plugin.py | 19 +++++++++++-------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/conan/internal/api/uploader.py b/conan/internal/api/uploader.py index 24a49a6c911..04d8c5a6248 100644 --- a/conan/internal/api/uploader.py +++ b/conan/internal/api/uploader.py @@ -295,30 +295,26 @@ def _compress_files_with_plugin(files, name, dest_dir, conf, ref, recursive, com t1 = time.time() abs_path_without_extension = os.path.join(dest_dir, COMPRESSED_PLUGIN_TAR_NAME) ConanOutput(scope=str(ref or "")).info(f"Compressing {name} using compression plugin") - compressed_path = compression_plugin.tar_compress( + compressed_extension = compression_plugin.tar_compress( archive_path=abs_path_without_extension, files=files, recursive=recursive, conf=conf, ref=ref, ) - ConanOutput().debug(f"Compressed {compressed_path} in {time.time() - t1} time") - # Check if compressed_path == abs_path_without_extension + .* (any extension) - path, extension = os.path.splitext(compressed_path) - if path != abs_path_without_extension or not extension: - raise ConanException("The 'compression.py' plugin returned an unexpected path. Plugin should return the 'archive_path' with an extra extension") + ConanOutput().debug(f"Compressed in {time.time() - t1} time") + if not compressed_extension or not compressed_extension.startswith("."): + raise ConanException("The 'compression.py' did not return the compressed extension.") + compressed_path = abs_path_without_extension + compressed_extension t1 = time.time() - ConanOutput().debug(f"Wrapping {compressed_path} in {name}") tgz_path = os.path.join(dest_dir, name) with set_dirty_context_manager(tgz_path), open(tgz_path, "wb") as tgz_handle: tgz = gzopen_without_timestamps(name, fileobj=tgz_handle, compresslevel=0) tgz.add(compressed_path, arcname=os.path.basename(compressed_path), recursive=recursive) tgz.close() ConanOutput().debug(f"{name} wrapped in {time.time() - t1} time") - # Only remove wrapped if it is different from the tgz_path - if compressed_path != os.path.basename(tgz_path): - remove(compressed_path) + remove(compressed_path) return tgz_path def _total_size(cache_files): diff --git a/test/integration/extensions/test_compression_plugin.py b/test/integration/extensions/test_compression_plugin.py index 96144d95aa9..55bbd7257e6 100644 --- a/test/integration/extensions/test_compression_plugin.py +++ b/test/integration/extensions/test_compression_plugin.py @@ -39,7 +39,7 @@ def test_compression_plugin_returning_invalid_path(): compression_plugin = textwrap.dedent( """ def tar_compress(archive_path, files, recursive, conf=None, *args, **kwargs): - return archive_path + return def tar_extract(archive_path, dest_dir, conf=None, *args, **kwargs): pass """ @@ -56,7 +56,7 @@ def tar_extract(archive_path, dest_dir, conf=None, *args, **kwargs): c.run("create .") c.run("cache save 'pkg/*:*'", assert_error=True) assert ( - "ERROR: The 'compression.py' plugin returned an unexpected path. Plugin should return the 'archive_path' with an extra extension" + "ERROR: The 'compression.py' did not return the compressed extension." in c.out ) @@ -76,7 +76,8 @@ def test_compression_plugin_correctly_load(): # xz compression def tar_compress(archive_path, files, recursive, conf=None, ref=None, *args, **kwargs): - archive_path += ".xz" + extension = ".xz" + archive_path += extension name = os.path.basename(archive_path) ConanOutput(scope=ref).info(f"Compressing {name} using compression plugin (xz)") compresslevel = conf.get("core.gzip:compresslevel", check_type=int) if conf else None @@ -84,7 +85,7 @@ def tar_compress(archive_path, files, recursive, conf=None, ref=None, *args, **k with tarfile.open(archive_path, f"w:xz", **kwargs) as tgz: for filename, abs_path in sorted(files.items()): tgz.add(abs_path, filename, recursive=True) - return archive_path + return extension def tar_extract(archive_path, dest_dir, conf=None, *args, **kwargs): ConanOutput().info(f"Decompressing {os.path.basename(archive_path)} using compression plugin (xz)") @@ -141,7 +142,8 @@ def test_compression_plugin_tar_not_compatible_with_builtin(): # zip compression def tar_compress(archive_path, files, recursive, conf=None, *args, **kwargs): # compress files using zipfile library taking into account recursive - archive_path += ".zip" + extension = ".zip" + archive_path += extension name = os.path.basename(archive_path) compresslevel = conf.get("core.gzip:compresslevel", check_type=int) if conf else None ConanOutput().info(f"Compressing {name} using compression plugin (zip)") @@ -152,7 +154,7 @@ def tar_compress(archive_path, files, recursive, conf=None, *args, **kwargs): zipf.write(abs_path, arcname) else: zipf.write(abs_path, filename) - return archive_path + return extension def tar_extract(archive_path, dest_dir, conf=None, *args, **kwargs): # extract tar using zipfile library @@ -192,7 +194,8 @@ def test_compress_in_subdirectory(): from conan.api.output import ConanOutput def tar_compress(archive_path, files, recursive, *args, **kwargs): # compress files using tarfile putting all content in a `conan/` subfolder - archive_path += ".tgz" + extension = ".tgz" + archive_path += extension name = os.path.basename(archive_path) ConanOutput().info(f"Compressing {os.path.basename(name)} in conan subfolder") with open(archive_path, "wb") as tgz_handle: @@ -200,7 +203,7 @@ def tar_compress(archive_path, files, recursive, *args, **kwargs): for filename, abs_path in sorted(files.items()): tgz.add(abs_path, os.path.join("conan", filename), recursive=recursive) tgz.close() - return archive_path + return extension def tar_extract(archive_path, dest_dir, *args, **kwargs): ConanOutput().info(f"Decompressing {archive_path} in conan subfolder") From dcbb29e5f71bbab0d1737108173d1403f25bb50e Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Mon, 14 Jul 2025 12:46:45 +0200 Subject: [PATCH 024/110] Adapt changes to support metadata in wrapped files Add new test case to check those files are correctly pruned after extraction Improve performance by extracting in the actual destination folder even when plugin is enabled --- conan/internal/api/uploader.py | 1 + conan/internal/util/files.py | 44 ++++++++++--------- .../extensions/test_compression_plugin.py | 42 +++++++++++++++++- 3 files changed, 65 insertions(+), 22 deletions(-) diff --git a/conan/internal/api/uploader.py b/conan/internal/api/uploader.py index 04d8c5a6248..7960ebc1226 100644 --- a/conan/internal/api/uploader.py +++ b/conan/internal/api/uploader.py @@ -303,6 +303,7 @@ def _compress_files_with_plugin(files, name, dest_dir, conf, ref, recursive, com ref=ref, ) ConanOutput().debug(f"Compressed in {time.time() - t1} time") + ConanOutput().success(f"{time.time() - t1}") if not compressed_extension or not compressed_extension.startswith("."): raise ConanException("The 'compression.py' did not return the compressed extension.") diff --git a/conan/internal/util/files.py b/conan/internal/util/files.py index 36bbe80daaa..40a989cc5e7 100644 --- a/conan/internal/util/files.py +++ b/conan/internal/util/files.py @@ -278,29 +278,33 @@ def tar_extract(fileobj, destination_dir, compression_plugin=None, conf=None): "This file has been compressed using a `compression` plugin.\n" "If your organization uses this plugin, ensure it is correctly installed on your environment.") + def _tar_extract_with_plugin(fileobj, destination_dir, compression_plugin, conf): """First remove tar.gz wrapper and then call the plugin to extract""" - with tempfile.TemporaryDirectory() as temp_dir: - t1 = time.time() - the_tar = tarfile.open(fileobj=fileobj) - the_tar.extraction_filter = (lambda member, path: member) # fully_trusted, avoid Py3.14 break - the_tar.extractall(path=temp_dir) - extracted_file = the_tar.getnames()[0] - the_tar.close() - # Check if the tar was compressed with the compression plugin by checking the existence of - # our constant COMPRESSED_PLUGIN_TAR_NAME (without extension as extension is added by the plugin) - if list(Path(temp_dir).glob(f"{COMPRESSED_PLUGIN_TAR_NAME}.*")): - # Get the only extracted file: the plugin tar - plugin_tar_path = os.path.join(temp_dir, extracted_file) - ConanOutput().debug(f"Unwrapped in {time.time() - t1} time") + t1 = time.time() + the_tar = tarfile.open(fileobj=fileobj) + the_tar.extraction_filter = (lambda member, path: member) # fully_trusted, avoid Py3.14 break + the_tar.extractall(path=destination_dir) + extracted_files = the_tar.getnames() + the_tar.close() + # Check if the tar was compressed with the compression plugin by checking the existence of + # our constant COMPRESSED_PLUGIN_TAR_NAME (without extension as extension is added by the plugin) + ConanOutput().success(f"{time.time() - t1}") + for path in extracted_files: + if os.path.basename(path).startswith(COMPRESSED_PLUGIN_TAR_NAME): + # Extract the actual contents from the plugin tar (ignore other files present). + ConanOutput().debug(f"Unwrapped in {time.time() - t1}") t1 = time.time() - compression_plugin.tar_extract(archive_path=plugin_tar_path, dest_dir=destination_dir, conf=conf) - ConanOutput().debug(f"Extracted in {time.time() - t1} time on plugin") - else: - # The tar was not compressed using the plugin, copy files to destination - from conan.tools.files import copy - ConanOutput().debug(f"Extracted in {time.time() - t1} time built in") - copy(None, pattern="*", src=temp_dir, dst=destination_dir) + compression_plugin.tar_extract( + archive_path=os.path.join(destination_dir, path), + dest_dir=destination_dir, + conf=conf, + ) + # Remove extracted files from tar + for f in extracted_files: + remove(os.path.join(destination_dir, f)) + break + ConanOutput().debug(f"Extracted in {time.time() - t1}") def merge_directories(src, dst): from conan.tools.files import copy diff --git a/test/integration/extensions/test_compression_plugin.py b/test/integration/extensions/test_compression_plugin.py index 55bbd7257e6..0090d03fd34 100644 --- a/test/integration/extensions/test_compression_plugin.py +++ b/test/integration/extensions/test_compression_plugin.py @@ -1,7 +1,8 @@ import os import textwrap +import tarfile -from conan.internal.util.files import COMPRESSED_PLUGIN_TAR_NAME +from conan.internal.util.files import COMPRESSED_PLUGIN_TAR_NAME, mkdir from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient @@ -227,5 +228,42 @@ def tar_extract(archive_path, dest_dir, *args, **kwargs): ) c.run("create .") c.run("cache save 'pkg/*:*'") + tgz = os.path.join(c.current_folder, "conan_cache_save.tgz") + assert os.path.exists(tgz) + + mkdir("extract_folder") + destination_dir = os.path.join(c.current_folder, "extract_folder") + extracted_files = _tar_extract(tgz, destination_dir) + assert extracted_files == [COMPRESSED_PLUGIN_TAR_NAME + ".tgz"] + # Create example files + c.save({os.path.join(destination_dir, "README.md"): "This is a readme file.", + os.path.join(destination_dir, "Cache-contents-graph-report.html"): "Cache contents graph report", + os.path.join(destination_dir, "Cache-contents-graph-report.exe"): "executable file...", + os.path.join(destination_dir, "conan_cache_save.tgz"): "this should also be ignored", + }) + # Recompress the file with metadata + _tar_compress(os.path.join(c.current_folder, "conan_cache_save_rearchived.tgz"), destination_dir) + c.run("remove pkg/* -c") - c.run("cache restore conan_cache_save.tgz") + c.run("cache restore conan_cache_save_rearchived.tgz") + + # Check any of the metadata are present in the cache + assert any(item not in os.listdir(os.path.join(c.cache_folder, "p")) for item in ("README.md", + "Cache-contents-graph-report.html", + "Cache-contents-graph-report.exe")) + + +def _tar_extract(tgz_path, destination_dir): + with open(tgz_path, "rb") as fileobj: + the_tar = tarfile.open(fileobj=fileobj) + the_tar.extraction_filter = (lambda member, path: member) + the_tar.extractall(path=destination_dir) + return the_tar.getnames() + + +def _tar_compress(archive_path, folder): + with open(archive_path, "wb") as tgz_handle: + tgz = tarfile.open(os.path.basename(archive_path), "w", fileobj=tgz_handle) + for filename in os.listdir(folder): + tgz.add(os.path.join(folder, filename), filename, recursive=True) + tgz.close() From 8b50ae079bd28ff0a0d899666e2bb8c075ed700c Mon Sep 17 00:00:00 2001 From: memsharded Date: Tue, 13 Jan 2026 17:25:24 +0100 Subject: [PATCH 025/110] branching from Perseo --- .ci/docker/conan-tests | 22 +- .github/CONTRIBUTING.md | 14 +- .github/workflows/build-binaries.yml | 68 ++ .github/workflows/main.yml | 24 +- .github/workflows/osx-tests.yml | 55 +- .github/workflows/win-tests.yml | 10 +- README.md | 2 +- conan/__init__.py | 2 +- conan/api/conan_api.py | 61 +- conan/api/input.py | 2 +- conan/api/model/list.py | 159 ++- conan/api/model/remote.py | 21 +- conan/api/output.py | 42 +- conan/api/subapi/audit.py | 23 +- conan/api/subapi/cache.py | 98 +- conan/api/subapi/config.py | 232 +++- conan/api/subapi/download.py | 21 +- conan/api/subapi/export.py | 101 +- conan/api/subapi/graph.py | 30 +- conan/api/subapi/install.py | 21 +- conan/api/subapi/list.py | 116 +- conan/api/subapi/lockfile.py | 21 +- conan/api/subapi/new.py | 2 +- conan/api/subapi/profiles.py | 7 +- conan/api/subapi/remotes.py | 30 +- conan/api/subapi/report.py | 20 +- conan/api/subapi/upload.py | 99 +- conan/api/subapi/workspace.py | 149 ++- conan/cli/args.py | 121 +- conan/cli/cli.py | 13 +- conan/cli/command.py | 33 +- conan/cli/commands/audit.py | 11 +- conan/cli/commands/build.py | 4 +- conan/cli/commands/cache.py | 19 +- conan/cli/commands/config.py | 29 +- conan/cli/commands/create.py | 10 +- conan/cli/commands/download.py | 2 +- conan/cli/commands/editable.py | 13 +- conan/cli/commands/export.py | 6 +- conan/cli/commands/export_pkg.py | 51 +- conan/cli/commands/graph.py | 7 +- conan/cli/commands/inspect.py | 4 +- conan/cli/commands/install.py | 47 +- conan/cli/commands/list.py | 9 +- conan/cli/commands/lock.py | 74 +- conan/cli/commands/remote.py | 28 +- conan/cli/commands/remove.py | 54 +- conan/cli/commands/report.py | 3 +- conan/cli/commands/run.py | 55 + conan/cli/commands/search.py | 5 +- conan/cli/commands/source.py | 4 +- conan/cli/commands/test.py | 4 +- conan/cli/commands/upload.py | 39 +- conan/cli/commands/workspace.py | 72 +- conan/cli/formatters/audit/vulnerabilities.py | 172 ++- conan/cli/formatters/graph/graph_info_text.py | 18 +- conan/cli/formatters/graph/info_graph_html.py | 39 + conan/cli/formatters/report/diff.py | 74 +- conan/cli/formatters/report/diff_html.py | 890 +++++++++++++- conan/cps/cps.py | 66 +- conan/internal/api/audit/providers.py | 30 +- conan/internal/api/config/config_installer.py | 7 +- conan/internal/api/detect/detect_api.py | 12 +- conan/internal/api/detect/detect_vs.py | 3 +- conan/internal/api/export.py | 26 +- conan/internal/api/install/generators.py | 33 +- conan/internal/api/list/query_parse.py | 4 +- conan/internal/api/new/basic.py | 1 + conan/internal/api/new/bazel_7_exe.py | 2 +- conan/internal/api/new/workspace.py | 38 +- conan/internal/api/profile/profile_loader.py | 31 +- conan/internal/api/upload.py | 14 +- conan/internal/api/uploader.py | 215 ++-- conan/internal/cache/cache.py | 52 +- conan/internal/cache/db/cache_database.py | 23 +- conan/internal/cache/db/packages_table.py | 40 +- conan/internal/cache/db/recipes_table.py | 40 +- conan/internal/cache/db/table.py | 34 +- conan/internal/cache/integrity_check.py | 25 +- conan/internal/conan_app.py | 8 +- conan/internal/default_settings.py | 38 +- conan/internal/deploy.py | 9 +- conan/internal/graph/compatibility.py | 18 +- conan/internal/graph/compute_pid.py | 18 +- conan/internal/graph/graph.py | 14 +- conan/internal/graph/graph_binaries.py | 226 ++-- conan/internal/graph/graph_builder.py | 67 +- conan/internal/graph/graph_error.py | 19 +- conan/internal/graph/install_graph.py | 2 +- conan/internal/graph/installer.py | 43 +- conan/internal/graph/proxy.py | 35 +- conan/internal/graph/python_requires.py | 4 +- conan/internal/graph/range_resolver.py | 8 + conan/internal/hook_manager.py | 9 +- conan/internal/internal_tools.py | 3 +- conan/internal/loader.py | 3 +- conan/internal/methods.py | 3 + conan/internal/model/conan_file.py | 8 +- conan/internal/model/conanconfig.py | 36 + conan/internal/model/conanfile_interface.py | 10 + conan/internal/model/conf.py | 25 +- conan/internal/model/cpp_info.py | 2 +- conan/internal/model/dependencies.py | 10 +- conan/internal/model/info.py | 8 + conan/internal/model/layout.py | 6 +- conan/internal/model/lockfile.py | 4 +- conan/internal/model/manifest.py | 11 +- conan/internal/model/options.py | 5 + conan/internal/model/profile.py | 5 +- conan/internal/model/requires.py | 4 +- conan/internal/model/workspace.py | 45 +- conan/internal/paths.py | 8 +- .../internal/rest/caching_file_downloader.py | 1 - conan/internal/rest/client_routes.py | 7 +- conan/internal/rest/conan_requester.py | 21 +- conan/internal/rest/download_cache.py | 16 +- conan/internal/rest/file_downloader.py | 4 +- conan/internal/rest/file_uploader.py | 2 +- conan/internal/rest/pkg_sign.py | 6 +- conan/internal/rest/remote_manager.py | 108 +- conan/internal/rest/rest_client.py | 8 +- .../rest/rest_client_local_recipe_index.py | 11 +- conan/internal/rest/rest_client_v2.py | 55 +- conan/internal/rest/rest_routes.py | 2 +- conan/internal/runner/docker.py | 6 +- conan/internal/runner/output.py | 1 + conan/internal/util/__init__.py | 31 + conan/internal/util/files.py | 3 + conan/test/assets/genconanfile.py | 2 +- conan/test/assets/premake.py | 7 +- conan/test/assets/visual_project_files.py | 2 - conan/test/utils/artifactory.py | 4 +- conan/test/utils/mocks.py | 2 +- conan/test/utils/server_launcher.py | 4 +- conan/test/utils/test_files.py | 5 +- conan/test/utils/tools.py | 24 +- conan/tools/android/utils.py | 3 +- conan/tools/apple/apple.py | 53 +- conan/tools/apple/xcodebuild.py | 17 +- conan/tools/apple/xcodedeps.py | 15 +- conan/tools/apple/xcodetoolchain.py | 11 +- conan/tools/build/__init__.py | 1 + conan/tools/build/compiler.py | 44 + conan/tools/build/cpu.py | 37 +- conan/tools/build/cstd.py | 3 + conan/tools/build/flags.py | 70 +- conan/tools/cmake/__init__.py | 28 +- conan/tools/cmake/cmake.py | 71 +- .../__init__.py | 0 .../cmakeconfigdeps.py} | 51 +- .../{cmakedeps2 => cmakeconfigdeps}/config.py | 94 +- .../config_version.py | 2 +- .../target_configuration.py | 105 +- .../targets.py | 0 conan/tools/cmake/cmakedeps/cmakedeps.py | 16 +- .../cmake/cmakedeps/templates/__init__.py | 2 +- .../tools/cmake/cmakedeps/templates/config.py | 24 + .../cmake/cmakedeps/templates/target_data.py | 10 +- conan/tools/cmake/layout.py | 7 +- conan/tools/cmake/presets.py | 21 +- conan/tools/cmake/toolchain/blocks.py | 62 +- conan/tools/cmake/toolchain/toolchain.py | 30 +- conan/tools/cmake/utils.py | 40 + conan/tools/env/environment.py | 197 ++- conan/tools/files/__init__.py | 1 - conan/tools/files/conandata.py | 3 +- conan/tools/files/copy_pattern.py | 7 +- conan/tools/files/files.py | 30 +- conan/tools/files/packager.py | 99 -- conan/tools/gnu/autotools.py | 11 +- conan/tools/gnu/autotoolstoolchain.py | 31 +- conan/tools/gnu/gnudeps_flags.py | 2 +- conan/tools/gnu/gnutoolchain.py | 33 +- conan/tools/google/bazel.py | 2 +- conan/tools/google/bazeldeps.py | 31 +- conan/tools/intel/intel_cc.py | 6 +- conan/tools/layout/__init__.py | 3 + conan/tools/meson/helpers.py | 15 +- conan/tools/meson/meson.py | 11 +- conan/tools/meson/toolchain.py | 14 +- conan/tools/microsoft/msbuild.py | 6 +- conan/tools/microsoft/nmakedeps.py | 2 +- conan/tools/microsoft/nmaketoolchain.py | 25 +- conan/tools/microsoft/toolchain.py | 52 +- conan/tools/microsoft/visual.py | 28 +- conan/tools/premake/premake.py | 11 +- conan/tools/premake/premakedeps.py | 2 +- conan/tools/qbs/qbs.py | 2 +- conan/tools/ros/rosenv.py | 53 +- conan/tools/sbom/cyclonedx.py | 53 +- conan/tools/sbom/spdx_licenses.py | 1 - conan/tools/system/__init__.py | 1 + conan/tools/system/package_manager.py | 90 +- conan/tools/system/pip_manager.py | 73 ++ conans/migrations.py | 2 +- conans/model/package_ref.py | 31 - conans/model/recipe_ref.py | 31 - conans/requirements.txt | 4 +- conans/requirements_dev.txt | 2 - conans/server/rest/controller/v2/search.py | 5 +- conans/server/service/authorize.py | 6 +- conans/server/service/v2/search.py | 13 +- setup.py | 10 +- setup_server.py | 9 +- test/README.md | 16 +- test/conftest.py | 32 +- test/functional/command/report_test.py | 112 +- ...install_test.py => test_config_install.py} | 807 +++++++------ .../functional/command/test_install_deploy.py | 67 +- test/functional/conftest.py | 2 +- .../test_build_system_layout_helpers.py | 5 +- .../layout/test_layout_autopackage.py | 393 ------ test/functional/layout/test_source_folder.py | 2 +- test/functional/only_source_test.py | 151 --- test/functional/revisions_test.py | 209 ++-- test/functional/sbom/test_cyclonedx.py | 21 +- test/functional/subsystems_build_test.py | 3 +- .../toolchains/apple/test_xcodebuild.py | 66 + .../apple/test_xcodedeps_components.py | 14 +- .../toolchains/apple/test_xcodetoolchain.py | 2 +- .../toolchains/autotools}/__init__.py | 0 .../autotools/test_universal_binaries.py | 67 ++ .../cmake/cmakedeps/test_apple_frameworks.py | 2 +- .../cmakedeps/test_cmakedeps_components.py | 33 +- .../test_cmakedeps_components_names.py | 53 +- .../test_cmakedeps_custom_configs.py | 194 ++- .../test_cmakedeps_find_module_and_config.py | 10 +- .../cmakedeps2/test_cmakeconfigdeps_new.py | 48 +- .../cmake/test_cmake_and_no_soname_flag.py | 2 +- .../cmake/test_cmake_extra_variables.py | 54 + .../toolchains/cmake/test_cmake_find_none.py | 159 ++- .../toolchains/cmake/test_cmake_multi.py | 103 ++ .../toolchains/cmake/test_cmake_toolchain.py | 86 +- .../test_cmake_toolchain_vxworks_clang.py | 127 -- .../cmake/test_cmake_toolchain_win_clang.py | 38 +- .../cmake/test_cmake_toolchain_xcode_flags.py | 13 +- .../cmake/test_cmaketoolchain_paths.py | 2 +- test/functional/toolchains/cmake/test_cps.py | 559 +++++++++ .../functional/toolchains/cmake/test_ninja.py | 3 +- .../cmake/test_universal_binaries.py | 8 +- .../toolchains/emscripten}/__init__.py | 0 .../gnu/autotools/test_apple_toolchain.py | 2 - .../toolchains/gnu/autotools/test_win_bash.py | 13 +- .../toolchains/gnu/test_gnutoolchain_apple.py | 2 - .../toolchains/gnu/test_pkgconfigdeps.py | 2 - .../toolchains/gnu/test_universal_binaries.py | 66 + .../toolchains/google/test_bazel.py | 57 +- .../test_bazeltoolchain_cross_compilation.py | 2 +- .../toolchains/ios/test_using_cmake.py | 110 +- test/functional/toolchains/meson/_base.py | 54 +- .../toolchains/meson/test_backend.py | 2 - .../meson/test_cross_compilation.py | 49 +- .../toolchains/meson/test_install.py | 29 +- .../functional/toolchains/meson/test_meson.py | 95 +- .../meson/test_meson_and_gnu_deps_flags.py | 4 +- .../toolchains/meson/test_meson_and_objc.py | 5 - .../toolchains/meson/test_pkg_config_reuse.py | 28 +- .../meson/test_preprocessor_definitions.py | 40 +- .../toolchains/meson/test_subproject.py | 18 +- test/functional/toolchains/meson/test_test.py | 22 +- .../meson/test_v2_meson_template.py | 2 + .../toolchains/microsoft/test_msbuilddeps.py | 505 ++++---- .../microsoft/test_msbuildtoolchain.py | 40 + test/functional/toolchains/test_basic.py | 36 +- .../toolchains/test_nmake_toolchain.py | 40 +- test/functional/toolchains/test_premake.py | 77 +- .../functional/toolchains/test_txt_cmdline.py | 61 - .../tools/system/package_manager_test.py | 8 - .../tools/system/pip_manager_test.py | 141 +++ test/functional/tools/test_apple_tools.py | 8 +- test/functional/workspace/test_workspace.py | 55 +- .../build_requires/build_requires_test.py | 143 ++- .../profile_build_requires_test.py | 3 +- .../test_install_test_build_require.py | 6 +- .../build_requires/test_toolchain_packages.py | 3 +- test/integration/cache/backup_sources_test.py | 13 +- test/integration/cache/cache2_update_test.py | 78 +- test/integration/cache/storage_path_test.py | 17 + .../cache/test_home_special_char.py | 3 - .../cache/test_package_revisions.py | 2 +- .../command/cache/test_cache_clean.py | 17 +- .../command/cache/test_cache_integrity.py | 51 +- .../command/cache/test_cache_save_restore.py | 32 +- test/integration/command/create_test.py | 46 +- .../command/custom_commands_test.py | 33 + .../download_selected_packages_test.py | 2 +- .../command/export/export_path_test.py | 2 +- .../export}/export_sources_test.py | 0 .../integration/command/export/export_test.py | 4 +- .../integration/command/export/test_export.py | 18 + test/integration/command/export_pkg_test.py | 2 +- test/integration/command/help_test.py | 5 +- test/integration/command/info/info_test.py | 68 +- .../command/info/test_graph_info_graphical.py | 11 + .../command/info/test_info_build_order.py | 16 +- .../command/install/install_test.py | 8 +- .../command/install/install_update_test.py | 6 +- .../command/install/test_graph_build_mode.py | 31 + test/integration/command/list/list_test.py | 29 +- .../list/test_combined_pkglist_flows.py | 170 ++- .../integration/command/list/test_list_lru.py | 17 + .../command/remote}/__init__.py | 0 .../command/{ => remote}/remote_test.py | 48 +- .../{ => remote}/remote_verify_ssl_test.py | 0 .../command/{ => remote}/test_remote_users.py | 2 +- test/integration/command/remove_test.py | 2 +- test/integration/command/source_test.py | 2 +- test/integration/command/test_audit.py | 33 +- test/integration/command/test_build.py | 2 +- .../command/test_graph_find_binaries.py | 36 +- test/integration/command/test_inspect.py | 2 +- test/integration/command/test_new.py | 10 +- test/integration/command/test_outdated.py | 2 +- test/integration/command/test_output.py | 4 +- test/integration/command/test_package_test.py | 14 +- test/integration/command/test_run.py | 105 ++ .../command/upload/test_upload_bundle.py | 22 +- .../command/upload/upload_compression_test.py | 2 +- .../integration/command/upload/upload_test.py | 9 +- test/integration/conanfile/init_test.py | 33 + .../conanfile/required_conan_version_test.py | 2 +- test/integration/conanfile/test_deprecated.py | 17 - .../conf/test_auth_source_plugin.py | 86 +- .../configuration/conf/test_conf.py | 2 +- .../custom_setting_test_package_test.py | 2 +- .../configuration/default_profile_test.py | 13 +- .../integration/configuration/profile_test.py | 32 +- .../configuration/requester_test.py | 8 +- .../configuration/required_version_test.py | 2 +- .../configuration/test_auth_remote_plugin.py | 17 +- .../configuration/test_profile_jinja.py | 18 + test/integration/cps/test_cps.py | 98 +- .../integration/editable/editable_add_test.py | 2 +- .../editable/editable_remove_test.py | 2 +- .../editable/test_editable_envvars.py | 108 +- .../environment/test_buildenv_profile.py | 17 + test/integration/environment/test_env.py | 141 ++- .../integration/extensions/hooks/hook_test.py | 11 + .../graph/core/graph_manager_base.py | 3 +- .../graph/core/graph_manager_test.py | 39 +- test/integration/graph/core/test_alias.py | 2 +- .../graph/core/test_auto_package_type.py | 1 + .../graph/core/test_build_requires.py | 53 +- test/integration/graph/core/test_provides.py | 23 +- .../graph/core/test_version_ranges.py | 14 +- .../graph/test_dependencies_visit.py | 23 +- .../graph/test_divergent_cppstd_build_host.py | 11 +- .../graph/test_platform_requires.py | 2 +- .../graph/test_replace_requires.py | 30 + .../graph/test_require_same_pkg_versions.py | 142 +++ test/integration/graph/test_system_tools.py | 1 - test/integration/graph/test_test_requires.py | 11 +- .../version_ranges_cached_test.py | 2 +- test/integration/layout/devflow_test.py | 43 +- .../lockfile/test_lock_pyrequires.py | 80 +- .../lockfile/test_lock_requires.py | 2 +- .../lockfile/test_user_overrides.py | 54 +- .../metadata/test_metadata_commands.py | 89 +- .../metadata/test_metadata_deploy.py | 2 +- test/integration/options/options_test.py | 79 +- .../options/test_configure_options.py | 10 +- .../integration/package_id/compatible_test.py | 249 +++- .../package_id_requires_modes_test.py | 18 +- .../python_requires_package_id_test.py | 2 +- .../package_id/test_cache_compatibles.py | 20 + .../package_id/test_config_package_id.py | 14 +- test/integration/package_id/test_validate.py | 121 +- .../package_id/transitive_header_only_test.py | 1 - .../transitive_options_affect_id_test.py | 42 + .../py_requires/python_requires_test.py | 2 +- .../remote/broken_download_test.py | 6 +- test/integration/remote/rest_api_test.py | 3 +- test/integration/remote/retry_test.py | 6 +- .../remote/test_local_recipes_index.py | 45 + .../remote/test_remote_recipes_only.py | 84 ++ .../settings/settings_override_test.py | 4 +- test/integration/symlinks/symlinks_test.py | 55 +- test/integration/test_components.py | 1 + test/integration/test_components_error.py | 25 + test/integration/test_compressions.py | 143 +++ test/integration/test_migrations.py | 37 +- test/integration/test_recipe_policies.py | 53 + test/integration/tgz_macos_dot_files_test.py | 6 +- .../toolchains/apple/test_xcodedeps.py | 59 + .../cmake/cmakedeps/test_cmakedeps.py | 31 + .../test_cmakeconfigdeps_frameworks.py | 24 + .../cmake/cmakedeps2/test_cmakedeps.py | 355 ++++-- .../toolchains/cmake/test_cmaketoolchain.py | 32 +- .../toolchains/env/test_buildenv.py | 40 + .../toolchains/gnu/test_autotoolstoolchain.py | 7 +- .../toolchains/gnu/test_basic_layout.py | 57 + .../toolchains/gnu/test_gnutoolchain.py | 6 +- .../toolchains/gnu/test_makedeps.py | 1 - .../toolchains/google/test_bazeldeps.py | 181 ++- .../toolchains/intel/test_intel_cc.py | 14 + .../toolchains/meson/test_mesontoolchain.py | 57 +- .../toolchains/microsoft/vcvars_test.py | 75 +- .../toolchains/premake/test_premake.py | 32 +- .../toolchains/premake/test_premakedeps.py | 4 +- .../test_raise_on_universal_binaries.py | 96 +- test/integration/tools/ros/test_rosenv.py | 37 +- .../tools/system/package_manager_test.py | 186 ++- test/integration/workspace/test_workspace.py | 448 ++++++- .../test_compatibility_performance.py | 129 ++ test/performance/test_db_performance.py | 74 ++ test/unittests/cli/test_cli_ref_matching.py | 7 + .../client/build/c_std_flags_test.py | 77 +- .../client/build/compiler_flags_test.py | 177 +-- .../client/build/cpp_std_flags_test.py | 106 +- .../client/command/parse_arguments_test.py | 1 + .../unittests/client/conanfile_loader_test.py | 1 - .../client/conf/detect/test_gcc_compiler.py | 2 +- .../unittests/client/graph/deps_graph_test.py | 2 +- test/unittests/client/remote_manager_test.py | 65 +- test/unittests/client/rest/requester_test.py | 2 +- test/unittests/client/rest/response_test.py | 2 +- .../rest_client_v2/rest_client_v2_test.py | 22 +- .../toolchain/autotools/autotools_test.py | 77 -- .../client/tools/cppstd_required_test.py | 2 +- .../client/tools/files/rename_test.py | 64 +- test/unittests/client/tools/test_env.py | 2 +- test/unittests/client/userio_test.py | 1 - .../model/build_info/components_test.py | 2 +- .../model/build_info/new_build_info_test.py | 4 +- test/unittests/model/options_test.py | 38 +- test/unittests/model/other_settings_test.py | 133 +-- test/unittests/model/settings_test.py | 31 +- test/unittests/model/test_list.py | 49 + .../model/version/test_version_comparison.py | 5 +- .../model/version/test_version_range.py | 17 + test/unittests/model/versionrepr_test.py | 2 - .../search/search_query_parse_test.py | 2 +- .../server/conan_server_config_parser_test.py | 18 +- test/unittests/server/service/service_test.py | 6 + .../unittests/tools/apple/test_apple_tools.py | 2 +- .../tools/apple/test_xcodebuild.py | 0 test/unittests/tools/build/test_can_run.py | 14 +- test/unittests/tools/build/test_compiler.py | 34 + test/unittests/tools/build/test_cppstd.py | 17 + test/unittests/tools/build/test_cstd.py | 13 + .../tools/cmake/test_cmake_install.py | 2 +- .../cmake/test_cmake_presets_definitions.py | 2 +- test/unittests/tools/cmake/test_cmake_test.py | 24 + .../tools/cmake/test_cmaketoolchain.py | 1061 +++++++---------- test/unittests/tools/env/test_env.py | 7 +- test/unittests/tools/env/test_env_files.py | 49 +- test/unittests/tools/files/test_downloads.py | 1 + test/unittests/tools/files/test_patches.py | 2 +- test/unittests/tools/files/test_rm.py | 48 +- test/unittests/tools/files/test_tool_copy.py | 6 +- test/unittests/tools/files/test_zipping.py | 29 +- test/unittests/tools/gnu/autotools_test.py | 109 +- .../gnu}/autotools_toolchain_test.py | 67 +- test/unittests/tools/gnu/gnudepsflags_test.py | 2 +- test/unittests/tools/gnu/test_gnutoolchain.py | 21 +- test/unittests/tools/intel/test_intel_cc.py | 2 +- test/unittests/tools/meson/test_meson.py | 26 +- .../unittests/tools/microsoft/test_msbuild.py | 8 +- .../tools/microsoft/test_msvs_toolset.py | 1 + test/unittests/tools/system/__init__.py | 0 .../tools/system/pip_manager_test.py | 49 + test/unittests/util/conanfile_tools_test.py | 35 +- test/unittests/util/detect_libc_test.py | 6 +- test/unittests/util/detect_test.py | 25 +- .../util/detected_architecture_test.py | 2 +- .../util/files/strip_root_extract_test.py | 6 +- test/unittests/util/files/tar_extract_test.py | 6 +- test/unittests/util/test_encrypt.py | 13 +- 468 files changed, 13969 insertions(+), 5942 deletions(-) create mode 100644 .github/workflows/build-binaries.yml create mode 100644 conan/cli/commands/run.py create mode 100644 conan/internal/model/conanconfig.py create mode 100644 conan/tools/build/compiler.py rename conan/tools/cmake/{cmakedeps2 => cmakeconfigdeps}/__init__.py (100%) rename conan/tools/cmake/{cmakedeps2/cmakedeps.py => cmakeconfigdeps/cmakeconfigdeps.py} (86%) rename conan/tools/cmake/{cmakedeps2 => cmakeconfigdeps}/config.py (52%) rename conan/tools/cmake/{cmakedeps2 => cmakeconfigdeps}/config_version.py (97%) rename conan/tools/cmake/{cmakedeps2 => cmakeconfigdeps}/target_configuration.py (86%) rename conan/tools/cmake/{cmakedeps2 => cmakeconfigdeps}/targets.py (100%) delete mode 100644 conan/tools/files/packager.py create mode 100644 conan/tools/system/pip_manager.py delete mode 100644 conans/model/package_ref.py delete mode 100644 conans/model/recipe_ref.py rename test/functional/command/{config_install_test.py => test_config_install.py} (50%) delete mode 100644 test/functional/layout/test_layout_autopackage.py delete mode 100644 test/functional/only_source_test.py rename {conans/model => test/functional/toolchains/autotools}/__init__.py (100%) create mode 100644 test/functional/toolchains/autotools/test_universal_binaries.py create mode 100644 test/functional/toolchains/cmake/test_cmake_extra_variables.py create mode 100644 test/functional/toolchains/cmake/test_cmake_multi.py delete mode 100644 test/functional/toolchains/cmake/test_cmake_toolchain_vxworks_clang.py create mode 100644 test/functional/toolchains/cmake/test_cps.py rename test/{unittests/client/toolchain => functional/toolchains/emscripten}/__init__.py (100%) create mode 100644 test/functional/toolchains/gnu/test_universal_binaries.py delete mode 100644 test/functional/toolchains/test_txt_cmdline.py create mode 100644 test/functional/tools/system/pip_manager_test.py rename test/integration/{ => command/export}/export_sources_test.py (100%) rename test/{unittests/client/toolchain/autotools => integration/command/remote}/__init__.py (100%) rename test/integration/command/{ => remote}/remote_test.py (90%) rename test/integration/command/{ => remote}/remote_verify_ssl_test.py (100%) rename test/integration/command/{ => remote}/test_remote_users.py (99%) create mode 100644 test/integration/command/test_run.py create mode 100644 test/integration/remote/test_remote_recipes_only.py create mode 100644 test/integration/test_compressions.py create mode 100644 test/integration/test_recipe_policies.py create mode 100644 test/performance/test_compatibility_performance.py create mode 100644 test/performance/test_db_performance.py delete mode 100644 test/unittests/client/toolchain/autotools/autotools_test.py create mode 100644 test/unittests/model/test_list.py rename test/unittests/{client => }/tools/apple/test_xcodebuild.py (100%) create mode 100644 test/unittests/tools/build/test_compiler.py rename test/unittests/{client/toolchain/autotools => tools/gnu}/autotools_toolchain_test.py (90%) create mode 100644 test/unittests/tools/system/__init__.py create mode 100644 test/unittests/tools/system/pip_manager_test.py diff --git a/.ci/docker/conan-tests b/.ci/docker/conan-tests index 5f7ee6f2931..5c9bc564c03 100644 --- a/.ci/docker/conan-tests +++ b/.ci/docker/conan-tests @@ -4,19 +4,20 @@ LABEL maintainer="Conan.io " ENV DEBIAN_FRONTEND=noninteractive -ENV PY36=3.6.15 \ +ENV PY37=3.7.9 \ PY38=3.8.6 \ PY39=3.9.2 \ PY310=3.10.16 \ PY312=3.12.3 \ PY313=3.13.0 \ + PY314=3.14.0 \ CMAKE_3_15=/usr/share/cmake-3.15.7/bin/cmake \ CMAKE_3_16=/usr/share/cmake-3.16.9/bin/cmake \ CMAKE_3_17=/usr/share/cmake-3.17.5/bin/cmake \ CMAKE_3_19=/usr/share/cmake-3.19.7/bin/cmake \ CMAKE_3_23=/usr/share/cmake-3.23.5/bin/cmake \ CMAKE_3_27=/usr/share/cmake-3.27.9/bin/cmake \ - CMAKE_4_0=/usr/share/cmake-4.0.0-rc3/bin/cmake \ + CMAKE_4_2=/usr/share/cmake-4.2.1/bin/cmake \ GCC_9=/usr/bin/gcc-9 \ GXX_9=/usr/bin/g++-9 \ GCC_11=/usr/bin/gcc-11 \ @@ -24,9 +25,9 @@ ENV PY36=3.6.15 \ CLANG_14=/usr/bin/clang-14 \ CLANGXX_14=/usr/bin/clang++-14 \ BAZEL_6=6.5.0 \ - BAZEL_7=7.4.1 \ - BAZEL_8=8.0.0 \ - EMSDK=4.0.10 + BAZEL_7=7.6.2 \ + BAZEL_8=8.4.2 \ + EMSDK=4.0.22 RUN apt-get update && \ apt-get install -y --no-install-recommends \ @@ -95,12 +96,12 @@ ENV PYENV_ROOT $HOME/.pyenv ENV PATH $PYENV_ROOT/bin:$PYENV_ROOT/shims:/usr/bin:/bin:$PATH RUN curl https://pyenv.run | bash && \ - pyenv install $PY36 && \ + pyenv install $PY37 && \ pyenv install $PY38 && \ pyenv install $PY39 && \ pyenv install $PY310 && \ pyenv install $PY312 && \ - pyenv install $PY313 && \ + pyenv install $PY314 && \ pyenv global $PY39 && \ curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py && \ python get-pip.py && \ @@ -122,15 +123,15 @@ RUN wget https://github.com/Kitware/CMake/releases/download/v3.15.7/cmake-3.15.7 tar -xvzf cmake-3.23.5-Linux-x86_64.tar.gz && mv cmake-3.23.5-linux-x86_64/ /usr/share/cmake-3.23.5 && \ wget https://github.com/Kitware/CMake/releases/download/v3.27.9/cmake-3.27.9-Linux-x86_64.tar.gz && \ tar -xvzf cmake-3.27.9-Linux-x86_64.tar.gz && mv cmake-3.27.9-linux-x86_64/ /usr/share/cmake-3.27.9 && \ - wget https://cmake.org/files/v4.0/cmake-4.0.0-rc3-linux-x86_64.tar.gz && \ - tar -xvzf cmake-4.0.0-rc3-linux-x86_64.tar.gz && mv cmake-4.0.0-rc3-linux-x86_64/ /usr/share/cmake-4.0.0-rc3 && \ + wget https://cmake.org/files/v4.2/cmake-4.2.1-linux-x86_64.tar.gz && \ + tar -xvzf cmake-4.2.1-linux-x86_64.tar.gz && mv cmake-4.2.1-linux-x86_64/ /usr/share/cmake-4.2.1 && \ update-alternatives --install /usr/bin/cmake cmake $CMAKE_3_15 10 && \ update-alternatives --install /usr/bin/cmake cmake $CMAKE_3_16 20 && \ update-alternatives --install /usr/bin/cmake cmake $CMAKE_3_17 30 && \ update-alternatives --install /usr/bin/cmake cmake $CMAKE_3_19 40 && \ update-alternatives --install /usr/bin/cmake cmake $CMAKE_3_23 50 && \ update-alternatives --install /usr/bin/cmake cmake $CMAKE_3_27 60 && \ - update-alternatives --install /usr/bin/cmake cmake $CMAKE_4_0 70 && \ + update-alternatives --install /usr/bin/cmake cmake $CMAKE_4_2 70 && \ # set CMake 3.15 as default update-alternatives --set cmake $CMAKE_3_15 @@ -174,6 +175,7 @@ RUN cd /tmp && \ wget https://github.com/emscripten-core/emsdk/archive/refs/tags/${EMSDK}.tar.gz && \ tar xzf ${EMSDK}.tar.gz --directory /usr/share && \ cd /usr/share/emsdk-${EMSDK} && \ + pyenv local 3.12 && \ ./emsdk update && \ ./emsdk install latest && \ ./emsdk activate latest --permanent && \ diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index e67ca7129fd..631791eb678 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -25,9 +25,10 @@ To contribute follow the next steps: 3. Fork the [Conan main repository](https://github.com/conan-io/conan) and create a `feature/xxx` branch from the `develop2` branch and develop your fix/feature as discussed in previous step. 4. Try to keep your branch updated with the `develop2` branch to avoid conflicts. -5. Open a pull request, and select `develop2` as the base branch. Never open a pull request to ``release/xxx`` branches, unless the branch is to be part of the next 2.X.Y patch. In that case, the PR should be targeted to release/2.0. -6. Add the text (besides other comments): "fixes #IssueNumber" in the body of the PR, referring to the issue of step 1. -7. Submit a PR to the Conan documentation about the changes done providing examples if needed. +5. Run the ``test/unittest`` and ``test/integration`` test suite locally, as described in [Conan tests guidelines section](https://github.com/conan-io/conan/blob/develop2/test/README.md). If you are doing changes to a build system integration, locate the respective folder for that integration in ``test/functional`` and run the tests of that folder. It is not expected that contributors have to run the full test suite locally, as it requires too many external tools. +6. Open a pull request, and select `develop2` as the base branch. Never open a pull request to ``release/xxx`` branches, unless the branch is to be part of the next 2.X.Y patch. In that case, the PR should be targeted to ``release/2.X``. +7. Add the text (besides other comments): "fixes #IssueNumber" in the body of the PR, referring to the issue of step 1. +8. Submit a PR to the Conan documentation about the changes done providing examples if needed. The ``conan-io`` organization maintainers will review and help with the coding of tests. Finally it will be assigned to milestone. @@ -102,8 +103,7 @@ import shutil from tqdm import tqdm -from conans.client.tools import which -from conans.errors import ConanException -from conans.model.version import Version +from conan.tools.cmake import CMakeToolchain +from conan.tools.files import save, load ``` -- Write unit tests, if possible +- Write tests, if possible diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml new file mode 100644 index 00000000000..a781d448c7e --- /dev/null +++ b/.github/workflows/build-binaries.yml @@ -0,0 +1,68 @@ +name: Build Conan Binaries +run-name: Build Conan Binaries - v${{ inputs.conan_version }} - ${{ inputs.target_sha }} - ${{ inputs.request_id }} + +on: + workflow_dispatch: + inputs: + conan_version: { description: Conan version to package, required: true, type: string } + target_sha: { description: Git SHA to package, required: true, type: string } + request_id: { description: ID to identify run, required: true, type: string } + +permissions: + contents: read + +jobs: + package: + name: Package for ${{ matrix.platform }}/${{ matrix.arch }} + strategy: + fail-fast: false + matrix: + include: + - { platform: Linux, arch: x86_64, runner: ubuntu-24.04 } + - { platform: Windows, arch: x86_64, runner: windows-2022 } + - { platform: Windows, arch: i686, runner: windows-2022 } + - { platform: Windows, arch: arm64, runner: windows-11-arm } + - { platform: Macos, arch: arm64, runner: macos-14 } + - { platform: Macos, arch: x86_64, runner: macos-14 } + - { platform: Linux, arch: arm64, runner: ubuntu-24.04-arm } + runs-on: ${{ matrix.runner }} + + steps: + - name: Show inputs + shell: bash + run: | + echo "conan_version=${{ inputs.conan_version }}" + echo "target_sha=${{ inputs.target_sha }}" + echo "request_id=${{ inputs.request_id }}" + + - name: Generate Read-Only App Token + id: generate_token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ secrets.GH_APP_RELEASE_ID }} + private-key: ${{ secrets.GH_APP_RELEASE_PRIVATE_KEY }} + permission-contents: read + owner: conan-io + repositories: | + conan + release-tools + + - name: Checkout release-tools + uses: actions/checkout@v4 + with: + repository: conan-io/release-tools + token: ${{ steps.generate_token.outputs.token }} + + - name: Build packages + shell: bash + env: + PLATFORM: ${{ matrix.platform }} + ARCH: ${{ matrix.arch }} + run: | + ./jenkins/build_packages.sh "${{ inputs.conan_version }}" "${{ inputs.target_sha }}" + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: conan-${{ inputs.conan_version }}-${{ inputs.target_sha }}-${{ matrix.platform }}-${{ matrix.arch }}-${{ inputs.request_id }} + path: dist/* diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6297ae0f1fc..1e8abfe2e16 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,6 +16,20 @@ concurrency: cancel-in-progress: true jobs: + ensure_latest_tag_merged: + runs-on: ubuntu-latest + name: Ensure latest release is merged in develop2 branch + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - shell: bash + run: | + git fetch --tags --prune + latest_tag=$(git tag -l --sort=-v:refname | head -n1) + echo "Checking that branch 'develop2' contains the latest release tag: $latest_tag" + git merge-base --is-ancestor "$latest_tag" origin/develop2 + set_python_versions: runs-on: ubuntu-latest outputs: @@ -27,29 +41,29 @@ jobs: id: set_versions run: | if [[ "${{ github.ref }}" == "refs/heads/develop2" || "${{ github.ref }}" == refs/heads/release/* ]]; then - echo "python_versions_linux_windows=['3.13', '3.6']" >> $GITHUB_OUTPUT - echo "python_versions_macos=['3.13', '3.8']" >> $GITHUB_OUTPUT + echo "python_versions_linux_windows=['3.14', '3.7']" >> $GITHUB_OUTPUT + echo "python_versions_macos=['3.14', '3.8']" >> $GITHUB_OUTPUT else echo "python_versions_linux_windows=['3.10']" >> $GITHUB_OUTPUT echo "python_versions_macos=['3.10']" >> $GITHUB_OUTPUT fi linux_suite: - needs: set_python_versions + needs: [ensure_latest_tag_merged, set_python_versions] uses: ./.github/workflows/linux-tests.yml name: Linux test suite with: python-versions: ${{ needs.set_python_versions.outputs.python_versions_linux_windows }} osx_suite: - needs: set_python_versions + needs: [ensure_latest_tag_merged, set_python_versions] uses: ./.github/workflows/osx-tests.yml name: OSX test suite with: python-versions: ${{ needs.set_python_versions.outputs.python_versions_macos }} windows_suite: - needs: set_python_versions + needs: [ensure_latest_tag_merged, set_python_versions] uses: ./.github/workflows/win-tests.yml name: Windows test suite with: diff --git a/.github/workflows/osx-tests.yml b/.github/workflows/osx-tests.yml index 3547d0110b7..b98cb026f2b 100644 --- a/.github/workflows/osx-tests.yml +++ b/.github/workflows/osx-tests.yml @@ -20,13 +20,6 @@ jobs: with: python-version: '3.10' - - name: Cache pip packages - id: cache-pip - uses: actions/cache@v4 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('conans/requirements*.txt') }} - - name: Install Python requirements run: | pip install --upgrade pip @@ -58,10 +51,10 @@ jobs: ~/Applications/CMake/3.19.7 ~/Applications/CMake/3.23.5 ~/Applications/CMake/3.27.9 - ~/Applications/CMake/4.0.0-rc3 + ~/Applications/CMake/4.2.1 ~/Applications/bazel/6.5.0 - ~/Applications/bazel/7.4.1 - ~/Applications/bazel/8.0.0 + ~/Applications/bazel/7.6.2 + ~/Applications/bazel/8.4.2 key: ${{ runner.os }}-conan-tools-cache - name: Build CMake old versions not available for ARM @@ -87,7 +80,7 @@ jobs: if: steps.cache-tools.outputs.cache-hit != 'true' run: | set -e - CMAKE_PRECOMP_VERSIONS=("3.19.7" "3.23.5" "3.27.9" "4.0.0-rc3") + CMAKE_PRECOMP_VERSIONS=("3.19.7" "3.23.5" "3.27.9" "4.2.1") for version in "${CMAKE_PRECOMP_VERSIONS[@]}"; do echo "Downloading and installing precompiled universal CMake version ${version}..." wget -q --no-check-certificate https://cmake.org/files/v${version%.*}/cmake-${version}-macos-universal.tar.gz @@ -107,7 +100,7 @@ jobs: if: steps.cache-tools.outputs.cache-hit != 'true' run: | set -e - for version in 6.5.0 7.4.1 8.0.0; do + for version in 6.5.0 7.6.2 8.4.2; do mkdir -p ${HOME}/Applications/bazel/${version} wget -q -O ${HOME}/Applications/bazel/${version}/bazel https://github.com/bazelbuild/bazel/releases/download/${version}/bazel-${version}-darwin-arm64 chmod +x ${HOME}/Applications/bazel/${version}/bazel @@ -133,12 +126,6 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Restore pip cache - uses: actions/cache@v4 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('conans/requirements*.txt') }} - - name: Restore tools cache uses: actions/cache@v4 with: @@ -147,12 +134,31 @@ jobs: ~/Applications/CMake/3.19.7 ~/Applications/CMake/3.23.5 ~/Applications/CMake/3.27.9 - ~/Applications/CMake/4.0.0-rc3 + ~/Applications/CMake/4.2.1 ~/Applications/bazel/6.5.0 - ~/Applications/bazel/7.4.1 - ~/Applications/bazel/8.0.0 + ~/Applications/bazel/7.6.2 + ~/Applications/bazel/8.4.2 key: ${{ runner.os }}-conan-tools-cache + - name: Select Xcode 16.4 + run: | + sudo xcode-select -s /Applications/Xcode_16.4.app + xcodebuild -version + xcrun --sdk macosx --show-sdk-version + clang --version + + # Install system dependencies BEFORE setting up the matrix Python. + # This prevents Emscripten (which requires Python 3.10+) from crashing + # when trying to use Python 3.8 from the matrix. + - name: Install homebrew dependencies + run: | + brew update + brew install xcodegen make libtool zlib autoconf automake ninja emscripten + export PATH=${HOME}/Applications/CMake/3.15.7/bin:$PATH:/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin + emcc --version + cmake --version + bazel --version + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: @@ -166,13 +172,6 @@ jobs: pip install -r conans/requirements_dev.txt pip install meson - - name: Install homebrew dependencies - run: | - brew install xcodegen make libtool zlib autoconf automake ninja emscripten - export PATH=${HOME}/Applications/CMake/3.15.7/bin:$PATH:/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin - cmake --version - bazel --version - - name: Run tests uses: ./.github/actions/test-coverage with: diff --git a/.github/workflows/win-tests.yml b/.github/workflows/win-tests.yml index bfc3bcff231..dc51e9329f2 100644 --- a/.github/workflows/win-tests.yml +++ b/.github/workflows/win-tests.yml @@ -162,16 +162,16 @@ jobs: C:\tools\cmake\3.19.7 C:\tools\cmake\3.23.5 C:\tools\cmake\3.27.9 - C:\tools\cmake\4.0.0-rc3 + C:\tools\cmake\4.2.1 C:\tools\bazel\6.5.0 - C:\tools\bazel\7.4.1 - C:\tools\bazel\8.0.0 + C:\tools\bazel\7.6.2 + C:\tools\bazel\8.4.2 key: ${{ runner.os }}-conan-tools-cache - name: Install CMake versions if: steps.cache-tools.outputs.cache-hit != 'true' run: | - $CMAKE_BUILD_VERSIONS = "3.15.7", "3.19.7", "3.23.5", "3.27.9", "4.0.0-rc3" + $CMAKE_BUILD_VERSIONS = "3.15.7", "3.19.7", "3.23.5", "3.27.9", "4.2.1" foreach ($version in $CMAKE_BUILD_VERSIONS) { Write-Host "Downloading CMake version $version for Windows..." $destination = "C:\tools\cmake\$version" @@ -194,7 +194,7 @@ jobs: - name: Install Bazel versions if: steps.cache-tools.outputs.cache-hit != 'true' run: | - $BAZEL_BUILD_VERSIONS = "6.5.0", "7.4.1", "8.0.0" + $BAZEL_BUILD_VERSIONS = "6.5.0", "7.6.2", "8.4.2" foreach ($version in $BAZEL_BUILD_VERSIONS) { Write-Host "Downloading Bazel version $version for Windows..." $destination = "C:\tools\bazel\$version" diff --git a/README.md b/README.md index 70207a0bfed..d3318ced182 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Decentralized, open-source (MIT), C/C++ package manager. - Homepage: https://conan.io/ - Github: https://github.com/conan-io/conan - Docs: https://docs.conan.io -- Slack: https://cpplang.slack.com (#conan channel. Please, click [here](https://join.slack.com/t/cpplang/shared_invite/zt-1snzdn6rp-rOUxF3166oz1_11Tr5H~xg) to get an invitation) +- Slack: https://cpplang.slack.com (#conan channel. Please, click [here](https://cppalliance.org/slack/#cpp-slack) to get an invitation) - Twitter: https://twitter.com/conan_io - Blog: https://blog.conan.io - Security reports: https://jfrog.com/trust/report-vulnerability diff --git a/conan/__init__.py b/conan/__init__.py index 892f6ea47d9..febca134c4b 100644 --- a/conan/__init__.py +++ b/conan/__init__.py @@ -2,5 +2,5 @@ from conan.internal.model.workspace import Workspace from conan.internal.model.version import Version -__version__ = '2.20.0-dev' +__version__ = '2.25.0-dev' conan_version = Version(__version__) diff --git a/conan/api/conan_api.py b/conan/api/conan_api.py index 17edb639207..a3d9225f751 100644 --- a/conan/api/conan_api.py +++ b/conan/api/conan_api.py @@ -23,6 +23,7 @@ from conan.errors import ConanException from conan.internal.cache.home_paths import HomePaths from conan.internal.hook_manager import HookManager +from conan.internal.loader import load_python_file from conan.internal.model.conf import load_global_conf, ConfDefinition, CORE_CONF_PATTERN from conan.internal.model.settings import load_settings_yml from conan.internal.paths import get_conan_user_home @@ -40,12 +41,12 @@ class ConanAPI: def __init__(self, cache_folder=None): """ :param cache_folder: Conan cache/home folder. It will have less priority than the - "home_folder" defined in a Workspace. + ``"home_folder"`` defined in a Workspace. """ version = sys.version_info - if version.major == 2 or version.minor < 6: - raise ConanException("Conan needs Python >= 3.6") + if version.major == 2 or version.minor < 7: + raise ConanException("Conan needs Python >= 3.7") if cache_folder is not None and not os.path.isabs(cache_folder): raise ConanException("cache_folder has to be an absolute path") @@ -66,21 +67,27 @@ def __init__(self, cache_folder=None): self.profiles = ProfilesAPI(self, self._api_helpers) self.install = InstallAPI(self, self._api_helpers) self.graph = GraphAPI(self, self._api_helpers) - self.export = ExportAPI(self, self._api_helpers) + #: Used to export recipes and pre-compiled package binaries to the Conan cache + self.export: ExportAPI = ExportAPI(self, self._api_helpers) self.remove = RemoveAPI(self) self.new = NewAPI(self) - self.upload = UploadAPI(self, self._api_helpers) - self.download = DownloadAPI(self) - self.cache = CacheAPI(self, self._api_helpers) - self.lockfile = LockfileAPI(self) + #: Used to upload recipes and packages to remotes + self.upload: UploadAPI = UploadAPI(self, self._api_helpers) + #: Used to download recipes and packages from remotes + self.download: DownloadAPI = DownloadAPI(self) + #: Used to interact wit the packages storage cache + self.cache: CacheAPI = CacheAPI(self, self._api_helpers) + #: Used to read and manage lockfile files + self.lockfile: LockfileAPI = LockfileAPI(self) self.local = LocalAPI(self, self._api_helpers) - self.audit = AuditAPI(self) + #: Used to check vulnerabilities of dependencies + self.audit: AuditAPI = AuditAPI(self) # Now, lazy loading of editables self.workspace = WorkspaceAPI(self) - self.report = ReportAPI(self, self._api_helpers) + self.report: ReportAPI = ReportAPI(self, self._api_helpers) @property - def home_folder(self): + def home_folder(self) -> str: """ Where the Conan user home is located. Read only. Can be modified by the ``CONAN_HOME`` environment variable or by the ``.conanrc`` file in the current directory or any parent directory @@ -93,7 +100,6 @@ def reinit(self): Reinitialize the Conan API. This is useful when the configuration changes. """ self._api_helpers.reinit() - self.remotes.reinit() self.local.reinit() def migrate(self): @@ -109,6 +115,26 @@ def __init__(self, conan_api): self._cli_core_confs = None self._init_global_conf() self.hook_manager = HookManager(HomePaths(self._conan_api.home_folder).hooks_path) + # Wraps an http_requester to inject proxies, certs, etc + self._requester = ConanRequester(self.global_conf, self._conan_api.home_folder) + self._settings_yml = None + self._compression_plugin = None + + @property + def compression_plugin(self): + if self._compression_plugin is None: + compression_plugin_path = HomePaths( + self._conan_api.home_folder).compression_plugin_path + if not os.path.exists(compression_plugin_path): + self._compression_plugin = False # Avoid FS re-check + return None + mod, _ = load_python_file(compression_plugin_path) + # A plugin can provide just 1 of them + if not hasattr(mod, "tar_extract") and not hasattr(mod, "tar_compress"): + raise ConanException("The 'compression.py' plugin does not contain " + "required `tar_extract` or `tar_compress` functions") + self._compression_plugin = mod + return self._compression_plugin def set_core_confs(self, core_confs): confs = ConfDefinition() @@ -133,7 +159,16 @@ def _init_global_conf(self): def reinit(self): self._init_global_conf() self.hook_manager.reinit() + self._requester = ConanRequester(self.global_conf, self._conan_api.home_folder) + self._settings_yml = None + self._compression_plugin = None @property def settings_yml(self): - return load_settings_yml(self._conan_api.home_folder) + if self._settings_yml is None: + self._settings_yml = load_settings_yml(self._conan_api.home_folder) + return self._settings_yml + + @property + def requester(self): + return self._requester diff --git a/conan/api/input.py b/conan/api/input.py index 258ded10dfa..6636697a9ab 100644 --- a/conan/api/input.py +++ b/conan/api/input.py @@ -78,7 +78,7 @@ def request_boolean(self, msg, default_option=None): """Request user to input a boolean""" ret = None while ret is None: - if default_option is True: + if default_option: s = self.request_string("%s (YES/no)" % msg) elif default_option is False: s = self.request_string("%s (NO/yes)" % msg) diff --git a/conan/api/model/list.py b/conan/api/model/list.py index 8faedec405d..637330afa26 100644 --- a/conan/api/model/list.py +++ b/conan/api/model/list.py @@ -1,9 +1,12 @@ +import copy import fnmatch import json import os from json import JSONDecodeError +from typing import Iterable, Tuple, Dict from conan.api.model import RecipeReference, PkgReference +from conan.api.output import ConanOutput from conan.errors import ConanException from conan.internal.errors import NotFoundException from conan.internal.model.version_range import VersionRange @@ -104,7 +107,7 @@ def load_graph(graphfile, graph_recipes=None, graph_binaries=None, context=None) ) mpkglist = MultiPackagesList._define_graph(graph, graph_recipes, graph_binaries, - context=base_context) + context=base_context) if context == "build-only": host = MultiPackagesList._define_graph(graph, graph_recipes, graph_binaries, context="host") @@ -150,11 +153,11 @@ def _define_graph(graph, graph_recipes=None, graph_binaries=None, context=None): continue pyref = RecipeReference.loads(pyref) if any(r == "*" or r == pyrecipe for r in recipes): - cache_list.add_refs([pyref]) + cache_list.add_ref(pyref) pyremote = pyreq["remote"] if pyremote: remote_list = pkglist.lists.setdefault(pyremote, PackagesList()) - remote_list.add_refs([pyref]) + remote_list.add_ref(pyref) recipe = node["recipe"] if recipe in (RECIPE_EDITABLE, RECIPE_CONSUMER, RECIPE_VIRTUAL, RECIPE_PLATFORM): @@ -165,18 +168,18 @@ def _define_graph(graph, graph_recipes=None, graph_binaries=None, context=None): ref.timestamp = node["rrev_timestamp"] recipe = recipe.lower() if any(r == "*" or r == recipe for r in recipes): - cache_list.add_refs([ref]) + cache_list.add_ref(ref) remote = node["remote"] if remote: remote_list = pkglist.lists.setdefault(remote, PackagesList()) - remote_list.add_refs([ref]) + remote_list.add_ref(ref) pref = PkgReference(ref, node["package_id"], node["prev"], node["prev_timestamp"]) binary_remote = node["binary_remote"] if binary_remote: remote_list = pkglist.lists.setdefault(binary_remote, PackagesList()) - remote_list.add_refs([ref]) # Binary listed forces recipe listed - remote_list.add_prefs(ref, [pref]) + remote_list.add_ref(ref) # Binary listed forces recipe listed + remote_list.add_pref(pref) binary = node["binary"] if binary in (BINARY_SKIP, BINARY_INVALID, BINARY_MISSING): @@ -184,18 +187,23 @@ def _define_graph(graph, graph_recipes=None, graph_binaries=None, context=None): binary = binary.lower() if any(b == "*" or b == binary for b in binaries): - cache_list.add_refs([ref]) # Binary listed forces recipe listed - cache_list.add_prefs(ref, [pref]) - cache_list.add_configurations({pref: node["info"]}) + cache_list.add_ref(ref) # Binary listed forces recipe listed + cache_list.add_pref(pref, node["info"]) return pkglist class PackagesList: """ A collection of recipes, revisions and packages.""" def __init__(self): - self.recipes = {} + self._data = {} + + def __bool__(self): + """ Whether the package list contains any recipe""" + return bool(self._data) def merge(self, other): + assert isinstance(other, PackagesList) + def recursive_dict_update(d, u): # TODO: repeated from conandata.py for k, v in u.items(): if isinstance(v, dict): @@ -203,70 +211,94 @@ def recursive_dict_update(d, u): # TODO: repeated from conandata.py else: d[k] = v return d - recursive_dict_update(self.recipes, other.recipes) + recursive_dict_update(self._data, other._data) def keep_outer(self, other): - if not self.recipes: + assert isinstance(other, PackagesList) + if not self._data: return - for ref, info in other.recipes.items(): - if self.recipes.get(ref, {}) == info: - self.recipes.pop(ref) + for ref, info in other._data.items(): + if self._data.get(ref, {}) == info: + self._data.pop(ref) def split(self): """ - Returns a list of PackageList, splitted one per reference. + Returns a list of PackageList, split one per reference. This can be useful to parallelize things like upload, parallelizing per-reference """ result = [] - for r, content in self.recipes.items(): + for r, content in self._data.items(): subpkglist = PackagesList() - subpkglist.recipes[r] = content + subpkglist._data[r] = content result.append(subpkglist) return result - def only_recipes(self): - result = {} - for ref, ref_dict in self.recipes.items(): + def only_recipes(self) -> None: + """ Filter out all the packages and package revisions, keep only the recipes and + recipe revisions in self._data. + """ + for ref, ref_dict in self._data.items(): for rrev_dict in ref_dict.get("revisions", {}).values(): rrev_dict.pop("packages", None) - return result def add_refs(self, refs): + ConanOutput().warning("PackagesLists.add_refs() non-public, non-documented method will be " + "removed, use .add_ref() instead", warn_tag="deprecated") # RREVS alreday come in ASCENDING order, so upload does older revisions first for ref in refs: - ref_dict = self.recipes.setdefault(str(ref), {}) - if ref.revision: - revs_dict = ref_dict.setdefault("revisions", {}) - rev_dict = revs_dict.setdefault(ref.revision, {}) - if ref.timestamp: - rev_dict["timestamp"] = ref.timestamp + self.add_ref(ref) + + def add_ref(self, ref: RecipeReference) -> None: + """ + Adds a new RecipeReference to a package list + """ + ref_dict = self._data.setdefault(str(ref), {}) + if ref.revision: + revs_dict = ref_dict.setdefault("revisions", {}) + rev_dict = revs_dict.setdefault(ref.revision, {}) + if ref.timestamp: + rev_dict["timestamp"] = ref.timestamp def add_prefs(self, rrev, prefs): + ConanOutput().warning("PackageLists.add_prefs() non-public, non-documented method will be " + "removed, use .add_pref() instead", warn_tag="deprecated") # Prevs already come in ASCENDING order, so upload does older revisions first - revs_dict = self.recipes[str(rrev)]["revisions"] - rev_dict = revs_dict[rrev.revision] - packages_dict = rev_dict.setdefault("packages", {}) + for p in prefs: + self.add_pref(p) - for pref in prefs: - package_dict = packages_dict.setdefault(pref.package_id, {}) - if pref.revision: - prevs_dict = package_dict.setdefault("revisions", {}) - prev_dict = prevs_dict.setdefault(pref.revision, {}) - if pref.timestamp: - prev_dict["timestamp"] = pref.timestamp + def add_pref(self, pref: PkgReference, pkg_info: dict = None) -> None: + """ + Add a PkgReference to an already existing RecipeReference inside a package list + """ + # Prevs already come in ASCENDING order, so upload does older revisions first + rev_dict = self.recipe_dict(pref.ref) + packages_dict = rev_dict.setdefault("packages", {}) + package_dict = packages_dict.setdefault(pref.package_id, {}) + if pref.revision: + prevs_dict = package_dict.setdefault("revisions", {}) + prev_dict = prevs_dict.setdefault(pref.revision, {}) + if pref.timestamp: + prev_dict["timestamp"] = pref.timestamp + if pkg_info is not None: + package_dict["info"] = pkg_info def add_configurations(self, confs): + ConanOutput().warning("PackageLists.add_configurations() non-public, non-documented method " + "will be removed, use .add_pref() instead", + warn_tag="deprecated") for pref, conf in confs.items(): - rev_dict = self.recipes[str(pref.ref)]["revisions"][pref.ref.revision] + rev_dict = self.recipe_dict(pref.ref) try: rev_dict["packages"][pref.package_id]["info"] = conf except KeyError: # If package_id does not exist, do nothing, only add to existing prefs pass def refs(self): + ConanOutput().warning("PackageLists.refs() non-public, non-documented method will be " + "removed, use .items() instead", warn_tag="deprecated") result = {} - for ref, ref_dict in self.recipes.items(): + for ref, ref_dict in self._data.items(): for rrev, rrev_dict in ref_dict.get("revisions", {}).items(): t = rrev_dict.get("timestamp") recipe = RecipeReference.loads(f"{ref}#{rrev}") # TODO: optimize this @@ -275,8 +307,45 @@ def refs(self): result[recipe] = rrev_dict return result + def items(self) -> Iterable[Tuple[RecipeReference, Dict[PkgReference, Dict]]]: + """ Iterate the contents of the package list. + + The first dictionary is the information directly belonging to the recipe-revision. + The second dictionary contains PkgReference as keys, and a dictionary with the values + belonging to that specific package reference (settings, options, etc.). + """ + for ref, ref_dict in self._data.items(): + for rrev, rrev_dict in ref_dict.get("revisions", {}).items(): + recipe = RecipeReference.loads(f"{ref}#{rrev}") # TODO: optimize this + t = rrev_dict.get("timestamp") + if t is not None: + recipe.timestamp = t + packages = {} + for package_id, pkg_info in rrev_dict.get("packages", {}).items(): + prevs = pkg_info.get("revisions", {}) + for prev, prev_info in prevs.items(): + t = prev_info.get("timestamp") + pref = PkgReference(recipe, package_id, prev, t) + packages[pref] = prev_info + yield recipe, packages + + def recipe_dict(self, ref: RecipeReference): + """ Gives read/write access to the dictionary containing a specific RecipeReference + information. + """ + return self._data[str(ref)]["revisions"][ref.revision] + + def package_dict(self, pref: PkgReference): + """ Gives read/write access to the dictionary containing a specific PkgReference + information + """ + ref_dict = self.recipe_dict(pref.ref) + return ref_dict["packages"][pref.package_id]["revisions"][pref.revision] + @staticmethod def prefs(ref, recipe_bundle): + ConanOutput().warning("PackageLists.prefs() non-public, non-documented method will be " + "removed, use .items() instead", warn_tag="deprecated") result = {} for package_id, pkg_bundle in recipe_bundle.get("packages", {}).items(): prevs = pkg_bundle.get("revisions", {}) @@ -288,13 +357,13 @@ def prefs(ref, recipe_bundle): def serialize(self): """ Serialize the instance to a dictionary.""" - return self.recipes.copy() + return copy.deepcopy(self._data) @staticmethod def deserialize(data): """ Loads the data from a serialized dictionary.""" result = PackagesList() - result.recipes = data + result._data = copy.deepcopy(data) return result @@ -357,10 +426,10 @@ def _version_range(self): if self.version and self.version.startswith("[") and self.version.endswith("]"): return VersionRange(self.version[1:-1]) - def filter_versions(self, refs): + def filter_versions(self, refs, resolve_prereleases=None): vrange = self._version_range if vrange: - refs = [r for r in refs if vrange.contains(r.version, None)] + refs = [r for r in refs if vrange.contains(r.version, resolve_prereleases)] return refs @property diff --git a/conan/api/model/remote.py b/conan/api/model/remote.py index d8ab67f2bb0..728957f41dc 100644 --- a/conan/api/model/remote.py +++ b/conan/api/model/remote.py @@ -3,16 +3,29 @@ class Remote: """ - The ``Remote`` class represents a remote registry of packages. It's a read-only opaque object that - should not be created directly, but obtained from the relevant ``RemotesAPI`` subapi methods. + The ``Remote`` class represents a remote registry of packages. """ def __init__(self, name, url, verify_ssl=True, disabled=False, allowed_packages=None, - remote_type=None): + remote_type=None, recipes_only=False): + """ A Remote object can be constructed to be passed as an argument to + RemotesAPI methods. When possible, it is better to use Remote objects returned by the API, + but for the ``RemotesAPI.add()`` method, for which a new constructed object is necessary. + It is recommended to use named arguments like ``Remote(..., verify_ssl=False)`` in + the constructor. + :param name: The name of the remote. + :param url: The URL of the remote repository (or local folder for "local-recipes-index"). + :param verify_ssl: Enable SSL Certificate validation. + :param disabled: Disable the remote repository. + :param allowed_packages: List of patterns of allowed packages from this remote + :param remote_type: Type of the remote repository, use "local-recipes-index" or ``None`` + :param recipes_only: If True, binaries form this remote will be ignored and never used + """ self.name = name # Read only, is the key self.url = url self.verify_ssl = verify_ssl self.disabled = disabled self.allowed_packages = allowed_packages + self.recipes_only = recipes_only self.remote_type = remote_type self._caching = {} @@ -26,6 +39,8 @@ def __str__(self): allowed_msg = "" if self.allowed_packages: allowed_msg = ", Allowed packages: {}".format(", ".join(self.allowed_packages)) + if self.recipes_only: + allowed_msg += ", Recipes only" if self.remote_type == LOCAL_RECIPES_INDEX: return "{}: {} [{}, Enabled: {}{}]".format(self.name, self.url, LOCAL_RECIPES_INDEX, not self.disabled, allowed_msg) diff --git a/conan/api/output.py b/conan/api/output.py index 30391cba97a..520e5d560cb 100644 --- a/conan/api/output.py +++ b/conan/api/output.py @@ -124,22 +124,34 @@ def define_silence_warnings(cls, warnings): def set_warnings_as_errors(cls, value): cls._warnings_as_errors = value + @classmethod + def get_output_level(cls): + return cls._conan_output_level + + @classmethod + def set_output_level(cls, level): + cls._conan_output_level = level + + @classmethod + def valid_log_levels(cls): + return {"quiet": LEVEL_QUIET, # -vquiet 80 + "error": LEVEL_ERROR, # -verror 70 + "warning": LEVEL_WARNING, # -vwaring 60 + "notice": LEVEL_NOTICE, # -vnotice 50 + "status": LEVEL_STATUS, # -vstatus 40 + None: LEVEL_VERBOSE, # -v 30 + "verbose": LEVEL_VERBOSE, # -vverbose 30 + "debug": LEVEL_DEBUG, # -vdebug 20 + "v": LEVEL_DEBUG, # -vv 20 + "trace": LEVEL_TRACE, # -vtrace 10 + "vv": LEVEL_TRACE # -vvv 10 + } + @classmethod def define_log_level(cls, v): env_level = os.getenv("CONAN_LOG_LEVEL") v = env_level or v - levels = {"quiet": LEVEL_QUIET, # -vquiet 80 - "error": LEVEL_ERROR, # -verror 70 - "warning": LEVEL_WARNING, # -vwaring 60 - "notice": LEVEL_NOTICE, # -vnotice 50 - "status": LEVEL_STATUS, # -vstatus 40 - None: LEVEL_VERBOSE, # -v 30 - "verbose": LEVEL_VERBOSE, # -vverbose 30 - "debug": LEVEL_DEBUG, # -vdebug 20 - "v": LEVEL_DEBUG, # -vv 20 - "trace": LEVEL_TRACE, # -vtrace 10 - "vv": LEVEL_TRACE # -vvv 10 - } + levels = cls.valid_log_levels() try: level = levels[v] except KeyError: @@ -147,7 +159,7 @@ def define_log_level(cls, v): vals = "quiet, error, warning, notice, status, verbose, debug(v), trace(vv)" raise ConanException(f"Invalid argument '-v{v}'{msg}.\nAllowed values: {vals}") else: - cls._conan_output_level = level + cls.set_output_level(level) @classmethod def level_allowed(cls, level): @@ -266,13 +278,13 @@ def verbose(self, msg: str, fg: str = None, bg: str = None): self._write_message(msg, fg=fg, bg=bg) return self - def status(self, msg: str, fg: str = None, bg: str = None): + def status(self, msg: str, fg: str = None, bg: str = None, newline: bool = True): """ Provides general information about the system or ongoing operations. Info messages are basic and used to inform about common events, like the start or completion of processes, without implying specific problems or achievements.""" if self._conan_output_level <= LEVEL_STATUS: - self._write_message(msg, fg=fg, bg=bg) + self._write_message(msg, fg=fg, bg=bg, newline=newline) return self info = status diff --git a/conan/api/subapi/audit.py b/conan/api/subapi/audit.py index bc7afa26f1b..9545490f5aa 100644 --- a/conan/api/subapi/audit.py +++ b/conan/api/subapi/audit.py @@ -28,12 +28,14 @@ def __init__(self, conan_api): } @staticmethod - def scan(deps_graph, provider): + def scan(deps_graph, provider, context=None): """ Scan a given recipe for vulnerabilities in its dependencies. """ refs = sorted(set(RecipeReference.loads(f"{node.ref.name}/{node.ref.version}") - for node in deps_graph.nodes[1:]), key=lambda ref: ref.name) + for node in deps_graph.nodes[1:] + if context is None or node.context == context), + key=lambda ref: ref.name) return provider.get_cves(refs) @staticmethod @@ -66,9 +68,9 @@ def get_provider(self, provider_name): ) raise ConanException( - f"Provider '{provider_name}' not found. Please specify a valid provider name or add it using: " - f"'conan audit provider add {provider_name} {add_arguments} --token='\n" - f"{register_message}" + f"Provider '{provider_name}' not found. Please specify a valid provider name or add " + f"it using: 'conan audit provider add {provider_name} {add_arguments} " + f"--token='\n{register_message}" ) provider_data = providers[provider_name] @@ -80,9 +82,11 @@ def get_provider(self, provider_name): provider_data["token"] = env_token elif "token" in provider_data: try: - provider_data["token"] = decode(base64.standard_b64decode(provider_data["token"]).decode(), CYPHER_KEY) - except binascii.Error as e: - raise ConanException(f"Invalid token format for provider '{provider_name}'. The token might be corrupt.") + enc_token = base64.standard_b64decode(provider_data["token"]).decode() + provider_data["token"] = decode(enc_token, CYPHER_KEY) + except binascii.Error: + raise ConanException(f"Invalid token format for provider '{provider_name}'. " + f"The token might be corrupt.") provider_cls = self._provider_cls.get(provider_data["type"]) @@ -140,7 +144,8 @@ def auth_provider(self, provider, token): providers = _load_providers(self._providers_path) assert provider.name in providers - providers[provider.name]["token"] = base64.standard_b64encode(encode(token, CYPHER_KEY).encode()).decode() + encode_token = encode(token, CYPHER_KEY).encode() + providers[provider.name]["token"] = base64.standard_b64encode(encode_token).decode() setattr(provider, "token", token) _save_providers(self._providers_path, providers) diff --git a/conan/api/subapi/cache.py b/conan/api/subapi/cache.py index 5855f72eeb8..da86417e0e1 100644 --- a/conan/api/subapi/cache.py +++ b/conan/api/subapi/cache.py @@ -5,13 +5,15 @@ from conan.api.model import PackagesList from conan.api.output import ConanOutput -from conan.internal.api.uploader import compress_files +from conan.internal.api.uploader import compress_files, get_compress_level from conan.internal.cache.cache import PkgCache -from conan.internal.cache.conan_reference_layout import EXPORT_SRC_FOLDER, EXPORT_FOLDER, SRC_FOLDER, \ - METADATA, DOWNLOAD_EXPORT_FOLDER +from conan.internal.cache.conan_reference_layout import (EXPORT_SRC_FOLDER, EXPORT_FOLDER, + SRC_FOLDER, METADATA, + DOWNLOAD_EXPORT_FOLDER) from conan.internal.cache.home_paths import HomePaths from conan.internal.cache.integrity_check import IntegrityChecker from conan.internal.loader import load_python_file +from conan.internal.paths import COMPRESSIONS from conan.internal.rest.download_cache import DownloadCache from conan.errors import ConanException from conan.api.model import PkgReference @@ -21,6 +23,8 @@ class CacheAPI: + """ This CacheAPI is used to interact with the packages storage cache + """ def __init__(self, conan_api, api_helpers): self._conan_api = conan_api @@ -71,17 +75,29 @@ def package_path(self, pref: PkgReference): return ref_layout.finalize() return _check_folder_existence(pref, "package", ref_layout.package()) - def check_integrity(self, package_list): - """Check if the recipes and packages are corrupted (it will raise a ConanExcepcion)""" + def check_integrity(self, package_list, return_pkg_list=False): + """ + Check if the recipes and packages are corrupted + + :param package_list: PackagesList to check + :param return_pkg_list: If True, return a PackagesList with corrupted artifacts + :return: PackagesList with corrupted artifacts if return_pkg_list is True + :raises: ConanExcepcion if there are corrupted artifacts and return_pkg_list is False + """ cache = PkgCache(self._conan_api.cache_folder, self._api_helpers.global_conf) checker = IntegrityChecker(cache) - checker.check(package_list) + corrupted_pkg_list = checker.check(package_list) + if return_pkg_list: + return corrupted_pkg_list + if corrupted_pkg_list: + raise ConanException("There are corrupted artifacts, check the error logs") def clean(self, package_list, source=True, build=True, download=True, temp=True, backup_sources=False): """ Remove non critical folders from the cache, like source, build and download (.tgz store) folders. + :param package_list: the package lists that should be cleaned :param source: boolean, remove the "source" folder if True :param build: boolean, remove the "build" folder if True @@ -105,19 +121,20 @@ def clean(self, package_list, source=True, build=True, download=True, temp=True, if not os.path.exists(manifest) or not os.path.exists(info): rmdir(folder) if backup_sources: - backup_files = self._conan_api.cache.get_backup_sources(package_list, exclude=False, only_upload=False) + backup_files = self._conan_api.cache.get_backup_sources(package_list, exclude=False, + only_upload=False) ConanOutput().verbose(f"Cleaning {len(backup_files)} backup sources") for f in backup_files: remove(f) - for ref, ref_bundle in package_list.refs().items(): + for ref, packages in package_list.items(): ConanOutput(ref.repr_notime()).verbose("Cleaning recipe cache contents") ref_layout = cache.recipe_layout(ref) if source: rmdir(ref_layout.source()) if download: rmdir(ref_layout.download_export()) - for pref, _ in package_list.prefs(ref, ref_bundle).items(): + for pref in packages: ConanOutput(pref).verbose("Cleaning package cache contents") pref_layout = cache.pkg_layout(pref) if build: @@ -127,18 +144,24 @@ def clean(self, package_list, source=True, build=True, download=True, temp=True, if download: rmdir(pref_layout.download_package()) - def save(self, package_list, tgz_path, no_source=False): + def save(self, package_list: PackagesList, tgz_path, no_source=False) -> None: global_conf = self._api_helpers.global_conf cache = PkgCache(self._conan_api.cache_folder, global_conf) cache_folder = cache.store # Note, this is not the home, but the actual package cache out = ConanOutput() mkdir(os.path.dirname(tgz_path)) + tgz_name = os.path.basename(tgz_path) + compressformat = next((e for e in COMPRESSIONS if tgz_name.endswith(e)), None) + if not compressformat: + raise ConanException(f"Unsupported compression format for {tgz_name}") + compresslevel = get_compress_level(compressformat, global_conf) tar_files: dict[str, str] = {} # {path_in_tar: abs_path} - for ref, ref_bundle in package_list.refs().items(): + for ref, packages in package_list.items(): ref_layout = cache.recipe_layout(ref) recipe_folder = os.path.relpath(ref_layout.base_folder, cache_folder) recipe_folder = recipe_folder.replace("\\", "/") # make win paths portable + ref_bundle = package_list.recipe_dict(ref) ref_bundle["recipe_folder"] = recipe_folder out.info(f"Saving {ref}: {recipe_folder}") # Package only selected folders, not DOWNLOAD one @@ -152,19 +175,20 @@ def save(self, package_list, tgz_path, no_source=False): if os.path.exists(path): tar_files[f"{recipe_folder}/{DOWNLOAD_EXPORT_FOLDER}/{METADATA}"] = path - for pref, pref_bundle in package_list.prefs(ref, ref_bundle).items(): + for pref in packages: pref_layout = cache.pkg_layout(pref) pkg_folder = pref_layout.package() folder = os.path.relpath(pkg_folder, cache_folder) folder = folder.replace("\\", "/") # make win paths portable - pref_bundle["package_folder"] = folder + pkg_dict = package_list.package_dict(pref) + pkg_dict["package_folder"] = folder out.info(f"Saving {pref}: {folder}") tar_files[folder] = os.path.join(cache_folder, folder) if os.path.exists(pref_layout.metadata()): metadata_folder = os.path.relpath(pref_layout.metadata(), cache_folder) metadata_folder = metadata_folder.replace("\\", "/") # make paths portable - pref_bundle["metadata_folder"] = metadata_folder + pkg_dict["metadata_folder"] = metadata_folder out.info(f"Saving {pref} metadata: {metadata_folder}") tar_files[metadata_folder] = os.path.join(cache_folder, metadata_folder) @@ -173,11 +197,12 @@ def save(self, package_list, tgz_path, no_source=False): pkglist_path = os.path.join(tempfile.gettempdir(), "pkglist.json") save(pkglist_path, serialized) tar_files["pkglist.json"] = pkglist_path - compress_files(tar_files, os.path.basename(tgz_path), os.path.dirname(tgz_path), conf=self._conan_api.config, - recursive=True, ref=None, compression_plugin=self.compression_plugin) + compress_files(tar_files, tgz_name, os.path.dirname(tgz_path), compresslevel, + recursive=True, compression_plugin=self.compression_plugin) remove(pkglist_path) + ConanOutput().success(f"Created cache save file: {tgz_path}") - def restore(self, path): + def restore(self, path) -> PackagesList: if not os.path.isfile(path): raise ConanException(f"Restore archive doesn't exist in {path}") @@ -202,7 +227,8 @@ def restore(self, path): # After unzipping the files, we need to update the DB that references these files out = ConanOutput() package_list = PackagesList.deserialize(json.loads(pkglist)) - for ref, ref_bundle in package_list.refs().items(): + for ref, packages in package_list.items(): + ref_bundle = package_list.recipe_dict(ref) ref.timestamp = revision_timestamp_now() ref_bundle["timestamp"] = ref.timestamp try: @@ -215,8 +241,9 @@ def restore(self, path): # In the case of recipes, they are always "in place", so just checking it assert rel_path == recipe_folder, f"{rel_path}!={recipe_folder}" out.info(f"Restore: {ref} in {recipe_folder}") - for pref, pref_bundle in package_list.prefs(ref, ref_bundle).items(): + for pref in packages: pref.timestamp = revision_timestamp_now() + pref_bundle = package_list.package_dict(pref) pref_bundle["timestamp"] = pref.timestamp try: pkg_layout = cache.pkg_layout(pref) @@ -254,17 +281,21 @@ def restore(self, path): def get_backup_sources(self, package_list=None, exclude=True, only_upload=True): """Get list of backup source files currently present in the cache, - either all of them if no argument, or filtered by those belonging to the references in the package_list - - @param package_list: a PackagesList object to filter backup files from (The files should have been downloaded form any of the references in the package_list) - @param exclude: if True, exclude the sources that come from URLs present the core.sources:exclude_urls global conf - @param only_upload: if True, only return the files for packages that are set to be uploaded + either all of them if no argument, or filtered by those belonging to the references + in the package_list + + :param package_list: a PackagesList object to filter backup files from (The files should + have been downloaded form any of the references in the package_list) + :param exclude: if True, exclude the sources that come from URLs present the + core.sources:exclude_urls global conf + :param only_upload: if True, only return the files for packages that are set to be uploaded """ config = self._api_helpers.global_conf download_cache_path = config.get("core.sources:download_cache") download_cache_path = download_cache_path or HomePaths( self._conan_api.cache_folder).default_sources_backup_folder - excluded_urls = config.get("core.sources:exclude_urls", check_type=list, default=[]) if exclude else [] + excluded_urls = config.get("core.sources:exclude_urls", + check_type=list, default=[]) if exclude else [] download_cache = DownloadCache(download_cache_path) return download_cache.get_backup_sources_files(excluded_urls, package_list, only_upload) @@ -276,24 +307,11 @@ def path_to_ref(self, path): result = cache.path_to_ref(base) return result - @property - def compression_plugin(self): - if self._compression_plugin is None: - compression_plugin_path = HomePaths(self._conan_api.home_folder).compression_plugin_path - if not os.path.exists(compression_plugin_path): - self._compression_plugin = False # Avoid FS re-check - return None - mod, _ = load_python_file(compression_plugin_path) - if not hasattr(mod, "tar_extract") or not hasattr(mod, "tar_compress"): - raise ConanException("The 'compression.py' plugin does not contain required `tar_extract` or `tar_compress` functions") - self._compression_plugin = mod - return self._compression_plugin or None - def _resolve_latest_ref(cache, ref): if ref.revision is None or ref.revision == "latest": ref.revision = None - result = cache.get_latest_recipe_reference(ref) + result = cache.get_latest_recipe_revision(ref) if result is None: raise ConanException(f"'{ref}' not found in cache") ref = result @@ -304,7 +322,7 @@ def _resolve_latest_pref(cache, pref): pref.ref = _resolve_latest_ref(cache, pref.ref) if pref.revision is None or pref.revision == "latest": pref.revision = None - result = cache.get_latest_package_reference(pref) + result = cache.get_latest_package_revision(pref) if result is None: raise ConanException(f"'{pref.repr_notime()}' not found in cache") pref = result diff --git a/conan/api/subapi/config.py b/conan/api/subapi/config.py index 4fdb2b91e54..bf3c212b6de 100644 --- a/conan/api/subapi/config.py +++ b/conan/api/subapi/config.py @@ -1,4 +1,3 @@ -import json import os from conan.api.output import ConanOutput @@ -9,10 +8,12 @@ from conan.internal.graph.graph_builder import DepsGraphBuilder from conan.internal.graph.profile_node_definer import consumer_definer from conan.errors import ConanException + +from conan.internal.model.conanconfig import loadconanconfig, saveconanconfig, loadconanconfig_yml from conan.internal.model.conf import BUILT_IN_CONFS from conan.internal.model.pkg_type import PackageType -from conan.api.model import RecipeReference, PkgReference -from conan.internal.util.files import load, save, rmdir, remove +from conan.api.model import RecipeReference, Remote +from conan.internal.util.files import rmdir, remove class ConfigAPI: @@ -42,78 +43,152 @@ def install(self, path_or_url, verify_ssl, config_type=None, args=None, """ from conan.internal.api.config.config_installer import configuration_install cache_folder = self._conan_api.cache_folder - requester = self._conan_api.remotes.requester + requester = self._helpers.requester configuration_install(cache_folder, requester, path_or_url, verify_ssl, config_type=config_type, args=args, source_folder=source_folder, target_folder=target_folder) self._conan_api.reinit() - def install_pkg(self, ref, lockfile=None, force=False, remotes=None, - profile=None) -> PkgReference: + def install_package(self, require, lockfile=None, force=False, remotes=None, profile=None): + ConanOutput().warning("The 'conan config install-pkg' is experimental", + warn_tag="experimental") + require = RecipeReference.loads(require) + required_pkgs = self.fetch_packages([require], lockfile, remotes, profile) + installed_refs = self._install_pkgs(required_pkgs, force) + self._conan_api.reinit() + return installed_refs + + @staticmethod + def load_conanconfig(path, remotes): + if os.path.isdir(path): + path = os.path.join(path, "conanconfig.yml") + requested_requires, urls = loadconanconfig_yml(path) + if urls: + new_remotes = [Remote(f"config_install_url{'_' + str(i)}", url=url) + for i, url in enumerate(urls)] + remotes = remotes or [] + remotes += new_remotes + return requested_requires, remotes + + def install_conanconfig(self, path, lockfile=None, force=False, remotes=None, profile=None): + ConanOutput().warning("The 'conan config install-pkg' is experimental", + warn_tag="experimental") + requested_requires, remotes = self.load_conanconfig(path, remotes) + required_pkgs = self.fetch_packages(requested_requires, lockfile, remotes, profile) + installed_refs = self._install_pkgs(required_pkgs, force) + self._conan_api.reinit() + return installed_refs + + def _install_pkgs(self, required_pkgs, force): + out = ConanOutput() + out.title("Configuration packages to install") + config_version_file = HomePaths(self._conan_api.home_folder).config_version_path + if not os.path.exists(config_version_file): + config_versions = [] + else: + ConanOutput().info(f"Reading existing config-versions file: {config_version_file}") + config_versions = loadconanconfig(config_version_file) + config_versions_dict = {r.name: r for r in config_versions} + if len(config_versions_dict) < len(config_versions): + raise ConanException("There are multiple requirements for the same package " + f"with different versions: {config_version_file}") + + new_config = config_versions_dict.copy() + for required_pkg in required_pkgs: + new_config.pop(required_pkg.ref.name, None) # To ensure new order + new_config[required_pkg.ref.name] = required_pkg.ref + final_config_refs = [r for r in new_config.values()] + + prev_refs = "\n\t".join(repr(r) for r in config_versions) + out.info(f"Previously installed configuration packages:\n\t{prev_refs}") + + new_refs = "\n\t".join(r.repr_notime() for r in final_config_refs) + out.info(f"New configuration packages to install:\n\t{new_refs}") + + if list(config_versions_dict) == list(new_config)[:len(config_versions_dict)]: + # There is no conflict in order, can be done safely + if final_config_refs == config_versions: + if force: + out.warning("The requested configurations are identical to the already " + "installed ones, but forcing re-installation because --force") + to_install = required_pkgs + else: + out.info("The requested configurations are identical to the already " + "installed ones, skipping re-installation") + to_install = [] + else: + out.info("Installing new or updating configuration packages") + to_install = required_pkgs + else: + # Change in order of existing configuration + if force: + out.warning("Installing these configuration packages will break the " + "existing order, with possible side effects. " + "Forcing the installation because --force was defined", warn_tag="risk") + to_install = required_pkgs + else: + msg = ("Installing these configuration packages will break the " + "existing order, with possible side effects, like breaking 'package_ids'.\n" + "If you still want to enforce this configuration you can:\n" + " Use 'conan config clean' first to fully reset your configuration.\n" + " Or use 'conan config install-pkg --force' to force installation.") + raise ConanException(msg) + + out.title("Installing configuration from packages") + # install things and update the Conan cache "config_versions.json" file + from conan.internal.api.config.config_installer import configuration_install + cache_folder = self._conan_api.cache_folder + requester = self._helpers.requester + for pkg in to_install: + out.info(f"Installing configuration from {pkg.ref}") + configuration_install(cache_folder, requester, uri=pkg.conanfile.package_folder, + verify_ssl=False, config_type="dir", + ignore=["conaninfo.txt", "conanmanifest.txt"]) + + saveconanconfig(config_version_file, final_config_refs) + return final_config_refs + + def fetch_packages(self, refs, lockfile=None, remotes=None, profile=None): """ install configuration stored inside a Conan package The installation of configuration will reinitialize the full ConanAPI """ - ConanOutput().warning("The 'conan config install-pkg' is experimental", - warn_tag="experimental") conan_api = self._conan_api remotes = conan_api.remotes.list() if remotes is None else remotes profile_host = profile_build = profile or conan_api.profiles.get_profile([]) app = ConanApp(self._conan_api) - # Computation of a very simple graph that requires "ref" - conanfile = app.loader.load_virtual(requires=[RecipeReference.loads(ref)]) - consumer_definer(conanfile, profile_host, profile_build) - root_node = Node(ref=None, conanfile=conanfile, context=CONTEXT_HOST, recipe=RECIPE_VIRTUAL) - root_node.is_conf = True - update = ["*"] - builder = DepsGraphBuilder(app.proxy, app.loader, app.range_resolver, app.cache, remotes, - update, update, self._helpers.global_conf) - deps_graph = builder.load_graph(root_node, profile_host, profile_build, lockfile) - - # Basic checks of the package: correct package_type and no-dependencies - deps_graph.report_graph_error() - pkg = deps_graph.root.edges[0].dst - ConanOutput().info(f"Configuration from package: {pkg}") - if pkg.conanfile.package_type is not PackageType.CONF: - raise ConanException(f'{pkg.conanfile} is not of package_type="configuration"') - if pkg.edges: - raise ConanException(f"Configuration package {pkg.ref} cannot have dependencies") - - # The computation of the "package_id" and the download of the package is done as usual - # By default we allow all remotes, and build_mode=None, always updating - conan_api.graph.analyze_binaries(deps_graph, None, remotes, update=update, lockfile=lockfile) - conan_api.install.install_binaries(deps_graph=deps_graph, remotes=remotes) - - # We check if this specific version is already installed - config_pref = pkg.pref.repr_notime() - config_versions = [] - config_version_file = HomePaths(conan_api.home_folder).config_version_path - if os.path.exists(config_version_file): - config_versions = json.loads(load(config_version_file)) - config_versions = config_versions["config_version"] - if config_pref in config_versions: - if force: - ConanOutput().info(f"Package '{pkg}' already configured, " - "but re-installation forced") - else: - ConanOutput().info(f"Package '{pkg}' already configured, " - "skipping configuration install") - return pkg.pref # Already installed, we can skip repeating the install + ConanOutput().title("Fetching requested configuration packages") + result = [] + for ref in refs: + # Computation of a very simple graph that requires "ref" + # Need to convert input requires to RecipeReference + conanfile = app.loader.load_virtual(requires=[ref]) + consumer_definer(conanfile, profile_host, profile_build) + root_node = Node(ref=None, conanfile=conanfile, context=CONTEXT_HOST, + recipe=RECIPE_VIRTUAL) + root_node.is_conf = True + update = ["*"] + builder = DepsGraphBuilder(app.proxy, app.loader, app.range_resolver, app.cache, remotes, + update, update, self._helpers.global_conf) + deps_graph = builder.load_graph(root_node, profile_host, profile_build, lockfile) - from conan.internal.api.config.config_installer import configuration_install - cache_folder = self._conan_api.cache_folder - requester = self._conan_api.remotes.requester - configuration_install(cache_folder, requester, uri=pkg.conanfile.package_folder, - verify_ssl=False, config_type="dir", - ignore=["conaninfo.txt", "conanmanifest.txt"]) - # We save the current package full reference in the file for future - # And for ``package_id`` computation - config_versions = {ref.split("/", 1)[0]: ref for ref in config_versions} - config_versions[pkg.pref.ref.name] = pkg.pref.repr_notime() - save(config_version_file, json.dumps({"config_version": list(config_versions.values())})) - self._conan_api.reinit() - return pkg.pref + # Basic checks of the package: correct package_type and no-dependencies + deps_graph.report_graph_error() + pkg = deps_graph.root.edges[0].dst + ConanOutput().info(f"Configuration from package: {pkg}") + if pkg.conanfile.package_type is not PackageType.CONF: + raise ConanException(f'{pkg.conanfile} is not of package_type="configuration"') + if pkg.edges: + raise ConanException(f"Configuration package {pkg.ref} cannot have dependencies") + + # The computation of the "package_id" and the download of the package is done as usual + # By default we allow all remotes, and build_mode=None, always updating + conan_api.graph.analyze_binaries(deps_graph, None, remotes, update=update, + lockfile=lockfile) + conan_api.install.install_binaries(deps_graph=deps_graph, remotes=remotes) + result.append(pkg) + return result def get(self, name, default=None, check_type=None): """ get the value of a global.conf item @@ -150,3 +225,40 @@ def clean(self): self._conan_api.reinit() # CHECK: This also generates a remotes.json that is not there after a conan profile show? self._conan_api.migrate() + + @property + def settings_yml(self): + """ Get the contents of the settings.yml and user_settings.yml files, + which define the possible values for settings. + + Note that this is different from the settings present in a conanfile, + which represent the actual values for a specific package, while this + property represents the possible values for each setting. + + :returns: A read-only object representing the settings scheme, with a + ``possible_values()`` method that returns a dictionary with the possible values for each setting, + and a ``fields`` property that returns an ordered list with the fields of each setting. + Note that it's possible to access nested settings using attribute access, + such as ``settings_yml.compiler.possible_values()``. + """ + + class SettingsYmlInterface: + def __init__(self, settings): + self._settings = settings + + def possible_values(self): + """ returns a dict with the possible values for each setting """ + return self._settings.possible_values() + + @property + def fields(self): + """ returns a dict with the fields of each setting """ + return self._settings.fields + + def __getattr__(self, item): + return SettingsYmlInterface(getattr(self._settings, item)) + + def __str__(self): + return str(self._settings) + + return SettingsYmlInterface(self._helpers.settings_yml) diff --git a/conan/api/subapi/download.py b/conan/api/subapi/download.py index 145e2430d51..a4421a2b4d1 100644 --- a/conan/api/subapi/download.py +++ b/conan/api/subapi/download.py @@ -11,6 +11,7 @@ class DownloadAPI: + """ This API is used to download recipes and packages from a remote server.""" def __init__(self, conan_api): self._conan_api = conan_api @@ -36,7 +37,7 @@ def recipe(self, ref: RecipeReference, remote: Remote, metadata: Optional[List[s if ref.timestamp is None: # we didnt obtain the timestamp before (in general it should be) # Respect the timestamp of the server, the ``get_recipe()`` doesn't do it internally # Best would be that ``get_recipe()`` returns the timestamp in the same call - server_ref = app.remote_manager.get_recipe_revision_reference(ref, remote) + server_ref = app.remote_manager.get_recipe_revision(ref, remote) assert server_ref == ref ref.timestamp = server_ref.timestamp app.remote_manager.get_recipe(ref, remote, metadata) @@ -69,7 +70,7 @@ def package(self, pref: PkgReference, remote: Remote, metadata: Optional[List[st if pref.timestamp is None: # we didn't obtain the timestamp before (in general it should be) # Respect the timestamp of the server - server_pref = app.remote_manager.get_package_revision_reference(pref, remote) + server_pref = app.remote_manager.get_package_revision(pref, remote) assert server_pref == pref pref.timestamp = server_pref.timestamp @@ -79,18 +80,24 @@ def package(self, pref: PkgReference, remote: Remote, metadata: Optional[List[st def download_full(self, package_list: PackagesList, remote: Remote, metadata: Optional[List[str]] = None): - """Download the recipes and packages specified in the package_list from the remote, - parallelized based on `core.download:parallel`""" + """Download the recipes and packages specified in the ``package_list`` from the remote, + parallelized based on ``core.download:parallel``""" def _download_pkglist(pkglist): - for ref, recipe_bundle in pkglist.refs().items(): + for ref, packages in pkglist.items(): self.recipe(ref, remote, metadata) - for pref, _ in pkglist.prefs(ref, recipe_bundle).items(): + ref_dict = pkglist.recipe_dict(ref) + ref_dict.pop("files", None) + ref_dict.pop("upload-urls", None) + for pref in packages: self.package(pref, remote, metadata) + pkg_dict = pkglist.package_dict(pref) + pkg_dict.pop("files", None) + pkg_dict.pop("upload-urls", None) t = time.time() parallel = self._conan_api.config.get("core.download:parallel", default=1, check_type=int) thread_pool = ThreadPool(parallel) if parallel > 1 else None - if not thread_pool or len(package_list.refs()) <= 1: + if not thread_pool or len(package_list._data) <= 1: # FIXME: Iteration when multiple rrevs _download_pkglist(package_list) else: ConanOutput().subtitle(f"Downloading with {parallel} parallel threads") diff --git a/conan/api/subapi/export.py b/conan/api/subapi/export.py index 49557a17947..797364c1574 100644 --- a/conan/api/subapi/export.py +++ b/conan/api/subapi/export.py @@ -1,27 +1,115 @@ +import os +from typing import List, Tuple + +from conan import ConanFile from conan.api.output import ConanOutput +from conan.cli.printers.graph import print_graph_basic from conan.internal.cache.cache import PkgCache from conan.internal.conan_app import ConanApp from conan.internal.api.export import cmd_export from conan.internal.methods import run_package_method from conan.internal.graph.graph import BINARY_BUILD, RECIPE_INCACHE -from conan.api.model import PkgReference +from conan.api.model import PkgReference, Remote, RecipeReference from conan.internal.util.files import mkdir class ExportAPI: + """ This API provides methods to export artifacts, both recipes and pre-compiled package + binaries from user folders to the Conan cache, as Conan recipes and Conan package binaries + """ def __init__(self, conan_api, helpers): self._conan_api = conan_api self._helpers = helpers - def export(self, path, name, version, user, channel, lockfile=None, remotes=None): + def export(self, path, name: str = None, version: str = None, user: str = None, + channel: str = None, lockfile=None, + remotes: List[Remote] = None) -> Tuple[RecipeReference, ConanFile]: + """ Exports a ``conanfile.py`` recipe, together with its associated files to the Conan cache. + A "recipe-revision" will be computed and assigned. + + :param path: Path to the conanfile to be exported + :param name: Optional package name. Typically not necessary as it is defined by the recipe + attribute or dynamically with the ``set_name()`` method. + If it is defined in recipe and as an argument, but they don't match, an error will be raised. + :param version: Optional version. It can be defined in the recipe with the version + attribute or dynamically with the 'set_version()' method. + If it is defined in recipe and as an argument, but they don't match, an error will be raised. + :param user: Optional user. Can be defined by recipe attribute. + If it is defined in recipe and as an argument, but they don't match, an error will be raised. + :param channel: Optional channel. Can be defined by recipe attribute. + If it is defined in recipe and as an argument, but they don't match, an error will be raised. + :param lockfile: Optional, only relevant if the recipe has 'python-requires' to be locked + :param remotes: Optional, only relevant to resolve 'python-requires' in remotes + :return: A tuple of the exported RecipeReference and a ConanFile object + """ ConanOutput().title("Exporting recipe to the cache") app = ConanApp(self._conan_api) hook_manager = self._helpers.hook_manager - return cmd_export(app, hook_manager, self._helpers.global_conf, path, name, version, - user, channel, graph_lock=lockfile, remotes=remotes) + return cmd_export(app.loader, app.cache, hook_manager, self._helpers.global_conf, path, + name, version, user, channel, graph_lock=lockfile, remotes=remotes) + + def export_pkg_graph(self, path, ref: RecipeReference, profile_host, profile_build, + remotes: List[Remote], lockfile=None, is_build_require=False, + skip_binaries=False, output_folder=None): + """Computes a dependency graph for a given configuration, for an already existing (previously + exported) recipe in the Conan cache. This method computes the full dependency graph, using + the profiles, lockfile and remotes information as any other install/graph/create command. + This is necessary in order to compute the "package_id" of the binary being exported + into the Conan cache. + The resulting dependency graph can be passed to ``export_pkg()`` method + + :param path: Path to the conanfile.py in the user folder + :param ref: full RecipeReference, including recipe-revision + :param profile_host: Profile for the host context + :param profile_build: Profile for the build context + :param lockfile: Optional lockfile + :param remotes: List of Remotes + :param is_build_require: In case a package intended to be used as a tool-requires + :param skip_binaries: + :param output_folder: The folder containing output files, like potential environment scripts + :return: A Graph object that can be passed to ``export_pkg()`` method + """ + assert ref.revision, "ref argument must have recipe-revision defined" + conan_api = self._conan_api + deps_graph = conan_api.graph.load_graph_consumer(path, + ref.name, str(ref.version), ref.user, + ref.channel, + profile_host=profile_host, + profile_build=profile_build, + lockfile=lockfile, remotes=remotes, + update=None, + is_build_require=is_build_require) + + print_graph_basic(deps_graph) + deps_graph.report_graph_error() + conan_api.graph.analyze_binaries(deps_graph, build_mode=[ref.name], lockfile=lockfile, + remotes=remotes) + deps_graph.report_graph_error() + + root_node = deps_graph.root + root_node.ref = ref # Make sure the root node revision is well defined + + if not skip_binaries: + # unless the user explicitly opts-out with --skip-binaries, it is necessary to install + # binaries, in case there are build_requires necessary like tool-requires=cmake + # and package() method doing ``cmake.install()`` + # for most cases, deps will be in cache already because of a previous "conan install" + # but if it is not the case, the binaries from remotes will be downloaded + conan_api.install.install_binaries(deps_graph=deps_graph, remotes=remotes) + source_folder = os.path.dirname(path) + conan_api.install.install_consumer(deps_graph=deps_graph, source_folder=source_folder, + output_folder=output_folder) + return deps_graph + + def export_pkg(self, graph, output_folder=None) -> None: + """Executes the ``package()`` method of the exported recipe in order to copy the artifacts + from user folder to the Conan cache package folder - def export_pkg(self, deps_graph, source_folder, output_folder): + :param graph: A Graph object + :param output_folder: Optional folder where generated files like environment scripts + of dependencies have been installed + """ cache = PkgCache(self._conan_api.cache_folder, self._helpers.global_conf) hook_manager = self._helpers.hook_manager @@ -29,8 +117,9 @@ def export_pkg(self, deps_graph, source_folder, output_folder): # to be downloaded from remotes # passing here the create_reference=ref argument is useful so the recipe is in "develop", # because the "package()" method is in develop=True already - pkg_node = deps_graph.root + pkg_node = graph.root ref = pkg_node.ref + source_folder = os.path.dirname(pkg_node.path) out = ConanOutput(scope=pkg_node.conanfile.display_name) out.info("Exporting binary from user folder to Conan cache") conanfile = pkg_node.conanfile diff --git a/conan/api/subapi/graph.py b/conan/api/subapi/graph.py index 1a00b88a613..3fb81c3621a 100644 --- a/conan/api/subapi/graph.py +++ b/conan/api/subapi/graph.py @@ -88,7 +88,8 @@ def load_root_test_conanfile(self, path, tested_reference, profile_host, profile return root_node def _load_root_virtual_conanfile(self, profile_host, profile_build, requires, tool_requires, - lockfile, remotes, update, check_updates=False, python_requires=None): + lockfile, remotes, update, check_updates=False, + python_requires=None): if not python_requires and not requires and not tool_requires: raise ConanException("Provide requires or tool_requires") app = ConanApp(self._conan_api) @@ -108,12 +109,15 @@ def _scope_options(profile, requires, tool_requires): Command line helper to scope options when ``command -o myoption=myvalue`` is used, that needs to be converted to "-o pkg:myoption=myvalue". The "pkg" value will be computed from the given requires/tool_requires + + This is legacy, as options should always be scoped now """ - # FIXME: This helper function here is not great, find a better place if requires and len(requires) == 1 and not tool_requires: - profile.options.scope(requires[0]) - if tool_requires and len(tool_requires) == 1 and not requires: - profile.options.scope(tool_requires[0]) + ref = requires[0] + if str(ref.version).startswith("["): + ref = ref.copy() + ref.version = "*" + profile.options.scope(ref) def load_graph_requires(self, requires, tool_requires, profile_host, profile_build, lockfile, remotes, update, check_updates=False, python_requires=None): @@ -164,8 +168,8 @@ def load_graph(self, root_node, profile_host, profile_build, lockfile=None, remo """ Compute the dependency graph, starting from a root package, evaluation the graph with the provided configuration in profile_build, and profile_host. The resulting graph is a graph of recipes, but packages are not computed yet (package_ids) will be empty in the - result. The result might have errors, like version or configuration conflicts, but it is still - possible to inspect it. Only trying to install such graph will fail + result. The result might have errors, like version or configuration conflicts, but it is + still possible to inspect it. Only trying to install such graph will fail :param root_node: the starting point, an already initialized Node structure, as returned by the "load_root_node" api @@ -189,8 +193,8 @@ def load_graph(self, root_node, profile_host, profile_build, lockfile=None, remo deps_graph = builder.load_graph(root_node, profile_host, profile_build, lockfile) return deps_graph - def analyze_binaries(self, graph, build_mode=None, remotes=None, update=None, lockfile=None, - build_modes_test=None, tested_graph=None): + def analyze_binaries(self, graph, build_mode=None, remotes=None, update=None, + lockfile=None, build_modes_test=None, tested_graph=None): """ Given a dependency graph, will compute the package_ids of all recipes in the graph, and evaluate if they should be built from sources, downloaded from a remote server, of if the packages are already in the local Conan cache @@ -199,8 +203,12 @@ def analyze_binaries(self, graph, build_mode=None, remotes=None, update=None, lo :param graph: a Conan dependency graph, as returned by "load_graph()" :param build_mode: TODO: Discuss if this should be a BuildMode object or list of arguments :param remotes: list of remotes - :param update: (False by default), if Conan should look for newer versions or - revisions for already existing recipes in the Conan cache + :param update: (``False`` by default), if Conan should look for newer versions or + revisions for already existing recipes in the Conan cache. It also accepts an array of + reference patterns to limit the update to those references if any of the items match. + Eg. ``False``, ``None`` or ``[]`` *means no update*, + ``True`` or ``["*"]`` *means update all*, + and ``["pkgA/*", "pkgB/1.0@user/channel"]`` *means to update only specific packages*. :param build_modes_test: the --build-test argument :param tested_graph: In case of a "test_package", the graph being tested """ diff --git a/conan/api/subapi/install.py b/conan/api/subapi/install.py index ff4a141cf67..fa60e903028 100644 --- a/conan/api/subapi/install.py +++ b/conan/api/subapi/install.py @@ -6,7 +6,7 @@ from conan.internal.graph.install_graph import InstallGraph from conan.internal.graph.installer import BinaryInstaller -from conan.errors import ConanInvalidConfiguration +from conan.errors import ConanInvalidConfiguration, ConanException class InstallAPI: @@ -15,10 +15,11 @@ def __init__(self, conan_api, helpers): self._conan_api = conan_api self._helpers = helpers - def install_binaries(self, deps_graph, remotes=None): + def install_binaries(self, deps_graph, remotes=None, return_install_error=False): """ Install binaries for dependency graph :param deps_graph: Dependency graph to intall packages for :param remotes: + :param return_install_error: If True, do not raise an exception, but return it """ app = ConanBasicApp(self._conan_api) installer = BinaryInstaller(app, self._helpers.global_conf, app.editable_packages, @@ -27,7 +28,14 @@ def install_binaries(self, deps_graph, remotes=None): install_graph.raise_errors() install_order = install_graph.install_order() installer.install_system_requires(deps_graph, install_order=install_order) - installer.install(deps_graph, remotes, install_order=install_order) + try: # To be able to capture the output, report or save graph.json, then raise later + installer.install(deps_graph, remotes, install_order=install_order) + except ConanException as e: + # If true, allows to return the exception, so progress can be reported like the + # already built binaries to upload them + if not return_install_error: + raise + return e def install_system_requires(self, graph, only_info=False): """ Install binaries for dependency graph @@ -52,7 +60,8 @@ def install_sources(self, graph, remotes): # TODO: Look for a better name def install_consumer(self, deps_graph, generators=None, source_folder=None, output_folder=None, - deploy=False, deploy_package=None, deploy_folder=None, envs_generation=None): + deploy=False, deploy_package=None, deploy_folder=None, + envs_generation=None): """ Once a dependency graph has been installed, there are things to be done, like invoking generators for the root consumer. This is necessary for example for conanfile.txt/py, or for "conan install -g @@ -93,5 +102,5 @@ def install_consumer(self, deps_graph, generators=None, source_folder=None, outp envs_generation=envs_generation) def deploy(self, graph, deployer, deploy_package=None, deploy_folder=None): - return do_deploys(self._conan_api.home_folder, graph, deployer, deploy_package=deploy_package, - deploy_folder=deploy_folder) + return do_deploys(self._conan_api.home_folder, graph, deployer, + deploy_package=deploy_package, deploy_folder=deploy_folder) diff --git a/conan/api/subapi/list.py b/conan/api/subapi/list.py index 56b6f3461aa..1d3a0ddf8bb 100644 --- a/conan/api/subapi/list.py +++ b/conan/api/subapi/list.py @@ -1,4 +1,3 @@ -import copy import os from collections import OrderedDict from typing import Dict @@ -57,9 +56,9 @@ def latest_recipe_revision(self, ref: RecipeReference, remote: Remote = None): assert ref.revision is None, "latest_recipe_revision: ref already have a revision" app = ConanBasicApp(self._conan_api) if remote: - ret = app.remote_manager.get_latest_recipe_reference(ref, remote=remote) + ret = app.remote_manager.get_latest_recipe_revision(ref, remote=remote) else: - ret = app.cache.get_latest_recipe_reference(ref) + ret = app.cache.get_latest_recipe_revision(ref) return ret @@ -69,9 +68,9 @@ def recipe_revisions(self, ref: RecipeReference, remote: Remote = None): assert ref.revision is None, "recipe_revisions: ref already have a revision" app = ConanBasicApp(self._conan_api) if remote: - results = app.remote_manager.get_recipe_revisions_references(ref, remote=remote) + results = app.remote_manager.get_recipe_revisions(ref, remote=remote) else: - results = app.cache.get_recipe_revisions_references(ref) + results = app.cache.get_recipe_revisions(ref) return results @@ -83,9 +82,9 @@ def latest_package_revision(self, pref: PkgReference, remote=None): assert pref.package_id is not None, "package_id must be defined" app = ConanBasicApp(self._conan_api) if remote: - ret = app.remote_manager.get_latest_package_reference(pref, remote=remote) + ret = app.remote_manager.get_latest_package_revision(pref, remote=remote) else: - ret = app.cache.get_latest_package_reference(pref) + ret = app.cache.get_latest_package_revision(pref) return ret def package_revisions(self, pref: PkgReference, remote=None): @@ -93,23 +92,20 @@ def package_revisions(self, pref: PkgReference, remote=None): "check latest first if needed" app = ConanBasicApp(self._conan_api) if remote: - results = app.remote_manager.get_package_revisions_references(pref, remote=remote) + results = app.remote_manager.get_package_revisions(pref, remote=remote) else: - results = app.cache.get_package_revisions_references(pref, only_latest_prev=False) + results = app.cache.get_package_revisions(pref) return results def _packages_configurations(self, ref: RecipeReference, remote=None) -> Dict[PkgReference, dict]: - assert ref.revision is not None, "packages: ref should have a revision. " \ - "Check latest if needed." + assert ref.revision is not None and ref.revision != "latest", \ + "packages: ref should have a revision. Check latest if needed." app = ConanBasicApp(self._conan_api) if not remote: prefs = app.cache.get_package_references(ref) packages = _get_cache_packages_binary_info(app.cache, prefs) else: - if ref.revision == "latest": - ref.revision = None - ref = app.remote_manager.get_latest_recipe_reference(ref, remote=remote) packages = app.remote_manager.search_packages(remote, ref) return packages @@ -120,8 +116,6 @@ def _filter_packages_configurations(pkg_configurations, query): :param query: str like "os=Windows AND (arch=x86 OR compiler=gcc)" :return: Dict[PkgReference, PkgConfiguration] """ - if query is None: - return pkg_configurations try: if "!" in query: raise ConanException("'!' character is not allowed") @@ -163,7 +157,8 @@ def _filter_packages_profile(packages, profile, ref): return result - def select(self, pattern: ListPattern, package_query=None, remote: Remote = None, lru=None, profile=None) -> PackagesList: + def select(self, pattern: ListPattern, package_query=None, remote: Remote = None, lru=None, + profile=None) -> PackagesList: """For a given pattern, return a list of recipes and packages matching the provided filters. :parameter ListPattern pattern: Search criteria @@ -177,6 +172,7 @@ def select(self, pattern: ListPattern, package_query=None, remote: Remote = None It can be a string like ``"2d"`` (2 days) or ``"3h"`` (3 hours). :parameter Profile profile: Profile to filter the packages by settings and options. """ + # TODO: Implement better error forwarding for "list" command that captures Exceptions if package_query and pattern.package_id and "*" not in pattern.package_id: raise ConanException("Cannot specify '-p' package queries, " "if 'package_id' is not a pattern") @@ -192,7 +188,9 @@ def select(self, pattern: ListPattern, package_query=None, remote: Remote = None remote_name = "local cache" if not remote else remote.name if search_ref: refs = _search_recipes(app, search_ref, remote=remote) - refs = pattern.filter_versions(refs) + global_conf = self._conan_api._api_helpers.global_conf # noqa + resolve_prereleases = global_conf.get("core.version_ranges:resolve_prereleases") + refs = pattern.filter_versions(refs, resolve_prereleases) pattern.check_refs(refs) out.info(f"Found {len(refs)} pkg/version recipes matching {search_ref} in {remote_name}") else: @@ -200,7 +198,8 @@ def select(self, pattern: ListPattern, package_query=None, remote: Remote = None # Show only the recipe references if pattern.package_id is None and pattern.rrev is None: - select_bundle.add_refs(refs) + for r in refs: + select_bundle.add_ref(r) return select_bundle def msg_format(msg, item, total): @@ -222,7 +221,8 @@ def msg_format(msg, item, total): if lru and pattern.package_id is None: # Filter LRUs rrevs = [r for r in rrevs if app.cache.get_recipe_lru(r) < limit_time] - select_bundle.add_refs(rrevs) + for rr in rrevs: + select_bundle.add_ref(rr) if pattern.package_id is None: # Stop if not displaying binaries continue @@ -263,8 +263,12 @@ def msg_format(msg, item, total): if lru: # Filter LRUs prefs = [r for r in prefs if app.cache.get_package_lru(r) < limit_time] - select_bundle.add_prefs(rrev, prefs) - select_bundle.add_configurations(packages) + # Packages dict has been listed, even if empty + select_bundle.recipe_dict(rrev)["packages"] = {} + for p in prefs: + # the "packages" dict is not using the package-revision + pkg_info = packages.get(PkgReference(p.ref, p.package_id)) + select_bundle.add_pref(p, pkg_info) return select_bundle def explain_missing_binaries(self, ref, conaninfo, remotes): @@ -292,7 +296,7 @@ def explain_missing_binaries(self, ref, conaninfo, remotes): candidates.sort() pkglist = PackagesList() - pkglist.add_refs([ref]) + pkglist.add_ref(ref) # Return the closest matches, stop adding when distance is increased candidate_distance = None for candidate in candidates: @@ -300,10 +304,9 @@ def explain_missing_binaries(self, ref, conaninfo, remotes): break candidate_distance = candidate.distance pref = candidate.pref - pkglist.add_prefs(ref, [pref]) - pkglist.add_configurations({pref: candidate.binary_config}) + pkglist.add_pref(pref, candidate.binary_config) # Add the diff data - rev_dict = pkglist.recipes[str(pref.ref)]["revisions"][pref.ref.revision] + rev_dict = pkglist.recipe_dict(ref) rev_dict["packages"][pref.package_id]["diff"] = candidate.serialize() remote = candidate.remote.name if candidate.remote else "Local Cache" rev_dict["packages"][pref.package_id]["remote"] = remote @@ -314,30 +317,51 @@ def find_remotes(self, package_list, remotes): (Experimental) Find the remotes where the current package lists can be found """ result = MultiPackagesList() + app = ConanBasicApp(self._conan_api) for r in remotes: result_pkg_list = PackagesList() - for ref, recipe_bundle in package_list.refs().items(): - ref_no_rev = copy.copy(ref) # TODO: Improve ugly API - ref_no_rev.revision = None + for ref, ref_contents in package_list.serialize().items(): + ref = RecipeReference.loads(ref) try: - revs = self.recipe_revisions(ref_no_rev, remote=r) + remote_rrevs = app.remote_manager.get_recipe_revisions(ref, remote=r) except NotFoundException: continue - if ref not in revs: # not found + revisions = ref_contents.get("revisions") + if revisions is None: # This is a package list just with name/version + if remote_rrevs: + result_pkg_list.add_ref(ref) continue - result_pkg_list.add_refs([ref]) - for pref, pref_bundle in package_list.prefs(ref, recipe_bundle).items(): - pref_no_rev = copy.copy(pref) # TODO: Improve ugly API - pref_no_rev.revision = None - try: - prevs = self.package_revisions(pref_no_rev, remote=r) - except NotFoundException: + + for revision, rev_content in revisions.items(): + ref.revision = revision + # We look for the value of revision in server, to return timestamp too + found = next((r for r in remote_rrevs if r == ref), None) + if not found: + continue + result_pkg_list.add_ref(found) + packages = rev_content.get("packages") + if packages is None: continue - if pref in prevs: - result_pkg_list.add_prefs(ref, [pref]) - info = recipe_bundle["packages"][pref.package_id]["info"] - result_pkg_list.add_configurations({pref: info}) - if result_pkg_list.recipes: + for pkgid, pkgcontent in packages.items(): + pref = PkgReference(ref, pkgid) + try: + remote_prefs = app.remote_manager.get_package_revisions(pref, remote=r) + except NotFoundException: + continue + pkg_revisions = pkgcontent.get("revisions") + if pkg_revisions is None: # This is a package_id, no prevs + if remote_prefs: + result_pkg_list.add_pref(pref, pkgcontent.get("info")) + continue + for pkg_revision, prev_content in pkg_revisions.items(): + pref.revision = pkg_revision + # We look for the value of revision in server, to return timestamp too + pfound = next((r for r in remote_prefs if r == pref), None) + if not pfound: + continue + result_pkg_list.add_pref(pfound, pkgcontent.get("info")) + + if result_pkg_list: result.add(r.name, result_pkg_list) return result @@ -368,9 +392,9 @@ def outdated(self, deps_graph, remotes): remote_ref_list = self.select(ref_pattern, package_query=None, remote=remote) except NotFoundException: continue - if not remote_ref_list.recipes: + if not remote_ref_list: continue - str_latest_ref = list(remote_ref_list.recipes.keys())[-1] + str_latest_ref = list(remote_ref_list.serialize().keys())[-1] recipe_ref = RecipeReference.loads(str_latest_ref) if (node_info["latest_remote"] is None or node_info["latest_remote"]["ref"] < recipe_ref): @@ -507,7 +531,7 @@ def _get_cache_packages_binary_info(cache, prefs) -> Dict[PkgReference, dict]: result = OrderedDict() for pref in prefs: - latest_prev = cache.get_latest_package_reference(pref) + latest_prev = cache.get_latest_package_revision(pref) pkg_layout = cache.pkg_layout(latest_prev) # Read conaninfo diff --git a/conan/api/subapi/lockfile.py b/conan/api/subapi/lockfile.py index bb5ae08c3eb..dc056a3bb46 100644 --- a/conan/api/subapi/lockfile.py +++ b/conan/api/subapi/lockfile.py @@ -13,15 +13,20 @@ def __init__(self, conan_api): self._conan_api = conan_api @staticmethod - def get_lockfile(lockfile=None, conanfile_path=None, cwd=None, partial=False, overrides=None): + def get_lockfile(lockfile=None, conanfile_path=None, cwd=None, partial=False, + overrides=None) -> Lockfile: """ obtain a lockfile, following this logic: - - If lockfile is explicitly defined, it would be either absolute or relative to cwd and - the lockfile file must exist. If lockfile="" (empty string) the default "conan.lock" - lockfile will not be automatically used even if it is present. - - If lockfile is not defined, it will still look for a default conan.lock: - - if conanfile_path is defined, it will be besides it - - if conanfile_path is not defined, the default conan.lock should be in cwd - - if the default conan.lock cannot be found, it is not an error + + If lockfile is explicitly defined, it would be either absolute or relative to cwd and + the lockfile file must exist. If lockfile="" (empty string) the default "conan.lock" + lockfile will not be automatically used even if it is present. + + If lockfile is not defined, it will still look for a default conan.lock: + + - if conanfile_path is defined, it will be besides it + - if conanfile_path is not defined, the default conan.lock should be in cwd + - if the default conan.lock cannot be found, it is not an error + :param partial: If the obtained lockfile will allow partial resolving :param cwd: the current working dir, if None, os.getcwd() will be used diff --git a/conan/api/subapi/new.py b/conan/api/subapi/new.py index 50a73f07fd6..2837fdcaeb7 100644 --- a/conan/api/subapi/new.py +++ b/conan/api/subapi/new.py @@ -166,7 +166,7 @@ def _read_files(self, template_folder): @staticmethod def render(template_files, definitions): result = {} - name = definitions.get("name", "pkg") + name = definitions.get("name", "mypkg") if isinstance(name, list): raise ConanException(f"name argument can't be multiple: {name}") if name != name.lower(): diff --git a/conan/api/subapi/profiles.py b/conan/api/subapi/profiles.py index a254977110f..10b4d4f666a 100644 --- a/conan/api/subapi/profiles.py +++ b/conan/api/subapi/profiles.py @@ -43,8 +43,11 @@ def get_default_build(self): :return: the path to the default "build" profile, either in the cache or as defined by the user in configuration """ - global_conf = self._api_helpers.global_conf - default_profile = global_conf.get("core:default_build_profile", default=DEFAULT_PROFILE_NAME) + default_profile = os.environ.get("CONAN_DEFAULT_BUILD_PROFILE") + if default_profile is None: + global_conf = self._api_helpers.global_conf + default_profile = global_conf.get("core:default_build_profile", + default=DEFAULT_PROFILE_NAME) default_profile = os.path.join(self._home_paths.profiles_path, default_profile) if not os.path.exists(default_profile): msg = ("The default build profile '{}' doesn't exist.\n" diff --git a/conan/api/subapi/remotes.py b/conan/api/subapi/remotes.py index 54191cac7a2..30e754b0694 100644 --- a/conan/api/subapi/remotes.py +++ b/conan/api/subapi/remotes.py @@ -8,7 +8,6 @@ from conan.api.output import ConanOutput from conan.internal.cache.home_paths import HomePaths from conan.internal.conan_app import ConanBasicApp -from conan.internal.rest.conan_requester import ConanRequester from conan.internal.rest.remote_credentials import RemoteCredentials from conan.internal.rest.rest_client_local_recipe_index import add_local_recipes_index_remote, \ remove_local_recipes_index_remote @@ -35,11 +34,6 @@ def __init__(self, conan_api, api_helpers): self._api_helpers = api_helpers self._home_folder = conan_api.home_folder self._remotes_file = HomePaths(self._home_folder).remotes_path - # Wraps an http_requester to inject proxies, certs, etc - self._requester = ConanRequester(api_helpers.global_conf, self._home_folder) - - def reinit(self): - self._requester = ConanRequester(self._api_helpers.global_conf, self._home_folder) def list(self, pattern=None, only_enabled=True): """ @@ -64,7 +58,8 @@ def disable(self, pattern): :param pattern: single ``str`` or list of ``str``. If the pattern is an exact name without wildcards like "*" and no remote is found matching that exact name, it will raise an error. - :return: the list of disabled :ref:`Remote ` objects (even if they were already disabled) + :return: the list of disabled :ref:`Remote ` objects (even if they + were already disabled) """ remotes = _load(self._remotes_file) disabled = _filter(remotes, pattern, only_enabled=False) @@ -82,7 +77,8 @@ def enable(self, pattern): :param pattern: single ``str`` or list of ``str``. If the pattern is an exact name without wildcards like "*" and no remote is found matching that exact name, it will raise an error. - :return: the list of enabled :ref:`Remote ` objects (even if they were already enabled) + :return: the list of enabled :ref:`Remote ` objects (even if they + were already enabled) """ remotes = _load(self._remotes_file) enabled = _filter(remotes, pattern, only_enabled=False) @@ -99,7 +95,8 @@ def get(self, remote_name): Obtain a :ref:`Remote ` object :param remote_name: the exact name of the remote to be returned - :return: the :ref:`Remote ` object, or raise an Exception if the remote does not exist. + :return: the :ref:`Remote ` object, or raise an Exception if the + remote does not exist. """ remotes = _load(self._remotes_file) try: @@ -159,7 +156,7 @@ def remove(self, pattern): return removed def update(self, remote_name: str, url=None, secure=None, disabled=None, index=None, - allowed_packages=None): + allowed_packages=None, recipes_only=None): """ Update an existing remote @@ -169,6 +166,8 @@ def update(self, remote_name: str, url=None, secure=None, disabled=None, index=N :param disabled: optional disabled state :param index: optional integer to change the order of the remote :param allowed_packages: optional list of packages allowed from this remote + :param recipes_only: optional boolean to only allow recipe downloads from this remote, + never package binaries """ remotes = _load(self._remotes_file) try: @@ -186,6 +185,8 @@ def update(self, remote_name: str, url=None, secure=None, disabled=None, index=N remote.disabled = disabled if allowed_packages is not None: remote.allowed_packages = allowed_packages + if recipes_only is not None: + remote.recipes_only = recipes_only if index is not None: remotes = [r for r in remotes if r.name != remote.name] @@ -282,10 +283,6 @@ def user_auth(self, remote: Remote, with_user=False, force=False): user, token, _ = localdb.get_login(remote.url) return user - @property - def requester(self): - return self._requester - def _load(remotes_file): if not os.path.exists(remotes_file): @@ -300,7 +297,8 @@ def _load(remotes_file): result = [] for r in data.get("remotes", []): remote = Remote(r["name"], r["url"], r["verify_ssl"], r.get("disabled", False), - r.get("allowed_packages"), r.get("remote_type")) + r.get("allowed_packages"), r.get("remote_type"), + r.get("recipes_only", False)) result.append(remote) return result @@ -315,6 +313,8 @@ def _save(remotes_file, remotes): remote["allowed_packages"] = r.allowed_packages if r.remote_type: remote["remote_type"] = r.remote_type + if r.recipes_only: + remote["recipes_only"] = r.recipes_only remote_list.append(remote) # This atomic replace avoids a corrupted remotes.json file if this is killed during the process save(remotes_file + ".tmp", json.dumps({"remotes": remote_list}, indent=True)) diff --git a/conan/api/subapi/report.py b/conan/api/subapi/report.py index ad1a5b3f758..134a3f479dc 100644 --- a/conan/api/subapi/report.py +++ b/conan/api/subapi/report.py @@ -14,6 +14,9 @@ class ReportAPI: + """ Used to compute the differences (the "diff") between two versions or revisions, for + both the recipe and source code. + """ def __init__(self, conan_api, helpers): self._conan_api = conan_api self._helpers = helpers @@ -21,6 +24,7 @@ def __init__(self, conan_api, helpers): def diff(self, old_reference, new_reference, remotes, old_path=None, new_path=None, cwd=None): """ Compare two recipes and return the differences. + :param old_reference: The reference of the old recipe. :param new_reference: The reference of the new recipe. :param remotes: List of remotes to search for the recipes. @@ -32,7 +36,8 @@ def diff(self, old_reference, new_reference, remotes, old_path=None, new_path=No def _source(path_to_conanfile, reference): if path_to_conanfile is None: - export_ref, cache_path = _get_ref_from_cache_or_remote(self._conan_api, reference, remotes) + export_ref, cache_path = _get_ref_from_cache_or_remote(self._conan_api, reference, + remotes) else: export_ref, cache_path = _export_recipe_from_path(self._conan_api, path_to_conanfile, reference, remotes, cwd) @@ -41,7 +46,6 @@ def _source(path_to_conanfile, reference): remotes) return export_ref, cache_path - old_export_ref, old_cache_path = _source(old_path, old_reference) new_export_ref, new_cache_path = _source(new_path, new_reference) @@ -57,7 +61,8 @@ def _source(path_to_conanfile, reference): f'"{old_diff_path}" "{new_diff_path}"') ConanOutput().info( - f"Generating diff from {old_export_ref.repr_notime()} to {new_export_ref.repr_notime()} (this might take a while)") + f"Generating diff from {old_export_ref.repr_notime()} to {new_export_ref.repr_notime()} " + f"(this might take a while)") ConanOutput().info(command) stdout, stderr = StringIO(), StringIO() @@ -79,6 +84,7 @@ def _source(path_to_conanfile, reference): "dst_prefix": dst_prefix, } + def _configure_source(conan_api, hook_manager, conanfile_path, ref, remotes): app = ConanApp(conan_api) conanfile = app.loader.load_consumer(conanfile_path, name=ref.name, version=str(ref.version), @@ -101,6 +107,7 @@ def _configure_source(conan_api, hook_manager, conanfile_path, ref, remotes): conanfile.folders.set_base_recipe_metadata(recipe_layout.metadata()) config_source(export_source_folder, conanfile, hook_manager) + def _get_ref_from_cache_or_remote(conan_api, reference, enabled_remotes): ref = RecipeReference.loads(reference) full_ref, matching_remote = None, False @@ -115,19 +122,20 @@ def _get_ref_from_cache_or_remote(conan_api, reference, enabled_remotes): full_ref = ref matching_remote = remote break - except: + except (Exception,): continue else: try: latest_recipe_revision = conan_api.list.latest_recipe_revision(ref, remote) - except: + except (Exception,): continue if full_ref is None or (latest_recipe_revision.timestamp > full_ref.timestamp): full_ref = latest_recipe_revision matching_remote = remote if full_ref is None or matching_remote is False: raise ConanException(f"No matching reference for {reference} in remotes.\n" - "If you want to check against a local recipe, add an additional --{old,new}-path arg.\n") + "If you want to check against a local recipe, add an " + "additional --{old,new}-path arg.\n") if matching_remote is not None: conan_api.download.recipe(full_ref, matching_remote) cache_path = conan_api.cache.export_path(full_ref) diff --git a/conan/api/subapi/upload.py b/conan/api/subapi/upload.py index ca46e0a81b1..944a24750d2 100644 --- a/conan/api/subapi/upload.py +++ b/conan/api/subapi/upload.py @@ -1,7 +1,9 @@ import os import time from multiprocessing.pool import ThreadPool +from typing import List +from conan.api.model import PackagesList, Remote from conan.api.output import ConanOutput from conan.internal.api.upload import add_urls from conan.internal.conan_app import ConanApp @@ -14,35 +16,52 @@ class UploadAPI: + """ This API is used to upload recipes and packages to a remote server.""" def __init__(self, conan_api, api_helpers): self._conan_api = conan_api self._api_helpers = api_helpers - def check_upstream(self, package_list, remote, enabled_remotes, force=False): - """Check if the artifacts are already in the specified remote, skipping them from - the package_list in that case""" + def check_upstream(self, package_list: PackagesList, remote: Remote, + enabled_remotes: List[Remote], force=False): + """ Checks ``remote`` for the existence of the recipes and packages in ``package_list``. + Items that are not present in the remote will add an ``upload`` key to the entry + with the value ``True``. + + If the recipe has an upload policy of ``skip``, it will be discarded from the upload list. + + :parameter package_list: A ``PackagesList`` object with the recipes and packages to check. + :parameter remote: Remote to check. + :parameter enabled_remotes: List of enabled remotes. This is used to possibly load + python_requires from the listed recipes if necessary. + :parameter force: If ``True``, it will skip the check and mark that all items need to be + uploaded. A ``force_upload`` key will be added to the entries that will be uploaded. + """ app = ConanApp(self._conan_api) - for ref, bundle in package_list.refs().items(): + for ref, _ in package_list.items(): layout = app.cache.recipe_layout(ref) conanfile_path = layout.conanfile() conanfile = app.loader.load_basic(conanfile_path, remotes=enabled_remotes) if conanfile.upload_policy == "skip": ConanOutput().info(f"{ref}: Skipping upload of binaries, " "because upload_policy='skip'") - bundle["packages"] = {} + package_list.recipe_dict(ref)["packages"] = {} UploadUpstreamChecker(app).check(package_list, remote, force) - def prepare(self, package_list, enabled_remotes, metadata=None): + def prepare(self, package_list: PackagesList, enabled_remotes: List[Remote], + metadata: List[str] = None): """Compress the recipes and packages and fill the upload_data objects with the complete information. It doesn't perform the upload nor checks upstream to see if the recipe is still there - :param package_list: - :param enabled_remotes: - :param metadata: A list of patterns of metadata that should be uploaded. Default None - means all metadata will be uploaded together with the pkg artifacts. If metadata is empty - string (""), it means that no metadata files should be uploaded.""" + + :param package_list: A PackagesList object with the recipes and packages to upload. + :param enabled_remotes: A list of remotes that are enabled in the client. + Recipe sources will attempt to be fetched from these remotes. + :param metadata: A list of patterns of metadata that should be uploaded. + Default ``None`` means all metadata will be uploaded together with the package artifacts. + If metadata contains an empty string (``""``), + it means that no metadata files should be uploaded.""" if metadata and metadata != [''] and '' in metadata: raise ConanException("Empty string and patterns can not be mixed for metadata.") app = ConanApp(self._conan_api) @@ -54,21 +73,42 @@ def prepare(self, package_list, enabled_remotes, metadata=None): # This might add files entries to package_list with signatures signer.sign(package_list) - def upload(self, package_list, remote): + def _upload(self, package_list, remote): app = ConanApp(self._conan_api) app.remote_manager.check_credentials(remote) executor = UploadExecutor(app) executor.upload(package_list, remote) - def upload_full(self, package_list, remote, enabled_remotes, check_integrity=False, force=False, - metadata=None, dry_run=False): + def upload_full(self, package_list: PackagesList, remote: Remote, enabled_remotes: List[Remote], + check_integrity=False, force=False, metadata: List[str] = None, dry_run=False): """ Does the whole process of uploading, including the possibility of parallelizing - per recipe based on `core.upload:parallel`: - - calls check_integrity - - checks which revision already exist in the server (not necessary to upload) - - prepare the artifacts to upload (compress .tgz) - - execute the actual upload - - upload potential sources backups + per recipe based on the ``core.upload:parallel`` conf. + + The steps that this method performs are: + - calls ``conan_api.cache.check_integrity`` to ensure the packages are not corrupted + - checks the upload policy of the recipes + - (if it is ``"skip"``, it will not upload the binaries, but will still upload + the metadata) + - checks which revisions already exist in the server so that it can skip the upload + - prepares the artifacts to upload (compresses the conan_package.tgz) + - executes the actual upload + - uploads associated sources backups if any + + :param package_list: A PackagesList object with the recipes and packages to upload. + :param remote: The remote to upload the packages to. + :param enabled_remotes: A list of remotes that are enabled in the client. + Recipe sources will attempt to be fetched from these remotes, + and to possibly load python_requires from the listed recipes if necessary. + :param check_integrity: If ``True``, it will check the integrity of the cache packages + before uploading them. This is useful to ensure that the packages are not corrupted. + :param force: If ``True``, it will force the upload of the recipes and packages, + even if they already exist in the remote. Note that this might update the timestamps + :param metadata: A list of patterns of metadata that should be uploaded. + Default ``None`` means all metadata will be uploaded together with the package artifacts. + If metadata contains an empty string (``""``), + it means that no metadata files should be uploaded. + :param dry_run: If ``True``, it will not perform the actual upload, + but will still prepare the artifacts and check the upstream. """ def _upload_pkglist(pkglist, subtitle=lambda _: None): @@ -76,14 +116,14 @@ def _upload_pkglist(pkglist, subtitle=lambda _: None): subtitle("Checking integrity of cache packages") self._conan_api.cache.check_integrity(pkglist) # Check if the recipes/packages are in the remote - subtitle("Checking server existing packages") + subtitle("Checking server for existing packages") self.check_upstream(pkglist, remote, enabled_remotes, force) subtitle("Preparing artifacts for upload") self.prepare(pkglist, enabled_remotes, metadata) if not dry_run: subtitle("Uploading artifacts") - self.upload(pkglist, remote) + self._upload(pkglist, remote) backup_files = self._conan_api.cache.get_backup_sources(pkglist) self.upload_backup_sources(backup_files) @@ -91,7 +131,7 @@ def _upload_pkglist(pkglist, subtitle=lambda _: None): ConanOutput().title(f"Uploading to remote {remote.name}") parallel = self._conan_api.config.get("core.upload:parallel", default=1, check_type=int) thread_pool = ThreadPool(parallel) if parallel > 1 else None - if not thread_pool or len(package_list.recipes) <= 1: + if not thread_pool or len(package_list._data) <= 1: # FIXME: Iteration when multiple rrevs _upload_pkglist(package_list, subtitle=ConanOutput().subtitle) else: ConanOutput().subtitle(f"Uploading with {parallel} parallel threads") @@ -103,7 +143,13 @@ def _upload_pkglist(pkglist, subtitle=lambda _: None): ConanOutput().success(f"Upload completed in {int(elapsed)}s\n") add_urls(package_list, remote) - def upload_backup_sources(self, files): + def upload_backup_sources(self, files: List) -> None: + """ + Upload to the server the backup sources files, that have been typically gathered by + CacheAPI.get_backup_sources() + + :param files: The list of files that must be uploaded + """ config = self._api_helpers.global_conf url = config.get("core.sources:upload_url", check_type=str) if url is None: @@ -114,9 +160,9 @@ def upload_backup_sources(self, files): output.subtitle("Uploading backup sources") if not files: output.info("No backup sources files to upload") - return files + return - requester = self._conan_api.remotes.requester + requester = self._api_helpers.requester uploader = FileUploader(requester, verify=True, config=config, source_credentials=True) # TODO: For Artifactory, we can list all files once and check from there instead # of 1 request per file, but this is more general @@ -142,4 +188,3 @@ def upload_backup_sources(self, files): f"/permissions, please provide 'source_credentials.json': {e}") output.success("Upload backup sources complete\n") - return files diff --git a/conan/api/subapi/workspace.py b/conan/api/subapi/workspace.py index ddfec264a8c..d2f56bac545 100644 --- a/conan/api/subapi/workspace.py +++ b/conan/api/subapi/workspace.py @@ -11,13 +11,17 @@ from conan.cli.printers.graph import print_graph_basic, print_graph_packages from conan.errors import ConanException from conan.internal.conan_app import ConanApp +from conan.internal.errors import conanfile_exception_formatter from conan.internal.graph.install_graph import ProfileArgs +from conan.internal.methods import auto_language, auto_shared_fpic_config_options, \ + auto_shared_fpic_configure +from conan.internal.model.options import Options from conan.internal.model.workspace import Workspace, WORKSPACE_YML, WORKSPACE_PY, WORKSPACE_FOLDER from conan.tools.scm import Git -from conan.internal.graph.graph import RECIPE_EDITABLE, DepsGraph, CONTEXT_HOST, RECIPE_VIRTUAL, Node, \ - RECIPE_CONSUMER +from conan.internal.graph.graph import (RECIPE_EDITABLE, DepsGraph, CONTEXT_HOST, RECIPE_VIRTUAL, + Node, RECIPE_CONSUMER) from conan.internal.graph.graph import TransitiveRequirement -from conan.internal.graph.profile_node_definer import consumer_definer +from conan.internal.graph.profile_node_definer import consumer_definer, initialize_conanfile_profile from conan.internal.loader import load_python_file from conan.internal.source import retrieve_exports_sources from conan.internal.util.files import merge_directories, save @@ -91,7 +95,6 @@ def __init__(self, conan_api): def enable(self, value): self._enabled = value - @property def name(self): self._check_ws() return self._ws.name() @@ -114,7 +117,7 @@ def packages(self): rel_path = editable_info["path"] path = os.path.normpath(os.path.join(self._folder, rel_path, "conanfile.py")) if not os.path.isfile(path): - raise ConanException(f"Workspace editable not found: {path}") + raise ConanException(f"Workspace package not found: {path}") ref = editable_info.get("ref") try: if ref is None: @@ -124,12 +127,12 @@ def packages(self): else: reference = RecipeReference.loads(ref) reference.validate_ref(reference) - except: - raise ConanException(f"Workspace editable reference could not be deduced by" + except Exception as e: + raise ConanException(f"Workspace package reference could not be deduced by" f" {rel_path}/conanfile.py or it is not" - f" correctly defined in the conanws.yml file.") + f" correctly defined in the conanws.yml file: {e}") if reference in packages: - raise ConanException(f"Workspace editable reference '{str(reference)}' already exists.") + raise ConanException(f"Workspace package '{str(reference)}' already exists.") packages[reference] = {"path": path} if editable_info.get("output_folder"): packages[reference]["output_folder"] = ( @@ -137,9 +140,10 @@ def packages(self): ) return packages - def open(self, require, remotes, cwd=None): + def open(self, ref, remotes, cwd=None): + cwd = cwd or os.getcwd() app = ConanApp(self._conan_api) - ref = RecipeReference.loads(require) + ref = RecipeReference.loads(ref) if isinstance(ref, str) else ref recipe = app.proxy.get_recipe(ref, remotes, update=False, check_update=False) layout, recipe_status, remote = recipe @@ -150,7 +154,7 @@ def open(self, require, remotes, cwd=None): conanfile, module = app.loader.load_basic_module(conanfile_path, remotes=remotes) scm = conanfile.conan_data.get("scm") if conanfile.conan_data else None - dst_path = os.path.join(cwd or os.getcwd(), ref.name) + dst_path = os.path.join(cwd, ref.name) if scm is None: conanfile.output.warning("conandata doesn't contain 'scm' information\n" "doing a local copy!!!") @@ -200,6 +204,45 @@ def add(self, path, name=None, version=None, user=None, channel=None, cwd=None, self._ws.add(ref, full_path, output_folder) return ref + def complete(self, profile_host, profile_build, lockfile, remotes, update): + packages = self.packages() + if not packages: + ConanOutput().info("There are no packages in this workspace, nothing to complete") + return + + for ref, info in packages.items(): + ConanOutput().title(f"Computing the dependency graph for package: {ref}") + gapi = self._conan_api.graph + deps_graph = gapi.load_graph_requires([ref], None, profile_host, profile_build, + lockfile, remotes, update) + deps_graph.report_graph_error() + print_graph_basic(deps_graph) + + nodes_to_complete = [] + for node in deps_graph.nodes[1:]: # Exclude the current virtual root + if node.recipe != RECIPE_EDITABLE: + # sanity check, a pacakge in the cache cannot have dependencies to the workspace + if any(d.node.recipe == RECIPE_EDITABLE for d in node.transitive_deps.values()): + nodes_to_complete.append(node) + + if not nodes_to_complete: + ConanOutput().info("There are no intermediate packages to add to the workspace") + return + + for node in nodes_to_complete: + full_path = os.path.join(self._folder, node.name, "conanfile.py") + dep_ref = node.ref + ConanOutput().info(f"Adding to workspace {dep_ref}") + try: + self._ws.add(dep_ref, full_path, output_folder=None) + except ConanException: + if os.path.isfile(full_path): + raise + ConanOutput().info(f"Conanfile in {node.name} not found, trying " + "to open it first") + self.open(dep_ref, remotes, cwd=self._folder) + self._ws.add(dep_ref, full_path, output_folder=None) + @staticmethod def init(path): abs_path = make_abs_path(path) @@ -234,24 +277,72 @@ def clean(self): def info(self): self._check_ws() - return {"name": self.name, + return {"name": self._ws.name(), "folder": self._folder, "packages": self._ws.packages()} + @staticmethod + def _init_options(conanfile, options): + if hasattr(conanfile, "config_options"): + with conanfile_exception_formatter(conanfile, "config_options"): + conanfile.config_options() + elif "auto_shared_fpic" in conanfile.implements: + auto_shared_fpic_config_options(conanfile) + + auto_language(conanfile) # default implementation removes `compiler.cstd` + + # Assign only the current package options values, but none of the dependencies + conanfile.options.apply_downstream(Options(), options, None, True) + + if hasattr(conanfile, "configure"): + with conanfile_exception_formatter(conanfile, "configure"): + conanfile.configure() + elif "auto_shared_fpic" in conanfile.implements: + auto_shared_fpic_configure(conanfile) + def super_build_graph(self, deps_graph, profile_host, profile_build): + order = [] + packages = self.packages() + + def find_folder(ref): + return next(os.path.dirname(os.path.relpath(p["path"], self._folder)) for p_ref, p in + packages.items() if p_ref == ref) + + for level in deps_graph.by_levels(): + items = [item for item in level if item.recipe == "Editable"] + level_order = [] + for node in items: + conanfile = node.conanfile + if hasattr(conanfile, "layout"): + with conanfile_exception_formatter(conanfile, "layout"): + conanfile.layout() + base_folder = find_folder(node.ref) + src_folder = os.path.normpath(os.path.join(base_folder, conanfile.folders.source)) + level_order.append({"ref": node.ref, "folder": src_folder.replace("\\", "/")}) + order.append(level_order) + + self._ws.build_order(order) + ConanOutput().title("Collapsing workspace packages") root_class = self._ws.root_conanfile() if root_class is not None: conanfile = root_class(f"{WORKSPACE_PY} base project Conanfile") - consumer_definer(conanfile, profile_host, profile_build) - root = Node(None, conanfile, context=CONTEXT_HOST, recipe=RECIPE_CONSUMER, - path=self._folder) # path lets use the conanws.py folder - root.should_build = True # It is a consumer, this is something we are building + # To inject things like cmd_wrapper to the consumer conanfile, so self.run() works + helpers = ConanApp(self._conan_api).loader._conanfile_helpers # noqa + conanfile._conan_helpers = helpers + conanfile._conan_is_consumer = True + initialize_conanfile_profile(conanfile, profile_build, profile_host, CONTEXT_HOST, + is_build_require=False) + # consumer_definer(conanfile, profile_host, profile_build) + self._init_options(conanfile, profile_host.options) for field in ("requires", "build_requires", "test_requires", "requirements", "build", "source", "package"): if getattr(conanfile, field, None): raise ConanException(f"Conanfile in conanws.py shouldn't have '{field}'") + root = Node(None, conanfile, context=CONTEXT_HOST, recipe=RECIPE_CONSUMER, + path=self._folder) # path lets use the conanws.py folder + root.should_build = True # It is a consumer, this is something we are building else: ConanOutput().info(f"Workspace {WORKSPACE_PY} not found in the workspace folder, " "using default behavior") @@ -261,10 +352,16 @@ def super_build_graph(self, deps_graph, profile_host, profile_build): result = DepsGraph() # TODO: We might need to copy more information from the original graph result.add_node(root) + conanfile.workspace_packages = {} + + self._check_graph(deps_graph) for node in deps_graph.nodes[1:]: # Exclude the current root if node.recipe != RECIPE_EDITABLE: result.add_node(node) continue + # At the moment we are exposing the full conanfile, docs will warn against usage of + # non pure functions + conanfile.workspace_packages[node.ref] = node.conanfile for r, t in node.transitive_deps.items(): if t.node.recipe == RECIPE_EDITABLE: continue @@ -282,6 +379,20 @@ def super_build_graph(self, deps_graph, profile_host, profile_build): return result + @staticmethod + def _check_graph(graph): + for node in graph.nodes[1:]: # Exclude the current root + if node.recipe != RECIPE_EDITABLE: + # sanity check, a pacakge in the cache cannot have dependencies to the workspace + deps_edit = [d.node for d in node.transitive_deps.values() + if d.node.recipe == RECIPE_EDITABLE] + if deps_edit: + raise ConanException(f"Workspace definition error. Package {node} in the " + f"Conan cache has dependencies to packages " + f"in the workspace: {deps_edit}\n" + "Try the 'conan workspace complete' to open/add " + "intermediate packages") + def export(self, lockfile=None, remotes=None): self._check_ws() exported = [] @@ -293,7 +404,6 @@ def export(self, lockfile=None, remotes=None): exported.append(ref) return exported - def select_packages(self, packages): self._check_ws() editable = self.packages() @@ -323,6 +433,9 @@ def build_order(self, packages, profile_host, profile_build, build_mode, lockfil remotes, update) deps_graph.report_graph_error() print_graph_basic(deps_graph) + + self._check_graph(deps_graph) + conan_api.graph.analyze_binaries(deps_graph, build_mode, remotes=remotes, update=update, lockfile=lockfile) print_graph_packages(deps_graph) diff --git a/conan/cli/args.py b/conan/cli/args.py index 95b889c45db..4e4b148a506 100644 --- a/conan/cli/args.py +++ b/conan/cli/args.py @@ -27,29 +27,31 @@ def add_lockfile_args(parser): - parser.add_argument("-l", "--lockfile", action=OnceArgument, - help="Path to a lockfile. Use --lockfile=\"\" to avoid automatic use of " - "existing 'conan.lock' file") - parser.add_argument("--lockfile-partial", action="store_true", - help="Do not raise an error if some dependency is not found in lockfile") - parser.add_argument("--lockfile-out", action=OnceArgument, - help="Filename of the updated lockfile") - parser.add_argument("--lockfile-packages", action="store_true", - help=argparse.SUPPRESS) - parser.add_argument("--lockfile-clean", action="store_true", - help="Remove unused entries from the lockfile") - parser.add_argument("--lockfile-overrides", - help="Overwrite lockfile overrides") + group = parser.add_argument_group("lockfile arguments") + group.add_argument("-l", "--lockfile", action=OnceArgument, + help="Path to a lockfile. Use --lockfile=\"\" to avoid automatic use of " + "existing 'conan.lock' file") + group.add_argument("--lockfile-partial", action="store_true", + help="Do not raise an error if some dependency is not found in lockfile") + group.add_argument("--lockfile-out", action=OnceArgument, + help="Filename of the updated lockfile") + group.add_argument("--lockfile-packages", action="store_true", + help=argparse.SUPPRESS) + group.add_argument("--lockfile-clean", action="store_true", + help="Remove unused entries from the lockfile") + group.add_argument("--lockfile-overrides", + help="Overwrite lockfile overrides") def add_common_install_arguments(parser): parser.add_argument("-b", "--build", action="append", help=_help_build_policies) - group = parser.add_mutually_exclusive_group() - group.add_argument("-r", "--remote", action="append", default=None, - help='Look in the specified remote or remotes server') - group.add_argument("-nr", "--no-remote", action="store_true", - help='Do not use remote, resolve exclusively in the cache') + group = parser.add_argument_group("remote arguments") + exclusive_group = group.add_mutually_exclusive_group() + exclusive_group.add_argument("-r", "--remote", action="append", default=None, + help='Look in the specified remote or remotes server') + exclusive_group.add_argument("-nr", "--no-remote", action="store_true", + help='Do not use remote, resolve exclusively in the cache') update_help = ("Will install newer versions and/or revisions in the local cache " "for the given reference name, or all references in the graph if no argument is supplied. " @@ -57,13 +59,15 @@ def add_common_install_arguments(parser): "satisfies the range. It will update to the " "latest revision for the resolved version range.") - parser.add_argument("-u", "--update", action="append", nargs="?", help=update_help, const="*") + group.add_argument("-u", "--update", action="append", nargs="?", help=update_help, const="*") add_profiles_args(parser) def add_profiles_args(parser): contexts = ["build", "host"] + group = parser.add_argument_group("profile arguments") + # This comes from the _AppendAction code but modified to add to the contexts class ContextAllAction(argparse.Action): @@ -75,29 +79,29 @@ def __call__(self, action_parser, namespace, values, option_string=None): setattr(namespace, self.dest + "_" + context, items) def create_config(short, long, example=None): - parser.add_argument(f"-{short}", f"--{long}", - default=None, - action="append", - dest=f"{long}_host", - metavar=long.upper(), - help=f'Apply the specified {long}. ' - f'By default, or if specifying -{short}:h (--{long}:host), it applies to the host context. ' - f'Use -{short}:b (--{long}:build) to specify the build context, ' - f'or -{short}:a (--{long}:all) to specify both contexts at once' - + ('' if not example else f". Example: {example}")) + group.add_argument(f"-{short}", f"--{long}", + default=None, + action="append", + dest=f"{long}_host", + metavar=long.upper(), + help=f'Apply the specified {long}. ' + f'By default, or if specifying -{short}:h (--{long}:host), it applies to the host context. ' + f'Use -{short}:b (--{long}:build) to specify the build context, ' + f'or -{short}:a (--{long}:all) to specify both contexts at once' + + ('' if not example else f". Example: {example}")) for context in contexts: - parser.add_argument(f"-{short}:{context[0]}", f"--{long}:{context}", - default=None, - action="append", - dest=f"{long}_{context}", - help="") - - parser.add_argument(f"-{short}:a", f"--{long}:all", - default=None, - action=ContextAllAction, - dest=long, - metavar=f"{long.upper()}_ALL", - help="") + group.add_argument(f"-{short}:{context[0]}", f"--{long}:{context}", + default=None, + action="append", + dest=f"{long}_{context}", + help="") + + group.add_argument(f"-{short}:a", f"--{long}:all", + default=None, + action=ContextAllAction, + dest=long, + metavar=f"{long.upper()}_ALL", + help="") create_config("pr", "profile") create_config("o", "options", '-o="pkg/*:with_qt=True"') @@ -106,27 +110,31 @@ def create_config(short, long, example=None): def add_reference_args(parser): - parser.add_argument("--name", action=OnceArgument, - help='Provide a package name if not specified in conanfile') - parser.add_argument("--version", action=OnceArgument, - help='Provide a package version if not specified in conanfile') - parser.add_argument("--user", action=OnceArgument, - help='Provide a user if not specified in conanfile') - parser.add_argument("--channel", action=OnceArgument, - help='Provide a channel if not specified in conanfile') + group = parser.add_argument_group("reference arguments") + group.add_argument("--name", action=OnceArgument, + help='Provide a package name if not specified in conanfile') + group.add_argument("--version", action=OnceArgument, + help='Provide a package version if not specified in conanfile') + group.add_argument("--user", action=OnceArgument, + help='Provide a user if not specified in conanfile') + group.add_argument("--channel", action=OnceArgument, + help='Provide a channel if not specified in conanfile') def common_graph_args(subparser): subparser.add_argument("path", nargs="?", help="Path to a folder containing a recipe (conanfile.py " "or conanfile.txt) or to a recipe file. e.g., " - "./my_project/conanfile.txt.") - add_reference_args(subparser) + "./my_project/conanfile.txt. Defaults to the current " + "directory when no --requires or --tool-requires is " + "given", + default=None) + add_common_install_arguments(subparser) subparser.add_argument("--requires", action="append", help='Directly provide requires instead of a conanfile') subparser.add_argument("--tool-requires", action='append', help='Directly provide tool-requires instead of a conanfile') - add_common_install_arguments(subparser) + add_reference_args(subparser) add_lockfile_args(subparser) @@ -134,10 +142,13 @@ def validate_common_graph_args(args): if args.requires and (args.name or args.version or args.user or args.channel): raise ConanException("Can't use --name, --version, --user or --channel arguments with " "--requires") - if not args.path and not args.requires and not args.tool_requires: - raise ConanException("Please specify a path to a conanfile or a '--requires='") if args.path and (args.requires or args.tool_requires): raise ConanException("--requires and --tool-requires arguments are incompatible with " f"[path] '{args.path}' argument") - if not args.path and args.build_require: + + if not args.requires and not args.tool_requires and args.path is None: + args.path = "." + + # graph build-order command does not define a build-require argument + if not args.path and getattr(args, "build_require", False): raise ConanException("--build-require should only be used with argument") diff --git a/conan/cli/cli.py b/conan/cli/cli.py index 4a9ed3ecec8..889c2ae9ce6 100644 --- a/conan/cli/cli.py +++ b/conan/cli/cli.py @@ -95,6 +95,7 @@ def _add_command(self, import_path, method_name, package=None): if command_wrapper.doc: name = f"{package}:{command_wrapper.name}" if package else command_wrapper.name self._commands[name] = command_wrapper + command_wrapper._prog = name # set the program name with possible package, if any # Avoiding duplicated command help messages if name not in self._groups[command_wrapper.group]: self._groups[command_wrapper.group].append(name) @@ -236,17 +237,6 @@ def exception_exit_error(exception): return ERROR_UNEXPECTED -def _warn_python_version(): - version = sys.version_info - if version.minor == 6: - ConanOutput().writeln("") - ConanOutput().warning("*"*80, warn_tag="deprecated") - ConanOutput().warning("Python 3.6 is end-of-life since 2021. " - "Conan future versions will drop support for it, " - "please upgrade Python", warn_tag="deprecated") - ConanOutput().warning("*" * 80, warn_tag="deprecated") - - def _warn_frozen_center(conan_api): remotes = conan_api.remotes.list() for r in remotes: @@ -306,7 +296,6 @@ def ctrl_break_handler(_, __): error = SUCCESS try: cli.run(args) - _warn_python_version() except BaseException as e: error = cli.exception_exit_error(e) sys.exit(error) diff --git a/conan/cli/command.py b/conan/cli/command.py index 4958f3ec5bf..e364aced2bf 100644 --- a/conan/cli/command.py +++ b/conan/cli/command.py @@ -46,11 +46,16 @@ def __init__(self, method, formatters=None): "its use briefly.".format(self._name)) @staticmethod - def _init_log_levels(parser): + def _init_core_options(parser): + # Define possible levels, including "" for verbose + possible_levels = list(ConanOutput.valid_log_levels().keys()) + possible_levels.pop(possible_levels.index(None)) parser.add_argument("-v", default="status", nargs='?', help="Level of detail of the output. Valid options from less verbose " "to more verbose: -vquiet, -verror, -vwarning, -vnotice, -vstatus, " - "-v or -vverbose, -vv or -vdebug, -vvv or -vtrace") + "-v or -vverbose, -vv or -vdebug, -vvv or -vtrace", + choices=possible_levels, + ) parser.add_argument("-cc", "--core-conf", action="append", help="Define core configuration, overwriting global.conf " "values. E.g.: -cc core:non_interactive=True") @@ -70,7 +75,8 @@ def _init_formatters(self, parser): parser.add_argument('-f', '--format', action=OnceArgument, help=help_message) parser.add_argument("--out-file", action=OnceArgument, - help="Write the output of the command to the specified file instead of stdout.") + help="Write the output of the command to the specified file instead of " + "stdout.") @property def name(self): @@ -110,7 +116,12 @@ def _format(self, parser, info, *args): def _dispatch_errors(info): if info and isinstance(info, dict): if info.get("conan_error"): - raise ConanException(info["conan_error"]) + e = info["conan_error"] + # Storing and launching an exception is better than the string, as it keeps + # the correct backtrace for debugging. + if isinstance(e, Exception): + raise e + raise ConanException(e) if info.get("conan_warning"): ConanOutput().warning(info["conan_warning"]) @@ -146,6 +157,7 @@ def __init__(self, method, group=None, formatters=None): self._subcommands = {} self._group = group or "Other" self._name = method.__name__.replace("_", "-") + self._prog = self._name def add_subcommand(self, subcommand): subcommand.set_name(self.name) @@ -153,10 +165,11 @@ def add_subcommand(self, subcommand): def run_cli(self, conan_api, *args): parser = ConanArgumentParser(conan_api, description=self._doc, - prog="conan {}".format(self._name), + prog="conan {}".format(self._prog), formatter_class=SmartFormatter) - self._init_log_levels(parser) self._init_formatters(parser) + self._init_core_options(parser) + parser.suggest_on_error = True info = self._method(conan_api, parser, *args) if not self._subcommands: return info @@ -175,10 +188,11 @@ def run_cli(self, conan_api, *args): def run(self, conan_api, *args): parser = ConanArgumentParser(conan_api, description=self._doc, - prog="conan {}".format(self._name), + prog="conan {}".format(self._prog), formatter_class=SmartFormatter) - self._init_log_levels(parser) self._init_formatters(parser) + self._init_core_options(parser) + parser.suggest_on_error = True info = self._method(conan_api, parser, *args) @@ -226,7 +240,8 @@ def set_parser(self, subcommand_parser, conan_api): self._parser = subcommand_parser.add_parser(self._name, conan_api=conan_api, help=self._doc) self._parser.description = self._doc self._init_formatters(self._parser) - self._init_log_levels(self._parser) + self._init_core_options(self._parser) + self._parser.suggest_on_error = True def conan_command(group=None, formatters=None): diff --git a/conan/cli/commands/audit.py b/conan/cli/commands/audit.py index 9bbe1fff558..c5f08802d99 100644 --- a/conan/cli/commands/audit.py +++ b/conan/cli/commands/audit.py @@ -60,6 +60,9 @@ def audit_scan(conan_api: ConanAPI, parser, subparser, *args) -> dict: help="Set threshold for severity level to raise an error. " "By default raises an error for any critical CVSS (9.0 or higher). " " Use 100.0 to disable it.") + subparser.add_argument("--context", help="Context to scan, by default both contexts are scanned " + "if not specified", + choices=["host", "build"], default=None) _add_provider_arg(subparser) args = parser.parse_args(*args) @@ -97,7 +100,7 @@ def audit_scan(conan_api: ConanAPI, parser, subparser, *args) -> dict: provider = conan_api.audit.get_provider(args.provider or CONAN_CENTER_AUDIT_PROVIDER_NAME) - scan_result = conan_api.audit.scan(deps_graph, provider) + scan_result = conan_api.audit.scan(deps_graph, provider, args.context) _parse_error_threshold(scan_result, args.severity_level) return scan_result @@ -111,9 +114,9 @@ def audit_list(conan_api: ConanAPI, parser, subparser, *args): """ input_group = subparser.add_mutually_exclusive_group(required=True) input_group.add_argument("reference", help="Reference to list vulnerabilities for", nargs="?") - input_group.add_argument("-l", "--list", help="pkglist file to list vulnerabilities for") - input_group.add_argument("-s", "--sbom", help="sbom file to list vulnerabilities for") - input_group.add_argument("-lock", "--lockfile", help="lockfile file to list vulnerabilities for") + input_group.add_argument("-l", "--list", help="Package list file to list vulnerabilities for") + input_group.add_argument("-s", "--sbom", help="SBOM file to list vulnerabilities for") + input_group.add_argument("-lock", "--lockfile", help="Path to the lockfile to check for vulnerabilities") subparser.add_argument("-r", "--remote", help="Remote to use for listing") _add_provider_arg(subparser) args = parser.parse_args(*args) diff --git a/conan/cli/commands/build.py b/conan/cli/commands/build.py index 4720e6259a1..d1a79cf9aaa 100644 --- a/conan/cli/commands/build.py +++ b/conan/cli/commands/build.py @@ -17,7 +17,9 @@ def build(conan_api, parser, *args): parser.add_argument("path", help='Path to a python-based recipe file or a folder ' 'containing a conanfile.py recipe. conanfile.txt ' - 'cannot be used with conan build.') + 'cannot be used with conan build. ' + 'Defaults to current directory', + default=".", nargs='?') add_reference_args(parser) parser.add_argument("-g", "--generator", action="append", help='Generators to use') parser.add_argument("-of", "--output-folder", diff --git a/conan/cli/commands/cache.py b/conan/cli/commands/cache.py index 975ff02b3c3..36cbda6fde1 100644 --- a/conan/cli/commands/cache.py +++ b/conan/cli/commands/cache.py @@ -2,7 +2,7 @@ from conan.api.conan_api import ConanAPI from conan.api.model import ListPattern, MultiPackagesList -from conan.api.output import cli_out_write, ConanOutput +from conan.api.output import cli_out_write from conan.cli import make_abs_path from conan.cli.command import conan_command, conan_subcommand, OnceArgument from conan.cli.commands.list import print_list_text, print_list_json @@ -120,7 +120,14 @@ def cache_clean(conan_api: ConanAPI, parser, subparser, *args): conan_api.cache.clean(package_list) -@conan_subcommand() +def print_list_check_integrity_json(data): + results = data["results"] + myjson = json.dumps(results, indent=4) + cli_out_write(myjson) + + +@conan_subcommand(formatters={"text": lambda _: (), + "json": print_list_check_integrity_json}) def cache_check_integrity(conan_api: ConanAPI, parser, subparser, *args): """ Check the integrity of the local cache for the given references @@ -146,8 +153,10 @@ def cache_check_integrity(conan_api: ConanAPI, parser, subparser, *args): else: ref_pattern = ListPattern(args.pattern, rrev="*", package_id="*", prev="*") package_list = conan_api.list.select(ref_pattern, package_query=args.package_query) - conan_api.cache.check_integrity(package_list) - ConanOutput().success("Integrity check: ok") + + corrupted_artifacts = conan_api.cache.check_integrity(package_list, return_pkg_list=True) + return {"results": {"Local Cache": corrupted_artifacts.serialize()}, + "conan_error": "There are corrupted artifacts, check the error logs" if corrupted_artifacts else ""} @conan_subcommand(formatters={"text": print_list_text, @@ -200,6 +209,6 @@ def cache_backup_upload(conan_api: ConanAPI, parser, subparser, *args): """ Upload all the source backups present in the cache """ - args = parser.parse_args(*args) + parser.parse_args(*args) files = conan_api.cache.get_backup_sources() conan_api.upload.upload_backup_sources(files) diff --git a/conan/cli/commands/config.py b/conan/cli/commands/config.py index c2691e89c01..0bcd1719700 100644 --- a/conan/cli/commands/config.py +++ b/conan/cli/commands/config.py @@ -1,5 +1,8 @@ +import os + from conan.api.model import Remote from conan.api.output import cli_out_write +from conan.cli import make_abs_path from conan.cli.command import conan_command, conan_subcommand, OnceArgument from conan.cli.formatters import default_json_formatter from conan.errors import ConanException @@ -59,8 +62,11 @@ def get_bool_from_text(value): # TODO: deprecate this def config_install_pkg(conan_api, parser, subparser, *args): """ (Experimental) Install the configuration (remotes, profiles, conf), from a Conan package + or from a conanconfig.yml file """ - subparser.add_argument("item", help="Conan require") + subparser.add_argument("reference", nargs="?", + help="Package reference 'pkg/version' to install configuration from " + "or path to 'conanconfig.yml' file") subparser.add_argument("-l", "--lockfile", action=OnceArgument, help="Path to a lockfile. Use --lockfile=\"\" to avoid automatic use of " "existing 'conan.lock' file") @@ -78,6 +84,13 @@ def config_install_pkg(conan_api, parser, subparser, *args): subparser.add_argument("-o", "--options", action="append", help="Options to install config") args = parser.parse_args(*args) + path = make_abs_path(args.reference or ".") + if os.path.isdir(path): + path = os.path.join(path, "conanconfig.yml") + path = path if os.path.exists(path) else None + if path is None and args.reference is None: + raise ConanException("Must provide a package reference or a path to a conanconfig.yml file") + lockfile = conan_api.lockfile.get_lockfile(lockfile=args.lockfile, partial=args.lockfile_partial) @@ -87,10 +100,16 @@ def config_install_pkg(conan_api, parser, subparser, *args): default_profile = None profiles = [default_profile] if default_profile else [] profile = conan_api.profiles.get_profile(profiles, args.settings, args.options) - remotes = [Remote("_tmp_conan_config", url=args.url)] if args.url else None - config_pref = conan_api.config.install_pkg(args.item, lockfile=lockfile, force=args.force, - remotes=remotes, profile=profile) - lockfile = conan_api.lockfile.add_lockfile(lockfile, config_requires=[config_pref.ref]) + remotes = [Remote("config_install_url", url=args.url)] if args.url else None + + if path: + conanconfig = path + refs = conan_api.config.install_conanconfig(conanconfig, lockfile=lockfile, force=args.force, + remotes=remotes, profile=profile) + else: + refs = conan_api.config.install_package(args.reference, lockfile=lockfile, force=args.force, + remotes=remotes, profile=profile) + lockfile = conan_api.lockfile.add_lockfile(lockfile, config_requires=refs) conan_api.lockfile.save_lockfile(lockfile, args.lockfile_out) diff --git a/conan/cli/commands/create.py b/conan/cli/commands/create.py index f4ea4ff510f..25d5c3930af 100644 --- a/conan/cli/commands/create.py +++ b/conan/cli/commands/create.py @@ -68,6 +68,7 @@ def create(conan_api, parser, *args): if args.build is not None and args.build_test is None: args.build_test = args.build + install_error = None if is_python_require: deps_graph = conan_api.graph.load_graph_requires([], [], profile_host=profile_host, @@ -98,7 +99,9 @@ def create(conan_api, parser, *args): update=args.update, lockfile=lockfile) print_graph_packages(deps_graph) - conan_api.install.install_binaries(deps_graph=deps_graph, remotes=remotes) + install_error = conan_api.install.install_binaries(deps_graph=deps_graph, remotes=remotes, + return_install_error=True) + # We update the lockfile, so it will be updated for later ``test_package`` lockfile = conan_api.lockfile.update_lockfile(lockfile, deps_graph, args.lockfile_packages, clean=args.lockfile_clean) @@ -111,7 +114,7 @@ def create(conan_api, parser, *args): and deps_graph.root.edges[0].dst.binary != "Build": test_conanfile_path = None # disable it - if test_conanfile_path: + if test_conanfile_path and not install_error: # TODO: We need arguments for: # - decide update policy "--test_package_update" # If it is a string, it will be injected always, if it is a RecipeReference, then it will @@ -126,7 +129,8 @@ def create(conan_api, parser, *args): conan_api.lockfile.save_lockfile(lockfile, args.lockfile_out, cwd) return {"graph": deps_graph, - "conan_api": conan_api} + "conan_api": conan_api, + "conan_error": install_error} def _check_tested_reference_matches(deps_graph, tested_ref, out): diff --git a/conan/cli/commands/download.py b/conan/cli/commands/download.py index a7c72a0ccbd..386d9f25d37 100644 --- a/conan/cli/commands/download.py +++ b/conan/cli/commands/download.py @@ -56,7 +56,7 @@ def download(conan_api: ConanAPI, parser, *args): ref_pattern = ListPattern(args.pattern, package_id="*", only_recipe=args.only_recipe) package_list = conan_api.list.select(ref_pattern, args.package_query, remote) - if package_list.recipes: + if package_list: conan_api.download.download_full(package_list, remote, args.metadata) else: ConanOutput().warning(f"No packages were downloaded because the package list is empty.") diff --git a/conan/cli/commands/editable.py b/conan/cli/commands/editable.py index 23679fc65a8..20319823a66 100644 --- a/conan/cli/commands/editable.py +++ b/conan/cli/commands/editable.py @@ -19,7 +19,8 @@ def editable_add(conan_api, parser, subparser, *args): Define the given location as the package , so when this package is required, it is used from this location instead of the cache. """ - subparser.add_argument('path', help='Path to the package folder in the user workspace') + subparser.add_argument('path', help='Path to the package folder in the user workspace', + default=".", nargs='?') add_reference_args(subparser) subparser.add_argument("-of", "--output-folder", help='The root output folder for generated and build files') @@ -43,12 +44,16 @@ def editable_remove(conan_api, parser, subparser, *args): Remove the "editable" mode for this reference. """ subparser.add_argument("path", nargs="?", - help="Path to a folder containing a recipe (conanfile.py " - "or conanfile.txt) or to a recipe file. e.g., " - "./my_project/conanfile.txt.") + help="Path to a folder containing a recipe conanfile.py " + "or to a recipe file. e.g., " + "./my_project/conanfile.py.", + default=None) subparser.add_argument("-r", "--refs", action="append", help='Directly provide reference patterns') args = parser.parse_args(*args) + if not args.refs and args.path is None: + args.path = "." + # TODO: Fix this API to use get_conanfile_path editables = conan_api.local.editable_remove(args.path, args.refs) out = ConanOutput() if editables: diff --git a/conan/cli/commands/export.py b/conan/cli/commands/export.py index c052fe1cd4a..cc6c3e53d00 100644 --- a/conan/cli/commands/export.py +++ b/conan/cli/commands/export.py @@ -8,7 +8,9 @@ def common_args_export(parser): - parser.add_argument("path", help="Path to a folder containing a recipe (conanfile.py)") + parser.add_argument("path", help="Path to a folder containing a recipe (conanfile.py). " + "Defaults to current directory", + default=".", nargs="?") add_reference_args(parser) @@ -58,7 +60,7 @@ def export(conan_api, parser, *args): conan_api.lockfile.save_lockfile(lockfile, args.lockfile_out, cwd) exported_list = PackagesList() - exported_list.add_refs([ref]) + exported_list.add_ref(ref) pkglist = MultiPackagesList() pkglist.add("Local Cache", exported_list) diff --git a/conan/cli/commands/export_pkg.py b/conan/cli/commands/export_pkg.py index 06b7936715b..f17b984cfae 100644 --- a/conan/cli/commands/export_pkg.py +++ b/conan/cli/commands/export_pkg.py @@ -6,7 +6,6 @@ from conan.cli.command import conan_command, OnceArgument from conan.cli.commands.create import _get_test_conanfile_path from conan.cli.formatters.graph import format_graph_json -from conan.cli.printers.graph import print_graph_basic from conan.errors import ConanException @@ -15,7 +14,9 @@ def export_pkg(conan_api, parser, *args): """ Create a package directly from pre-compiled binaries. """ - parser.add_argument("path", help="Path to a folder containing a recipe (conanfile.py)") + parser.add_argument("path", help="Path to a folder containing a recipe (conanfile.py). " + "Defaults to current directory", + default=".", nargs="?") parser.add_argument("-of", "--output-folder", help='The root output folder for generated and build files') parser.add_argument("--build-require", action='store_true', default=False, @@ -44,51 +45,33 @@ def export_pkg(conan_api, parser, *args): overrides=overrides) profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args) remotes = conan_api.remotes.list(args.remote) if not args.no_remote else [] + output_folder = make_abs_path(args.output_folder, cwd) if args.output_folder else None + # UX error check, for python-requires use "conan export" or "conan create" conanfile = conan_api.local.inspect(path, remotes, lockfile, name=args.name, version=args.version, user=args.user, channel=args.channel) # The package_type is not fully processed at export if conanfile.package_type == "python-require": raise ConanException("export-pkg can only be used for binaries, not for 'python-require'") + + # First, ensure recipe is exported ref, conanfile = conan_api.export.export(path=path, name=args.name, version=args.version, user=args.user, channel=args.channel, lockfile=lockfile, remotes=remotes) lockfile = conan_api.lockfile.update_lockfile_export(lockfile, conanfile, ref, args.build_require) - # TODO: Maybe we want to be able to export-pkg it as --build-require - deps_graph = conan_api.graph.load_graph_consumer(path, - ref.name, str(ref.version), ref.user, ref.channel, - profile_host=profile_host, - profile_build=profile_build, - lockfile=lockfile, remotes=remotes, update=None, - is_build_require=args.build_require) - - print_graph_basic(deps_graph) - deps_graph.report_graph_error() - conan_api.graph.analyze_binaries(deps_graph, build_mode=[ref.name], lockfile=lockfile, - remotes=remotes) - deps_graph.report_graph_error() - - root_node = deps_graph.root - root_node.ref = ref - - if not args.skip_binaries: - # unless the user explicitly opts-out with --skip-binaries, it is necessary to install - # binaries, in case there are build_requires necessary to export, like tool-requires=cmake - # and package() method doing ``cmake.install()`` - # for most cases, deps would be in local cache already because of a previous "conan install" - # but if it is not the case, the binaries from remotes will be downloaded - conan_api.install.install_binaries(deps_graph=deps_graph, remotes=remotes) - source_folder = os.path.dirname(path) - output_folder = make_abs_path(args.output_folder, cwd) if args.output_folder else None - conan_api.install.install_consumer(deps_graph=deps_graph, source_folder=source_folder, - output_folder=output_folder) + # Compute the dependency graph to prepare the exporting of the package biary + graph = conan_api.export.export_pkg_graph(path=path, ref=ref, profile_host=profile_host, + profile_build=profile_build, lockfile=lockfile, + remotes=remotes, is_build_require=args.build_require, + skip_binaries=args.skip_binaries, + output_folder=output_folder) + # Now export the final binary ConanOutput().title("Exporting recipe and package to the cache") - conan_api.export.export_pkg(deps_graph, source_folder, output_folder) - - lockfile = conan_api.lockfile.update_lockfile(lockfile, deps_graph, args.lockfile_packages, + conan_api.export.export_pkg(graph, output_folder) + lockfile = conan_api.lockfile.update_lockfile(lockfile, graph, args.lockfile_packages, clean=args.lockfile_clean) test_package_folder = getattr(conanfile, "test_package_folder", None) \ @@ -102,5 +85,5 @@ def export_pkg(conan_api, parser, *args): remotes=remotes, lockfile=lockfile, update=None, build_modes=None) conan_api.lockfile.save_lockfile(lockfile, args.lockfile_out, cwd) - return {"graph": deps_graph, + return {"graph": graph, "conan_api": conan_api} diff --git a/conan/cli/commands/graph.py b/conan/cli/commands/graph.py index 290bac5bda8..0e94e186c33 100644 --- a/conan/cli/commands/graph.py +++ b/conan/cli/commands/graph.py @@ -72,11 +72,7 @@ def graph_build_order(conan_api, parser, subparser, *args): help='Reduce the build order, output only those to build. Use this ' 'only if the result will not be merged later with other build-order') args = parser.parse_args(*args) - - # parameter validation - if args.requires and (args.name or args.version or args.user or args.channel): - raise ConanException("Can't use --name, --version, --user or --channel arguments with " - "--requires") + validate_common_graph_args(args) if args.order_by is None: ConanOutput().warning("Please specify --order-by argument", warn_tag="deprecated") @@ -173,7 +169,6 @@ def graph_info(conan_api, parser, subparser, *args): subparser.add_argument("--build-require", action='store_true', default=False, help='Whether the provided reference is a build-require') args = parser.parse_args(*args) - # parameter validation validate_common_graph_args(args) if args.format in ("html", "dot") and args.filter: diff --git a/conan/cli/commands/inspect.py b/conan/cli/commands/inspect.py index a7268ef0d30..e129d132727 100644 --- a/conan/cli/commands/inspect.py +++ b/conan/cli/commands/inspect.py @@ -23,7 +23,9 @@ def inspect(conan_api, parser, *args): """ Inspect a conanfile.py to return its public fields. """ - parser.add_argument("path", help="Path to a folder containing a recipe (conanfile.py)") + parser.add_argument("path", help="Path to a folder containing a recipe (conanfile.py). " + "Defaults to current directory", + default=".", nargs="?") group = parser.add_mutually_exclusive_group() group.add_argument("-r", "--remote", default=None, action="append", help="Remote names. Accepts wildcards ('*' means all the remotes available)") diff --git a/conan/cli/commands/install.py b/conan/cli/commands/install.py index faff8ed2a96..61643aeeab7 100644 --- a/conan/cli/commands/install.py +++ b/conan/cli/commands/install.py @@ -43,8 +43,29 @@ def install(conan_api, parser, *args): help="Generation strategy for virtual environment files for the root") args = parser.parse_args(*args) validate_common_graph_args(args) - # basic paths cwd = os.getcwd() + + deps_graph, lockfile, install_error = _run_install_command(conan_api, args, cwd) + + # Update lockfile if necessary + lockfile = conan_api.lockfile.update_lockfile(lockfile, deps_graph, args.lockfile_packages, + clean=args.lockfile_clean) + conan_api.lockfile.save_lockfile(lockfile, args.lockfile_out, cwd) + return {"graph": deps_graph, + "conan_api": conan_api, + "conan_error": install_error} + + +def _run_install_command(conan_api, args, cwd, return_install_error=True): + """ + This method should not be imported as-is, it is internal to the installation process and + its signature might change without warning. + + Users are however free to copy its code and adapt it to their needs, as an example of + using the Conan API to perform an installation + """ + # basic paths + path = conan_api.local.get_conanfile_path(args.path, cwd, py=None) if args.path else None source_folder = os.path.dirname(path) if args.path else cwd output_folder = make_abs_path(args.output_folder, cwd) if args.output_folder else None @@ -72,17 +93,15 @@ def install(conan_api, parser, *args): print_graph_packages(deps_graph) # Installation of binaries and consumer generators - conan_api.install.install_binaries(deps_graph=deps_graph, remotes=remotes) - ConanOutput().title("Finalizing install (deploy, generators)") - conan_api.install.install_consumer(deps_graph, args.generator, source_folder, output_folder, - deploy=args.deployer, deploy_package=args.deployer_package, - deploy_folder=args.deployer_folder, - envs_generation=args.envs_generation) - ConanOutput().success("Install finished successfully") + install_error = conan_api.install.install_binaries(deps_graph=deps_graph, remotes=remotes, + return_install_error=return_install_error) + if not install_error: + ConanOutput().title("Finalizing install (deploy, generators)") + conan_api.install.install_consumer(deps_graph, args.generator, source_folder, output_folder, + deploy=getattr(args, "deployer", None), + deploy_package=getattr(args, "deployer_package", None), + deploy_folder=getattr(args, "deployer_folder", None), + envs_generation=getattr(args, "envs_generation", None)) + ConanOutput().success("Install finished successfully") - # Update lockfile if necessary - lockfile = conan_api.lockfile.update_lockfile(lockfile, deps_graph, args.lockfile_packages, - clean=args.lockfile_clean) - conan_api.lockfile.save_lockfile(lockfile, args.lockfile_out, cwd) - return {"graph": deps_graph, - "conan_api": conan_api} + return deps_graph, lockfile, install_error diff --git a/conan/cli/commands/list.py b/conan/cli/commands/list.py index 6b5100b2ae1..3f8b6f7da70 100644 --- a/conan/cli/commands/list.py +++ b/conan/cli/commands/list.py @@ -57,7 +57,7 @@ def print_serial(item, indent=None, color_index=None): def print_list_text(results): """ Do a little format modification to serialized - list bundle, so it looks prettier on text output + package list, so it looks prettier on text output """ info = results["results"] @@ -264,6 +264,13 @@ def list(conan_api: ConanAPI, parser, *args): args.filter_settings or args.filter_options): raise ConanException("--package-query and --filter-xxx can only be done for binaries, " "a 'pkgname/version:*' pattern is necessary") + if args.lru: + if not ref_pattern.rrev and not ref_pattern.package_id: # If package_id => #latest + raise ConanException("'--lru' must be used with recipe revision pattern, " + "use 'pkg/version#*' argument") + if ref_pattern.package_id and not ref_pattern.prev: + raise ConanException("'--lru' must be used with package revision pattern, " + "use 'pkg/version:*#*' argument") # If neither remote nor cache are defined, show results only from cache pkglist = MultiPackagesList() profile = conan_api.profiles.get_profile(args.filter_profile or [], diff --git a/conan/cli/commands/lock.py b/conan/cli/commands/lock.py index 76ec4cc78a6..83e3fc7bba1 100644 --- a/conan/cli/commands/lock.py +++ b/conan/cli/commands/lock.py @@ -1,4 +1,3 @@ -from collections import defaultdict import os from conan.api.output import ConanOutput @@ -27,7 +26,6 @@ def lock_create(conan_api, parser, subparser, *args): subparser.add_argument("--build-require", action='store_true', default=False, help='Whether the provided reference is a build-require') args = parser.parse_args(*args) - # parameter validation validate_common_graph_args(args) @@ -175,29 +173,28 @@ def lock_update(conan_api, parser, subparser, *args): 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. + (Experimental) Upgrade requires, build-requires or python-requires from an existing lockfile + given a conanfile or a reference. """ - 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('--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('--build-require', action='store_true', default=False, + help='Whether the provided reference is a build-require') 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]): + if not any([args.update_requires, args.update_build_requires, args.update_python_requires]): raise ConanException("At least one of --update-requires, --update-build-requires, " - "--update-python-requires or --update-config-requires should be specified") + "--update-python-requires should be specified") cwd = os.getcwd() path = conan_api.local.get_conanfile_path(args.path, cwd, py=None) if args.path else None @@ -205,14 +202,15 @@ def lock_upgrade(conan_api, parser, subparser, *args): 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) + if lockfile is None: + raise ConanException("No lockfile specified and default conan.lock not found") profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args) # Remove the lockfile entries that will be updated lockfile = conan_api.lockfile.remove_lockfile(lockfile, requires=args.update_requires, python_requires=args.update_python_requires, - build_requires=args.update_build_requires, - config_requires=args.update_config_requires) + build_requires=args.update_build_requires) # Resolve new graph if path: graph = conan_api.graph.load_graph_consumer(path, args.name, args.version, @@ -230,6 +228,44 @@ def lock_upgrade(conan_api, parser, subparser, *args): lockfile=lockfile) print_graph_packages(graph) - lockfile = conan_api.lockfile.update_lockfile(lockfile, graph, args.lockfile_packages, - clean=args.lockfile_clean) + lockfile = conan_api.lockfile.update_lockfile(lockfile, graph, clean=args.lockfile_clean) + conan_api.lockfile.save_lockfile(lockfile, args.lockfile_out or "conan.lock") + + +@conan_subcommand() +def lock_upgrade_config(conan_api, parser, subparser, *args): + """ + (Experimental) Upgrade config requires in a lockfile + """ + common_graph_args(subparser) + subparser.add_argument('--update-config-requires', action="append", + help='Update config-requires from lockfile') + args = parser.parse_args(*args) + validate_common_graph_args(args) + + if not args.update_config_requires: + raise ConanException("At least one --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 [] + lockfile = conan_api.lockfile.get_lockfile(lockfile=args.lockfile, conanfile_path=path, + cwd=cwd, partial=True) + if lockfile is None: + raise ConanException("No lockfile specified and default conan.lock not found") + profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args) + + # Remove the lockfile entries that will be updated + lockfile = conan_api.lockfile.remove_lockfile(lockfile, + config_requires=args.update_config_requires) + + if args.path: + path = make_abs_path(args.path) + reqs, remotes = conan_api.config.load_conanconfig(path, remotes) + else: + reqs = [RecipeReference.loads(r) for r in args.requires] + + pkgs = conan_api.config.fetch_packages(reqs, lockfile, remotes, profile_host) + refs = [p.ref for p in pkgs] + lockfile = conan_api.lockfile.add_lockfile(lockfile, config_requires=refs) conan_api.lockfile.save_lockfile(lockfile, args.lockfile_out or "conan.lock") diff --git a/conan/cli/commands/remote.py b/conan/cli/commands/remote.py index 671ecf75bd3..43cd7d2d813 100644 --- a/conan/cli/commands/remote.py +++ b/conan/cli/commands/remote.py @@ -17,7 +17,8 @@ def _print_remotes_json(remotes): "url": r.url, "verify_ssl": r.verify_ssl, "enabled": not r.disabled, - "allowed_packages": r.allowed_packages,} + "allowed_packages": r.allowed_packages, + "recipes_only": r.recipes_only} for r in remotes] cli_out_write(json.dumps(info, indent=4)) @@ -83,6 +84,9 @@ def remote_add(conan_api, parser, subparser, *args): "this remote") subparser.add_argument("-t", "--type", choices=[LOCAL_RECIPES_INDEX], help="Define the remote type") + subparser.add_argument("--recipes-only", action="store_true", default=False, + help="Disallow binary downloads from this remote, only recipes " + "will be downloaded") subparser.set_defaults(secure=True) args = parser.parse_args(*args) @@ -91,7 +95,8 @@ def remote_add(conan_api, parser, subparser, *args): remote_type = args.type or (LOCAL_RECIPES_INDEX if os.path.isdir(url_folder) else None) url = url_folder if remote_type == LOCAL_RECIPES_INDEX else args.url r = Remote(args.name, url, args.secure, disabled=False, remote_type=remote_type, - allowed_packages=args.allowed_packages) + allowed_packages=args.allowed_packages, + recipes_only=args.recipes_only) conan_api.remotes.add(r, force=args.force, index=args.index) @@ -122,12 +127,22 @@ def remote_update(conan_api, parser, subparser, *args): subparser.add_argument("--index", action=OnceArgument, type=int, help="Insert the remote at a specific position in the remote list") subparser.add_argument("-ap", "--allowed-packages", action="append", default=None, - help="Add recipe reference pattern to the list of allowed packages for this remote") + help="Add recipe reference pattern to the list of allowed packages " + "for this remote") + subparser.add_argument("--recipes-only", default=None, const="True", nargs="?", + choices=["True", "False"], + help="Disallow binary downloads from this remote, only recipes will " + "be downloaded") + subparser.set_defaults(secure=None) args = parser.parse_args(*args) - if args.url is None and args.secure is None and args.index is None and args.allowed_packages is None: + if (args.url is None and args.secure is None and args.index is None and + args.allowed_packages is None and args.recipes_only is None): subparser.error("Please add at least one argument to update") - conan_api.remotes.update(args.remote, args.url, args.secure, index=args.index, allowed_packages=args.allowed_packages) + args.recipes_only = None if args.recipes_only is None else args.recipes_only == "True" + conan_api.remotes.update(args.remote, args.url, args.secure, index=args.index, + allowed_packages=args.allowed_packages, + recipes_only=args.recipes_only) @conan_subcommand() @@ -263,7 +278,8 @@ def _print_auth_json(results): @conan_subcommand(formatters={"text": _print_auth, "json": _print_auth_json}) def remote_auth(conan_api, parser, subparser, *args): """ - Authenticate in the defined remotes. Use CONAN_LOGIN_USERNAME* and CONAN_PASSWORD* variables if available. + Authenticate in the defined remotes. Use CONAN_LOGIN_USERNAME* and CONAN_PASSWORD* variables + if available. Ask for username and password interactively in case (re-)authentication is required and there are no CONAN_LOGIN* and CONAN_PASSWORD* variables available which could be used. Usually you'd use this method over conan remote login for scripting which needs to run in CI diff --git a/conan/cli/commands/remove.py b/conan/cli/commands/remove.py index 5e488b9b030..f57e180d337 100644 --- a/conan/cli/commands/remove.py +++ b/conan/cli/commands/remove.py @@ -1,5 +1,5 @@ from conan.api.conan_api import ConanAPI -from conan.api.model import ListPattern, MultiPackagesList +from conan.api.model import ListPattern, MultiPackagesList, PackagesList from conan.api.output import cli_out_write, ConanOutput from conan.api.input import UserInput from conan.cli import make_abs_path @@ -10,7 +10,7 @@ def summary_remove_list(results): """ Do a little format modification to serialized - list bundle, so it looks prettier on text output + package list so it looks prettier on text output """ cli_out_write("Remove summary:") info = results["results"] @@ -81,7 +81,7 @@ def confirmation(message): listfile = make_abs_path(args.list) multi_package_list = MultiPackagesList.load(listfile) package_list = multi_package_list[cache_name] - refs_to_remove = package_list.refs() + refs_to_remove = list(package_list.items()) if not refs_to_remove: # the package list might contain only refs, no revs ConanOutput().warning("Nothing to remove, package list do not contain recipe revisions") else: @@ -92,36 +92,30 @@ def confirmation(message): multi_package_list = MultiPackagesList() multi_package_list.add(cache_name, package_list) - # TODO: This iteration and removal of not-confirmed is ugly and complicated, improve it - for ref, ref_bundle in package_list.refs().items(): - ref_dict = package_list.recipes[str(ref)]["revisions"] - packages = ref_bundle.get("packages") - if packages is None: + result = PackagesList() + for ref, packages in package_list.items(): + ref_dict = package_list.recipe_dict(ref).copy() + packages_dict = ref_dict.pop("packages", None) + if packages_dict is None: if confirmation(f"Remove the recipe and all the packages of '{ref.repr_notime()}'?"): if not args.dry_run: conan_api.remove.recipe(ref, remote=remote) - else: - ref_dict.pop(ref.revision) - if not ref_dict: - package_list.recipes.pop(str(ref)) - continue - prefs = package_list.prefs(ref, ref_bundle) - if not prefs: - ConanOutput().info(f"No binaries to remove for '{ref.repr_notime()}'") - ref_dict.pop(ref.revision) - if not ref_dict: - package_list.recipes.pop(str(ref)) - continue - - for pref, _ in prefs.items(): - if confirmation(f"Remove the package '{pref.repr_notime()}'?"): - if not args.dry_run: - conan_api.remove.package(pref, remote=remote) - else: - pref_dict = packages[pref.package_id]["revisions"] - pref_dict.pop(pref.revision) - if not pref_dict: - packages.pop(pref.package_id) + result.add_ref(ref) + result.recipe_dict(ref).update(ref_dict) # it doesn't contain "packages" + else: + if not packages: # weird, there is inner package-ids but without prevs + ConanOutput().info(f"No binaries to remove for '{ref.repr_notime()}'") + continue + for pref, pkg_id_info in packages.items(): + if confirmation(f"Remove the package '{pref.repr_notime()}'?"): + if not args.dry_run: + conan_api.remove.package(pref, remote=remote) + result.add_ref(ref) + result.recipe_dict(ref).update(ref_dict) # it doesn't contain "packages" + result.add_pref(pref, pkg_id_info) + pkg_dict = package_list.package_dict(pref) + result.package_dict(pref).update(pkg_dict) + multi_package_list.add(cache_name, result) return { "results": multi_package_list.serialize(), diff --git a/conan/cli/commands/report.py b/conan/cli/commands/report.py index 84b8fc55214..1fd1958f3a7 100644 --- a/conan/cli/commands/report.py +++ b/conan/cli/commands/report.py @@ -5,7 +5,6 @@ from conan.cli.command import conan_command, conan_subcommand - @conan_command(group="Security") def report(conan_api: ConanAPI, parser, *args): """ @@ -40,7 +39,7 @@ def report_diff(conan_api, parser, subparser, *args): subparser.add_argument("-nr", "--new-reference", help=ref_help.format(type="New"), required=True) subparser.add_argument("-r", "--remote", action="append", default=None, - help='Look in the specified remote or remotes server') + help='Look in the specified remote or remotes server') args = parser.parse_args(*args) diff --git a/conan/cli/commands/run.py b/conan/cli/commands/run.py new file mode 100644 index 00000000000..70007250d6f --- /dev/null +++ b/conan/cli/commands/run.py @@ -0,0 +1,55 @@ +import os +import tempfile + +from conan.api.output import ConanOutput, LEVEL_STATUS, Color, LEVEL_ERROR, LEVEL_QUIET +from conan.cli.args import common_graph_args, validate_common_graph_args +from conan.cli.command import conan_command +from conan.cli.commands.install import _run_install_command +from conan.errors import ConanException + + +@conan_command(group="Consumer") +def run(conan_api, parser, *args): + """ + (Experimental) Run a command given a set of requirements from a recipe or from command line. + """ + common_graph_args(parser) + parser.add_argument("command", help="Command to run") + parser.add_argument("--context", help="Context to use, by default both contexts are activated " + "if not specified", + choices=["host", "build"], default=None) + parser.add_argument("--build-require", action='store_true', default=False, + help='Whether the provided path is a build-require') + args = parser.parse_args(*args) + validate_common_graph_args(args) + cwd = os.getcwd() + + ConanOutput().info("Installing and building dependencies, this might take a while...", + fg=Color.BRIGHT_MAGENTA) + previous_log_level = ConanOutput.get_output_level() + if previous_log_level == LEVEL_STATUS: + ConanOutput.set_output_level(LEVEL_QUIET) + + with tempfile.TemporaryDirectory("conanrun") as tmpdir: + # Default values for install + setattr(args, "output_folder", tmpdir) + setattr(args, "generator", []) + try: + deps_graph, lockfile, _ = _run_install_command(conan_api, args, cwd, + return_install_error=False) + except ConanException as e: + ConanOutput.set_output_level(previous_log_level) + ConanOutput().error("Error installing the dependencies. To debug this, you can either:\n" + " - Re-run the command with increased verbosity (-v, -vv)\n" + " - Run 'conan install' first to ensure dependencies are installed, " + "or to see errors during installation\n") + raise e + + context_env_map = { + "build": "conanbuild", + "host": "conanrun", + } + envfiles = list(context_env_map.values()) if args.context is None \ + else [context_env_map.get(args.context)] + ConanOutput.set_output_level(LEVEL_ERROR) + deps_graph.root.conanfile.run(args.command, cwd=cwd, env=envfiles) diff --git a/conan/cli/commands/search.py b/conan/cli/commands/search.py index 7a34033502c..08ca30c1660 100644 --- a/conan/cli/commands/search.py +++ b/conan/cli/commands/search.py @@ -7,7 +7,6 @@ from conan.errors import ConanException -# FIXME: "conan search" == "conan list (*) -r="*"" --> implement @conan_alias_command?? @conan_command(group="Consumer", formatters={"text": print_list_text, "json": print_list_json}) def search(conan_api: ConanAPI, parser, *args): @@ -31,11 +30,11 @@ def search(conan_api: ConanAPI, parser, *args): results = OrderedDict() for remote in remotes: try: - list_bundle = conan_api.list.select(ref_pattern, package_query=None, remote=remote) + pkglist = conan_api.list.select(ref_pattern, package_query=None, remote=remote) except Exception as e: results[remote.name] = {"error": str(e)} else: - results[remote.name] = list_bundle.serialize() + results[remote.name] = pkglist.serialize() return { "results": results } diff --git a/conan/cli/commands/source.py b/conan/cli/commands/source.py index 213d4ff08ce..4a694964b74 100644 --- a/conan/cli/commands/source.py +++ b/conan/cli/commands/source.py @@ -9,7 +9,9 @@ def source(conan_api, parser, *args): """ Call the source() method. """ - parser.add_argument("path", help="Path to a folder containing a conanfile.py") + parser.add_argument("path", help="Path to a folder containing a conanfile.py. " + "Defaults to current directory", + default=".", nargs="?") add_reference_args(parser) args = parser.parse_args(*args) diff --git a/conan/cli/commands/test.py b/conan/cli/commands/test.py index b030a8eb645..ca8d461832b 100644 --- a/conan/cli/commands/test.py +++ b/conan/cli/commands/test.py @@ -16,7 +16,9 @@ def test(conan_api, parser, *args): Test a package from a test_package folder. """ parser.add_argument("path", action=OnceArgument, - help="Path to a test_package folder containing a conanfile.py") + help="Path to a test_package folder containing a conanfile.py. " + "Defaults to a 'test_package' folder in the current directory", + default="test_package", nargs='?') parser.add_argument("reference", action=OnceArgument, help='Provide a package reference to test') add_common_install_arguments(parser) diff --git a/conan/cli/commands/upload.py b/conan/cli/commands/upload.py index 0338002c9dd..8211d9bc53f 100644 --- a/conan/cli/commands/upload.py +++ b/conan/cli/commands/upload.py @@ -1,5 +1,5 @@ from conan.api.conan_api import ConanAPI -from conan.api.model import ListPattern, MultiPackagesList +from conan.api.model import ListPattern, MultiPackagesList, PackagesList from conan.api.output import ConanOutput from conan.cli import make_abs_path from conan.cli.command import conan_command, OnceArgument @@ -10,7 +10,7 @@ def summary_upload_list(results): """ Do a little format modification to serialized - list bundle, so it looks prettier on text output + package list, so it looks prettier on text output """ ConanOutput().subtitle("Upload summary") info = results["results"] @@ -97,10 +97,10 @@ def upload(conan_api: ConanAPI, parser, *args): ref_pattern = ListPattern(args.pattern, package_id="*", only_recipe=args.only_recipe) package_list = conan_api.list.select(ref_pattern, package_query=args.package_query) - if package_list.recipes: + if package_list: # If only if search with "*" we ask for confirmation if not args.list and not args.confirm and "*" in args.pattern: - _ask_confirm_upload(conan_api, package_list) + package_list = _ask_confirm_upload(conan_api, package_list) conan_api.upload.upload_full(package_list, remote, enabled_remotes, args.check, args.force, args.metadata, args.dry_run) @@ -121,20 +121,17 @@ def upload(conan_api: ConanAPI, parser, *args): def _ask_confirm_upload(conan_api, package_list): ui = UserInput(conan_api.config.get("core:non_interactive")) - for ref, bundle in package_list.refs().items(): - msg = "Are you sure you want to upload recipe '%s'?" % ref.repr_notime() - ref_dict = package_list.recipes[str(ref)]["revisions"] - if not ui.request_boolean(msg): - ref_dict.pop(ref.revision) - # clean up empy refs - if not ref_dict: - package_list.recipes.pop(str(ref)) - else: - for pref, prev_bundle in package_list.prefs(ref, bundle).items(): - msg = "Are you sure you want to upload package '%s'?" % pref.repr_notime() - pkgs_dict = ref_dict[ref.revision]["packages"] - if not ui.request_boolean(msg): - pref_dict = pkgs_dict[pref.package_id]["revisions"] - pref_dict.pop(pref.revision) - if not pref_dict: - pkgs_dict.pop(pref.package_id) + result = PackagesList() + for ref, packages in package_list.items(): + msg = f"Are you sure you want to upload recipe '{ref.repr_notime()}'?" + if ui.request_boolean(msg): + result.add_ref(ref) + ref_dict = package_list.recipe_dict(ref).copy() + ref_dict.pop("packages", None) + result.recipe_dict(ref).update(ref_dict) + for pref, pkg_id_info in packages.items(): + msg = f"Are you sure you want to upload package '{pref.repr_notime()}'?" + if ui.request_boolean(msg): + result.add_pref(pref, pkg_id_info) + result.package_dict(pref).update(package_list.package_dict(pref)) + return result diff --git a/conan/cli/commands/workspace.py b/conan/cli/commands/workspace.py index 8f85e1fc7bb..e21593262c4 100644 --- a/conan/cli/commands/workspace.py +++ b/conan/cli/commands/workspace.py @@ -9,6 +9,7 @@ from conan.cli.args import add_reference_args, add_common_install_arguments, add_lockfile_args from conan.cli.command import conan_command, conan_subcommand from conan.cli.commands.list import print_serial +from conan.cli.formatters.graph import format_graph_json from conan.cli.printers import print_profiles from conan.cli.printers.graph import print_graph_packages, print_graph_basic from conan.errors import ConanException @@ -74,6 +75,29 @@ def workspace_add(conan_api: ConanAPI, parser, subparser, *args): ConanOutput().success("Reference '{}' added to workspace".format(ref)) +@conan_subcommand() +def workspace_complete(conan_api: ConanAPI, parser, subparser, *args): + """ + Complete the workspace, opening or adding intermediate packages to it that have + requirements to other packages in the workspace. + """ + add_common_install_arguments(subparser) + add_lockfile_args(subparser) + args = parser.parse_args(*args) + remotes = conan_api.remotes.list(args.remote) if not args.no_remote else [] + overrides = eval(args.lockfile_overrides) if args.lockfile_overrides else None + # The lockfile by default if not defined will be read from the root workspace folder + ws_folder = conan_api.workspace.folder() + lockfile = conan_api.lockfile.get_lockfile(lockfile=args.lockfile, conanfile_path=ws_folder, + cwd=None, partial=args.lockfile_partial, + overrides=overrides) + profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args) + print_profiles(profile_host, profile_build) + + ConanOutput().box("Workspace completing intermediate packages") + conan_api.workspace.complete(profile_host, profile_build, lockfile, remotes, update=args.update) + + @conan_subcommand() def workspace_remove(conan_api: ConanAPI, parser, subparser, *args): """ @@ -86,7 +110,7 @@ def workspace_remove(conan_api: ConanAPI, parser, subparser, *args): ConanOutput().info(f"Removed from workspace: {removed}") -def print_json(data): +def _print_json(data): results = data["info"] myjson = json.dumps(results, indent=4) cli_out_write(myjson) @@ -103,7 +127,7 @@ def _print_workspace_info(data): print_serial(data["info"]) -@conan_subcommand(formatters={"text": _print_workspace_info, "json": print_json}) +@conan_subcommand(formatters={"text": _print_workspace_info, "json": _print_json}) def workspace_info(conan_api: ConanAPI, parser, subparser, *args): # noqa """ Display info for current workspace @@ -166,14 +190,17 @@ def _install_build(conan_api: ConanAPI, parser, subparser, build, *args): ref = RecipeReference.loads(elem["ref"]) for package_level in elem["packages"]: for package in package_level: - if package["binary"] == "Build": # Build external to Workspace - cmd = f'install {package["build_args"]} {profile_args}' - ConanOutput().box(f"Workspace building external {ref}") - ConanOutput().info(f"Command: {cmd}\n") - conan_api.command.run(cmd) - elif package["binary"] in ("Editable", "EditableBuild"): - path = all_editables[ref]["path"] - output_folder = all_editables[ref].get("output_folder") + ws_pkg = all_editables.get(ref) + is_editable = package["binary"] in ("Editable", "EditableBuild") + if ws_pkg is None: + if is_editable or package["binary"] == "Build": # Build extern to Workspace + cmd = f'install {package["build_args"]} {profile_args}' + ConanOutput().box(f"Workspace building external {ref}") + ConanOutput().info(f"Command: {cmd}\n") + conan_api.command.run(cmd) + else: + path = ws_pkg["path"] + output_folder = ws_pkg.get("output_folder") build_arg = "--build-require" if package["context"] == "build" else "" ref_args = " ".join(f"--{k}={getattr(ref, k)}" for k in ("name", "version", "user", "channel") @@ -187,7 +214,7 @@ def _install_build(conan_api: ConanAPI, parser, subparser, build, *args): conan_api.command.run(cmd) -@conan_subcommand() +@conan_subcommand(formatters={"json": format_graph_json}) def workspace_super_install(conan_api: ConanAPI, parser, subparser, *args): """ Install the workspace as a monolith, installing only external dependencies to the workspace, @@ -197,6 +224,15 @@ def workspace_super_install(conan_api: ConanAPI, parser, subparser, *args): subparser.add_argument("-g", "--generator", action="append", help='Generators to use') subparser.add_argument("-of", "--output-folder", help='The root output folder for generated and build files') + subparser.add_argument("-d", "--deployer", action="append", + help="Deploy using the provided deployer to the output folder. " + "Built-in deployers: 'full_deploy', 'direct_deploy', " + "'runtime_deploy'") + subparser.add_argument("--deployer-folder", + help="Deployer output folder, base build folder by default if not set") + subparser.add_argument("--deployer-package", action="append", + help="Execute the deploy() method of the packages matching " + "the provided patterns") subparser.add_argument("--envs-generation", default=None, choices=["false"], help="Generation strategy for virtual environment files for the root") add_common_install_arguments(subparser) @@ -217,7 +253,7 @@ def workspace_super_install(conan_api: ConanAPI, parser, subparser, *args): requires = conan_api.workspace.select_packages(args.pkg) deps_graph = conan_api.graph.load_graph_requires(requires, [], profile_host, profile_build, lockfile, - remotes, args.build, args.update) + remotes, update=args.update) deps_graph.report_graph_error() print_graph_basic(deps_graph) @@ -225,14 +261,20 @@ def workspace_super_install(conan_api: ConanAPI, parser, subparser, *args): ws_graph = conan_api.workspace.super_build_graph(deps_graph, profile_host, profile_build) ConanOutput().subtitle("Collapsed graph") print_graph_basic(ws_graph) - conan_api.graph.analyze_binaries(ws_graph, args.build, remotes=remotes, update=args.update, lockfile=lockfile) print_graph_packages(ws_graph) conan_api.install.install_binaries(deps_graph=ws_graph, remotes=remotes) + ConanOutput().title("Finalizing install (deploy, generators)") output_folder = make_abs_path(args.output_folder) if args.output_folder else None conan_api.install.install_consumer(ws_graph, args.generator, ws_folder, output_folder, + deploy=args.deployer, deploy_package=args.deployer_package, + deploy_folder=args.deployer_folder, envs_generation=args.envs_generation) + ConanOutput().success("Install finished successfully") + + return {"graph": ws_graph, + "conan_api": conan_api} @conan_subcommand() @@ -250,8 +292,8 @@ def workspace_init(conan_api: ConanAPI, parser, subparser, *args): Clean the temporary build folders when possible """ subparser.add_argument("path", nargs="?", default=os.getcwd(), - help="Path to a folder where the workspace will be initialized. " - "If does not exist") + help="Path to a folder where the workspace will be initialized. " + "Defaults to the current directory") args = parser.parse_args(*args) conan_api.workspace.init(args.path) diff --git a/conan/cli/formatters/audit/vulnerabilities.py b/conan/cli/formatters/audit/vulnerabilities.py index 70a10c4e485..468f5d8f21d 100644 --- a/conan/cli/formatters/audit/vulnerabilities.py +++ b/conan/cli/formatters/audit/vulnerabilities.py @@ -3,7 +3,7 @@ from jinja2 import select_autoescape, Template from conan.api.output import cli_out_write, Color -from conan.errors import ConanException + severity_order = { "Critical": 4, @@ -77,14 +77,56 @@ def wrap_and_indent(txt, limit=80, indent=2): desc = node.get("description", "") desc = (desc[:240] + "...") if len(desc) > 240 else desc desc_wrapped = wrap_and_indent(desc) + isWithdrawn = node.get("withdrawn", False) + publishedAt = node.get("publishedAt") cli_out_write(f"- {name}", fg=Color.BRIGHT_WHITE, endline="") + if isWithdrawn: + cli_out_write(" [WITHDRAWN]", fg=Color.BRIGHT_CYAN, endline="") cli_out_write(f" (Severity: {sev}{score_txt})", fg=sev_color) + advisories = node.get("advisories", {}) + jfrog_advisories = [adv for adv in advisories + if adv.get("name", "").startswith("JFSA-")] + for adv in jfrog_advisories: + if adv.get("shortDescription"): + cli_out_write(f" Summary provided by JFrog Research ({adv['name']})", + fg=Color.BRIGHT_GREEN) + cli_out_write(wrap_and_indent(f"Short description: {adv['shortDescription']}", + indent=4)) + if adv.get("severity"): + cli_out_write(f" Severity: ", endline="") + cli_out_write(adv['severity'], fg=severity_colors.get(adv['severity'])) + reasons = adv.get("impactReasons", []) + if reasons: + cli_out_write(f" Impact reasons:") + for reason in reasons: + cli_out_write(wrap_and_indent(f"* {reason['name']}", indent=8), + fg=Color.GREEN if reason['isPositive'] else Color.RED) + if result["provider_url"]: + expected_url = (result["provider_url"].rstrip("/") + + f"/ui/catalog/vulnerabilities/details/{adv['name']}") + cli_out_write(f" Url: {expected_url}") + cli_out_write("") + cli_out_write("\n" + desc_wrapped) + if publishedAt: + cli_out_write(f" Published at: ", endline="", fg=Color.BRIGHT_BLUE) + cli_out_write(publishedAt) + references = node.get("references") if references: - cli_out_write(f" url: {references[0]}", fg=Color.BRIGHT_BLUE) + cli_out_write(f" url: ", endline="", fg=Color.BRIGHT_BLUE) + cli_out_write(references[0]) + + vulnerablePackages = node.get("vulnerablePackages") + if vulnerablePackages: + fixVersions = [fix['version'] + for fix_edge in vulnerablePackages.get("edges", []) + for fix in fix_edge['node'].get("fixVersions", [])] + if fixVersions: + cli_out_write(f" fixed in version(s): ", endline="", fg=Color.BRIGHT_BLUE) + cli_out_write(', '.join(fixVersions)) cli_out_write("") color_for_total = Color.BRIGHT_RED if total_vulns else Color.BRIGHT_GREEN @@ -100,7 +142,7 @@ def wrap_and_indent(txt, limit=80, indent=2): "through patches applied in the recipe.\nTo verify if a patch has been applied, check the recipe in Conan Center.\n", fg=Color.BRIGHT_YELLOW) - if total_vulns > 0 or not "error" in result: + if total_vulns > 0 or "error" not in result: cli_out_write("\nVulnerability information provided by JFrog Catalog. Check " "https://audit.conan.io/jfrogcuration for more information.\n", fg=Color.BRIGHT_GREEN) @@ -118,29 +160,28 @@ def _render_vulns(vulns, template): template = Template(template, autoescape=select_autoescape(['html', 'xml'])) return template.render(vulns=vulns, version=__version__) + vuln_html = """ Conan Audit Vulnerabilities Report - + - - + + @@ -167,20 +214,16 @@ def _render_vulns(vulns, template):

Conan Audit Vulnerabilities Report

- +
- - - + - - - + @@ -190,21 +233,69 @@ def _render_vulns(vulns, template): {% set severity_id = parts[0] %} {% set severity_label = parts[1] if parts|length > 1 else parts[0] %} - - + - @@ -229,12 +320,14 @@ def _render_vulns(vulns, template): """ + def html_vuln_formatter(result): vulns = [] for ref, pkg_info in result["data"].items(): edges = pkg_info.get("vulnerabilities", {}).get("edges", []) if not edges: - description = "No vulnerabilities found." if not "error" in pkg_info else pkg_info["error"].get("details", "") + description = "No vulnerabilities found." if "error" not in pkg_info \ + else pkg_info["error"].get("details", "") vulns.append({ "package": ref, "vuln_id": "-", @@ -242,7 +335,12 @@ def html_vuln_formatter(result): "severity": "N/A", "score": "-", "description": description, - "references": [] + "references": [], + "withdrawn": False, + "advisories": [], + "provider_url": result.get("provider_url"), + "fixVersions": [], + "publishedAt": None }) else: sorted_vulns = sorted(edges, key=lambda v: -severity_order.get(v["node"].get("severity", "Medium"), 2)) @@ -256,6 +354,13 @@ def html_vuln_formatter(result): aliases = node.get("aliases", []) references = node.get("references", []) desc = node.get("description", "") + withdrawn = node.get("withdrawn", False) + advisories = node.get("advisories", []) + jfrogAdvisories = [adv for adv in advisories + if adv.get("name", "").startswith("JFSA-")] + fixVersions = [fix['version'] + for fix_edge in node.get("vulnerablePackages", {}).get("edges", []) + for fix in fix_edge['node'].get("fixVersions", [])] vulns.append({ "package": ref, "vuln_id": name, @@ -264,6 +369,11 @@ def html_vuln_formatter(result): "score": score_txt, "description": desc, "references": references, + "withdrawn": withdrawn, + "advisories": jfrogAdvisories, + "provider_url": result.get("provider_url"), + "fixVersions": fixVersions, + "publishedAt": node.get("publishedAt") }) cli_out_write(_render_vulns(vulns, vuln_html)) diff --git a/conan/cli/formatters/graph/graph_info_text.py b/conan/cli/formatters/graph/graph_info_text.py index aa431cd8573..ac04a056e60 100644 --- a/conan/cli/formatters/graph/graph_info_text.py +++ b/conan/cli/formatters/graph/graph_info_text.py @@ -1,13 +1,29 @@ import fnmatch from collections import OrderedDict +from conan.api.model import RecipeReference from conan.api.output import ConanOutput, cli_out_write def filter_graph(graph, package_filter=None, field_filter=None): if package_filter is not None: + def _matching(node, pattern): + if fnmatch.fnmatch(node["ref"] or "", pattern): + return True + if pattern == "&": # Handle the consumer pattern + if node["recipe"] == "Consumer": + return True + # How to deal with --requires=xxx --package-filter=& "consumers" + root = graph["nodes"]["0"] + if root["recipe"] == "Cli" and node is not root: + # We look if the current node is a direct dependency of the root node + node_ref = RecipeReference.loads(node["ref"]) + for dep in root["dependencies"].values(): + if dep["direct"] and node_ref == RecipeReference.loads(dep["ref"]): + return True + graph["nodes"] = {id_: n for id_, n in graph["nodes"].items() - if any(fnmatch.fnmatch(n["ref"] or "", p) for p in package_filter)} + if any(_matching(n, p) for p in package_filter)} if field_filter is not None: if "ref" not in field_filter: field_filter.append("ref") diff --git a/conan/cli/formatters/graph/info_graph_html.py b/conan/cli/formatters/graph/info_graph_html.py index a38edcf23cb..1d0160f71d0 100644 --- a/conan/cli/formatters/graph/info_graph_html.py +++ b/conan/cli/formatters/graph/info_graph_html.py @@ -87,8 +87,14 @@ global_edges = {}; let edge_counter = 0; let conflict=null; + let provide_conflict=null; + let missing_error=null; if (graph_data["error"] && graph_data["error"]["type"] == "conflict") conflict = graph_data["error"]; + else if (graph_data["error"] && graph_data["error"]["type"] == "provide_conflict") + provide_conflict = graph_data["error"]; + else if (graph_data["error"] && graph_data["error"]["type"] == "missing") + missing_error = graph_data["error"]; for (const [node_id, node] of Object.entries(graph_data["nodes"])) { if (node.context == "build" && hide_build) continue; if (node.test && hide_test) continue; @@ -128,6 +134,11 @@ color = "Black"; shape = "circle"; } + if (provide_conflict && provide_conflict.node.id == node_id){ + font.color = "white"; + color = "Black"; + shape = "circle"; + } if (search_pkgs) { let patterns = search_pkgs.split(',') .map(pattern => pattern.trim()) @@ -199,6 +210,34 @@ label: conflict.branch2.require.ref}); global_edges[edge_counter++] = conflict.branch2.require; } + if (provide_conflict) { + // The nodes are already there, we'll just add an edge to the conflict node + edges.push({id: edge_counter, + from: provide_conflict.conflicting_node.id, + to: provide_conflict.node.id, + color: {color: "Red", highlight: "Red"}, + label: provide_conflict.provided, + title: "Both nodes provide the same requirement: " + provide_conflict.provided.join(", "), + dashes: true}); + global_edges[edge_counter++] = {"provided": provide_conflict.provided}; + } + if(missing_error) { + nodes.push({ + id: "missing_node", + font: {multi: 'html', color: "white"}, + label: missing_error["require"]["ref"], + shape: "Circle", + color: {background: "Black"}, + }); + edges.push({id: edge_counter, + from: missing_error["node"]["id"], + to: "missing_node", + color: {color: "Red", highlight: "Red"}, + label: "missing", + title: "missing", + dashes: true}); + global_edges[edge_counter++] = {"missing": missing_error["error"]}; + } return {nodes: new vis.DataSet(nodes), edges: new vis.DataSet(edges)}; }; function define_legend() { diff --git a/conan/cli/formatters/report/diff.py b/conan/cli/formatters/report/diff.py index b64259d7a3a..547d1bbba0a 100644 --- a/conan/cli/formatters/report/diff.py +++ b/conan/cli/formatters/report/diff.py @@ -7,6 +7,7 @@ from conan.api.output import cli_out_write from conan.cli.formatters.report.diff_html import diff_html + def _generate_json(result): diff_text = result["diff"] src_prefix = result["src_prefix"] @@ -22,6 +23,7 @@ def _generate_json(result): ret[current_filename].append(line) return ret + def _get_filenames(line, src_prefix, dst_prefix): """ Extracts the source and destination filenames from a diff line. @@ -37,16 +39,15 @@ def _get_filenames(line, src_prefix, dst_prefix): return src_filename, dst_filename + def _render_diff(content, template, template_folder, **kwargs): from conan import __version__ template = Template(template, autoescape=True) + def _safe_filename(filename): # Calculate base64 of the filename return base64.b64encode(filename.encode(), altchars=b'-_').decode() - def _get_diff_filename(line): - return _get_filenames(line, kwargs["src_prefix"], kwargs["dst_prefix"])[0] - def _remove_prefixes(line): return line.replace(kwargs["src_prefix"][:-1], "").replace(kwargs["dst_prefix"][:-1], "") @@ -56,15 +57,80 @@ def _replace_cache_paths(line): def _replace_paths(line): return _remove_prefixes(_replace_cache_paths(line)) + def _extract_header(diff_lines): + # Header ends at the first occurrence of +++ line, + # and it can be at most 10 lines long + for i, line in enumerate(diff_lines[:10]): + if line.startswith("+++ "): + return diff_lines[:i + 1] + return diff_lines[:10] + + def _parse_header_is_deleted(header_contents): + return ("+++ /dev/null" in header_contents + or any("deleted file mode" in line for line in header_contents)) + + def _parse_header_rename_to(header_contents): + if not any("similarity index" in line for line in header_contents): + return None + for line in header_contents: + if line.startswith("rename to "): + return line[len("rename to "):] + return None + + per_folder = {"folders": {}, "files": {}} + for file in content: + header = _extract_header(content[file]) + renamed_to = _parse_header_rename_to(header) + replaced_path = _replace_paths(renamed_to or file) + replaced_file = replaced_path.replace("(old)", "").replace("(new)", "").replace("\\", "/") + bits = replaced_file.split("/")[1:] + cur = per_folder + for folder in bits[:-1]: + cur = cur["folders"].setdefault(folder, {"folders": {}, "files": {}}) + filename = bits[-1] + cur["files"][filename] = {"filename": file, # This is file so renamed use old name + "is_new": "(new)" in replaced_path, + "is_deleted": _parse_header_is_deleted(header), + "renamed_to": renamed_to, + "relative_path": replaced_path} + + def flatten_empty_folders(current_node): + for folder_data in current_node["folders"].values(): + flatten_empty_folders(folder_data) + + promoted_folders = {} + + # The list here is important to avoid modifying the dict while iterating + for folder_name, folder_data in list(current_node["folders"].items()): + if not folder_data["files"]: + for sub_folder_name, sub_folder_data in folder_data['folders'].items(): + new_key = os.path.join(folder_name, sub_folder_name) + promoted_folders[new_key] = sub_folder_data + + del current_node["folders"][folder_name] + + current_node["folders"].update(promoted_folders) + + flatten_empty_folders(per_folder) + + # Now sort each folder and file recursively + def sort_folders_and_files(node): + node["folders"] = dict(sorted(node["folders"].items())) + node["files"] = dict(sorted(node["files"].items(), key=lambda x: x[0].lower())) + for folder_data in node["folders"].values(): + sort_folders_and_files(folder_data) + sort_folders_and_files(per_folder) + return template.render(content=content, + per_folder=per_folder, base_template_path=template_folder, version=__version__, safe_filename=_safe_filename, replace_paths=_replace_paths, replace_cache_paths=_replace_cache_paths, remove_prefixes=_remove_prefixes, - get_diff_filename=_get_diff_filename, **kwargs) + def format_diff_html(result): conan_api = result["conan_api"] diff --git a/conan/cli/formatters/report/diff_html.py b/conan/cli/formatters/report/diff_html.py index b470fef1d4d..27a08057844 100644 --- a/conan/cli/formatters/report/diff_html.py +++ b/conan/cli/formatters/report/diff_html.py @@ -1,37 +1,674 @@ diff_html = r""" +{% macro render_sidebar_folder(folder, folder_info) %} + {%- for name, sub_folder_info in folder_info["folders"].items() %} + {% set folder_name = folder + "/" + name %} +
  • +
    + {{ name }} +
      + {{ render_sidebar_folder(folder_name, sub_folder_info) }} +
    +
    +
  • + {%- endfor %} + {%- for name, file_info in folder_info["files"].items() %} + {% set file_type = "renamed" if file_info["renamed_to"] else ( + "deleted" if file_info["is_deleted"] else ( + "new" if file_info["is_new"] else "old")) %} +
  • + + {% if file_info["renamed_to"] %} + {{ file_info["renamed_to"].split("/")[1:][-1] }} + {% else %} + {{ name }} + {% endif %} + +
  • + {%- endfor %} +{% endmacro %} + +{% macro render_diff_folder(folder_info) %} + {%- for name, sub_folder_info in folder_info["folders"].items() %} + {{ render_diff_folder(sub_folder_info) }} + {%- endfor %} + {%- for name, file_info in folder_info["files"].items() %} + {% set filename = file_info["filename"] %} + +
    +
    +
    + + + {{ replace_cache_paths(filename) | replace("(old)/", "") | replace("(new)/", "") }} + {% if file_info["renamed_to"] %} +  →  + {{ replace_cache_paths(file_info["renamed_to"]) | replace("(old)/", "") | replace("(new)/", "") }} + {% endif %} + +
    +
    +
    +
    +
    +
    +
    + {%- endfor %} +{% endmacro %} - {{ old_reference }} - {{ new_reference }} + Diff report for {{ old_reference }} - {{ new_reference }}
    +
    + + +
    @@ -63,6 +67,7 @@ const graph_data = {{ deps_graph | tojson }}; let hide_build = false; let hide_test = false; + let show_transitive = false; let search_pkgs = null; let focus_search = false; let excluded_pkgs = null; @@ -186,6 +191,13 @@ color: {color: "SkyBlue", highlight: "Blue"}}); global_edges[edge_counter++] = dep; } + if (show_transitive && dep.direct === false){ + let target_id = targets[dep_id] || dep_id; + edges.push({id: edge_counter, from: node_id, to: target_id, + color: {color: "LightGray", highlight: "Gray"}, + dashes: true}); + global_edges[edge_counter++] = dep; + } if (loop_error && loop_error[1] == node["name"] && loop_error[0] == dep["ref"]) { let target_id = targets[dep_id] || dep_id; edges.push({id: edge_counter, from: node_id, to: target_id, @@ -381,6 +393,10 @@ hide_test = !hide_test; draw(); } + function switchTransitive() { + show_transitive = !show_transitive; + draw(); + } function collapsePackages() { collapse_packages = !collapse_packages; draw(); From 5d2f00d7b3e6a79140cc357365cf18a114b97f25 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 9 Mar 2026 10:34:52 +0100 Subject: [PATCH 042/110] add trace to DB timeout (#19728) add trace --- conan/internal/cache/db/table.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/conan/internal/cache/db/table.py b/conan/internal/cache/db/table.py index eb77a64d726..71e0ebf5813 100644 --- a/conan/internal/cache/db/table.py +++ b/conan/internal/cache/db/table.py @@ -1,9 +1,11 @@ import sqlite3 import threading +import traceback from collections import defaultdict, namedtuple from contextlib import contextmanager from typing import Tuple, List +from conan.api.output import ConanOutput from conan.errors import ConanException @@ -26,6 +28,8 @@ def __init__(self, filename): @contextmanager def db_connection(self): if not self._lock.acquire(timeout=20): + m = traceback.format_exc() + "\n" + ConanOutput().error(m) raise ConanException("Conan failed to acquire database lock in 20s. Maybe the system is " "under very heavy load. Please report it to Github tickets") # isolation_level=None, puts it in regular SQLITE autocommit mode, every From ba2d6e7a88e05cbf2e6ea43c1874761321def626 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 9 Mar 2026 11:23:01 +0100 Subject: [PATCH 043/110] Tests/improves (#19727) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fake_runs * simplified dead attrs * wip * add workspace check * wip * fix * Rename cmakedeps2 tests folder --------- Co-authored-by: Abril Rincón Blanco <5364255+AbrilRBS@users.noreply.github.com> --- conan/internal/rest/rest_client_v2.py | 28 +++----- test/functional/sbom/test_cyclonedx.py | 71 +++++++++---------- .../apple/test_xcodebuild_targets.py | 2 - .../__init__.py | 0 .../test_cmakeconfigdeps_aliases.py | 0 .../test_cmakeconfigdeps_frameworks.py | 0 .../test_cmakeconfigdeps_new.py | 0 .../test_cmakeconfigdeps_new_cpp_linkage.py | 0 .../test_cmakeconfigdeps_new_paths.py | 0 .../test_cmakeconfigdeps_sources.py | 0 .../cmake/cmakedeps/test_apple_frameworks.py | 18 ++--- .../functional/toolchains/cmake/test_cmake.py | 20 +++--- .../toolchains/cmake/test_cmake_multi.py | 26 +++---- .../cmake/test_cmake_transitive_rpath.py | 34 +++++---- .../env/test_virtualenv_powershell.py | 1 - .../toolchains/scons/test_sconsdeps.py | 2 - test/functional/toolchains/test_premake.py | 12 ++-- .../build_requires/build_requires_test.py | 2 - .../cache/test_home_special_char.py | 1 - test/integration/command/test_package_test.py | 7 -- .../integration/editable/editable_add_test.py | 1 - test/integration/environment/test_env.py | 1 - .../graph/test_replace_requires.py | 16 ++--- .../lockfile/test_lock_requires.py | 3 +- .../package_id_requires_modes_test.py | 2 + .../python_requires_package_id_test.py | 32 +++++++++ test/integration/remote/server_error_test.py | 16 +++++ test/integration/sbom/test_cyclonedx.py | 16 ++--- test/integration/workspace/test_workspace.py | 4 +- .../tools/system/python_manager_test.py | 14 +++- 30 files changed, 176 insertions(+), 153 deletions(-) rename test/functional/toolchains/cmake/{cmakedeps2 => cmakeconfigdeps}/__init__.py (100%) rename test/functional/toolchains/cmake/{cmakedeps2 => cmakeconfigdeps}/test_cmakeconfigdeps_aliases.py (100%) rename test/functional/toolchains/cmake/{cmakedeps2 => cmakeconfigdeps}/test_cmakeconfigdeps_frameworks.py (100%) rename test/functional/toolchains/cmake/{cmakedeps2 => cmakeconfigdeps}/test_cmakeconfigdeps_new.py (100%) rename test/functional/toolchains/cmake/{cmakedeps2 => cmakeconfigdeps}/test_cmakeconfigdeps_new_cpp_linkage.py (100%) rename test/functional/toolchains/cmake/{cmakedeps2 => cmakeconfigdeps}/test_cmakeconfigdeps_new_paths.py (100%) rename test/functional/toolchains/cmake/{cmakedeps2 => cmakeconfigdeps}/test_cmakeconfigdeps_sources.py (100%) diff --git a/conan/internal/rest/rest_client_v2.py b/conan/internal/rest/rest_client_v2.py index ddcee217f13..aabd00536d1 100644 --- a/conan/internal/rest/rest_client_v2.py +++ b/conan/internal/rest/rest_client_v2.py @@ -52,19 +52,13 @@ def __call__(self, request): return request -def get_exception_from_error(error_code): +def _raise_exception_from_error(error_code, text): tmp = {v: k for k, v in EXCEPTION_CODE_MAPPING.items() # All except NotFound if k not in (RecipeNotFoundException, PackageNotFoundException)} - if error_code in tmp: - # logger.debug("REST ERROR: %s" % str(tmp[error_code])) - return tmp[error_code] - else: - base_error = int(str(error_code)[0] + "00") - # logger.debug("REST ERROR: %s" % str(base_error)) - try: - return tmp[base_error] - except KeyError: - return None + try: + raise tmp[error_code](text) + except KeyError: + raise ConanException(f"Server exception {error_code}: {text}") def _get_mac_digest(): # To avoid re-hashing all the time the same mac @@ -126,7 +120,7 @@ def check_credentials(self, force_auth=False): if ret.status_code != 200: ret.charset = "utf-8" # To be able to access ret.text (ret.content are bytes) text = ret.text if ret.status_code != 404 else "404 Not found" - raise get_exception_from_error(ret.status_code)(text) + _raise_exception_from_error(ret.status_code, text) return ret.content.decode() def server_capabilities(self): @@ -139,7 +133,7 @@ def server_capabilities(self): if not server_capabilities and not ret.ok: # Old Artifactory might return 401/403 without capabilities, we don't want # to cache them #5687, so raise the exception and force authentication - raise get_exception_from_error(ret.status_code)(response_to_str(ret)) + _raise_exception_from_error(ret.status_code, response_to_str(ret)) if server_capabilities is None: # Some servers returning 200-ok, even if not valid repo raise ConanException(f"Remote {self.remote_url} doesn't seem like a valid Conan remote") @@ -154,7 +148,7 @@ def _get_json(self, url, headers=None): if response.status_code != 200: # Error message is text response.charset = "utf-8" # To be able to access ret.text (ret.content are bytes) - raise get_exception_from_error(response.status_code)(response_to_str(response)) + _raise_exception_from_error(response.status_code, response_to_str(response)) content = response.content.decode() content_type = response.headers.get("Content-Type") @@ -358,7 +352,7 @@ def remove_all_packages(self, ref): if response.status_code != 200: # Error message is text # To be able to access ret.text (ret.content are bytes) response.charset = "utf-8" - raise get_exception_from_error(response.status_code)(response.text) + _raise_exception_from_error(response.status_code, response.text) def remove_packages(self, prefs): self.check_credentials() @@ -376,7 +370,7 @@ def remove_packages(self, prefs): if response.status_code != 200: # Error message is text # To be able to access ret.text (ret.content are bytes) response.charset = "utf-8" - raise get_exception_from_error(response.status_code)(response.text) + _raise_exception_from_error(response.status_code, response.text) def remove_recipe(self, ref): """ Remove a recipe and packages """ @@ -396,7 +390,7 @@ def remove_recipe(self, ref): if response.status_code != 200: # Error message is text # To be able to access ret.text (ret.content are bytes) response.charset = "utf-8" - raise get_exception_from_error(response.status_code)(response.text) + _raise_exception_from_error(response.status_code, response.text) def get_recipe_revision_reference(self, ref): # FIXME: implement this new endpoint in the remotes? diff --git a/test/functional/sbom/test_cyclonedx.py b/test/functional/sbom/test_cyclonedx.py index 528d413fff2..2b357ce8c6f 100644 --- a/test/functional/sbom/test_cyclonedx.py +++ b/test/functional/sbom/test_cyclonedx.py @@ -1,11 +1,11 @@ import json +import os import pytest +from conan.internal.util.files import save from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient -from conan.internal.util.files import save -import os # Using the sbom tool with "conan create" sbom_hook_post_package = """ @@ -13,73 +13,72 @@ import os from conan.errors import ConanException from conan.api.output import ConanOutput -from conan.tools.sbom import {cyclone_version} +from conan.tools.sbom import cyclonedx_1_4, cyclonedx_1_6 def post_package(conanfile): - sbom_{cyclone_version} = {cyclone_version}(conanfile, add_build={add_build}, add_tests={add_tests}) - metadata_folder = conanfile.package_metadata_folder - file_name = "sbom.cdx.json" - with open(os.path.join(metadata_folder, file_name), 'w') as f: - json.dump(sbom_{cyclone_version}, f, indent=4) - ConanOutput().success(f"CYCLONEDX CREATED - {{conanfile.package_metadata_folder}}") + sbom_cyclonedx_1_4 = cyclonedx_1_4(conanfile, add_build=True, add_tests=True) + sbom_cyclonedx_1_6 = cyclonedx_1_6(conanfile, add_build=True, add_tests=True) + with open(os.path.join(conanfile.package_metadata_folder, "sbom14.cdx.json"), 'w') as f: + json.dump(sbom_cyclonedx_1_4, f, indent=4) + with open(os.path.join(conanfile.package_metadata_folder, "sbom16.cdx.json"), 'w') as f: + json.dump(sbom_cyclonedx_1_6, f, indent=4) """ -@pytest.mark.parametrize("cyclone_version", ["cyclonedx_1_4", "cyclonedx_1_6"]) class TestCyclonedx: @pytest.fixture() - def hook_setup_post_package(self, cyclone_version): - tc = TestClient() - hook_path = os.path.join(tc.paths.hooks_path, "hook_sbom.py") - save(hook_path, sbom_hook_post_package.format(cyclone_version=cyclone_version, - add_build=True, add_tests=True)) - return tc - - @pytest.fixture() - def hook_setup_post_package_tl(self, cyclone_version, transitive_libraries): + def hook_setup_post_package_tl(self, transitive_libraries): tc = transitive_libraries hook_path = os.path.join(tc.paths.hooks_path, "hook_sbom.py") - save(hook_path, sbom_hook_post_package.format(cyclone_version=cyclone_version, - add_build=True, add_tests=True)) + save(hook_path, sbom_hook_post_package) return tc @pytest.mark.tool("cmake") def test_sbom_generation_create(self, hook_setup_post_package_tl): + # TODO This doesn't need to be a functional test, check why tc = hook_setup_post_package_tl tc.run("new cmake_lib -d name=bar -d version=1.0 -d requires=engine/1.0 -f") # bar -> engine/1.0 -> matrix/1.0 tc.run("create . -tf=") bar_layout = tc.created_layout() - assert os.path.exists(os.path.join(bar_layout.metadata(), "sbom.cdx.json")) + assert os.path.exists(os.path.join(bar_layout.metadata(), "sbom14.cdx.json")) + assert os.path.exists(os.path.join(bar_layout.metadata(), "sbom16.cdx.json")) @pytest.mark.tool("cmake") @pytest.mark.parametrize("user, channel, user_dep, channel_dep", [("user", None, "user_dep", None), ("user", "channel", "user_dep", "channel_dep")]) - def test_sbom_user_path(self, hook_setup_post_package_tl, user, channel, user_dep, channel_dep): - tc = hook_setup_post_package_tl + def test_sbom_user_path(self, user, channel, user_dep, channel_dep): + tc = TestClient(light=True) + hook_path = os.path.join(tc.paths.hooks_path, "hook_sbom.py") + save(hook_path, sbom_hook_post_package) channel_ref = f"/{channel_dep}" if channel_dep else "" tc.save({"dep/conanfile.py": GenConanfile("dep", "1.0"), "conanfile.py": GenConanfile("main", "1.0").with_requires( f"dep/1.0@{user_dep}{channel_ref}")}) command = "create dep" - if user: command += f" --user={user_dep}" - if channel: command += f" --channel={channel_dep}" + if user: + command += f" --user={user_dep}" + if channel: + command += f" --channel={channel_dep}" tc.run(command) command = "create ." - if user: command += f" --user={user}" - if channel: command += f" --channel={channel}" + if user: + command += f" --user={user}" + if channel: + command += f" --channel={channel}" tc.run(command) - create_layout = tc.created_layout() - cyclone_path = os.path.join(create_layout.metadata(), "sbom.cdx.json") - content = tc.load(cyclone_path) - content_json = json.loads(content) + for version in ("14", "16"): + create_layout = tc.created_layout() + cyclone_path = os.path.join(create_layout.metadata(), f"sbom{version}.cdx.json") + content = tc.load(cyclone_path) + content_json = json.loads(content) - assert content_json["components"][0]["bom-ref"].split("&user=")[ - 1] == f"{user}&channel={channel}" if channel else user - assert content_json["dependencies"][0]["dependsOn"][0].split("&user=")[ - 1] == f"{user_dep}&channel={channel_dep}" if channel_dep else user_dep + assert content_json["components"][0]["bom-ref"].split("&user=")[ + 1] == f"{user}&channel={channel}" if channel else user + assert content_json["dependencies"][0]["dependsOn"][0].split("&user=")[ + 1] == f"{user_dep}&channel={channel_dep}" if channel_dep else user_dep diff --git a/test/functional/toolchains/apple/test_xcodebuild_targets.py b/test/functional/toolchains/apple/test_xcodebuild_targets.py index 8c6767dedf8..4b759d06531 100644 --- a/test/functional/toolchains/apple/test_xcodebuild_targets.py +++ b/test/functional/toolchains/apple/test_xcodebuild_targets.py @@ -60,8 +60,6 @@ class HelloTestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "CMakeDeps", "CMakeToolchain", "VirtualBuildEnv", "VirtualRunEnv" - apply_env = False - test_type = "explicit" options = {"shared": [True, False], "fPIC": [True, False]} default_options = {"shared": False, "fPIC": True} diff --git a/test/functional/toolchains/cmake/cmakedeps2/__init__.py b/test/functional/toolchains/cmake/cmakeconfigdeps/__init__.py similarity index 100% rename from test/functional/toolchains/cmake/cmakedeps2/__init__.py rename to test/functional/toolchains/cmake/cmakeconfigdeps/__init__.py diff --git a/test/functional/toolchains/cmake/cmakedeps2/test_cmakeconfigdeps_aliases.py b/test/functional/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps_aliases.py similarity index 100% rename from test/functional/toolchains/cmake/cmakedeps2/test_cmakeconfigdeps_aliases.py rename to test/functional/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps_aliases.py diff --git a/test/functional/toolchains/cmake/cmakedeps2/test_cmakeconfigdeps_frameworks.py b/test/functional/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps_frameworks.py similarity index 100% rename from test/functional/toolchains/cmake/cmakedeps2/test_cmakeconfigdeps_frameworks.py rename to test/functional/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps_frameworks.py diff --git a/test/functional/toolchains/cmake/cmakedeps2/test_cmakeconfigdeps_new.py b/test/functional/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps_new.py similarity index 100% rename from test/functional/toolchains/cmake/cmakedeps2/test_cmakeconfigdeps_new.py rename to test/functional/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps_new.py diff --git a/test/functional/toolchains/cmake/cmakedeps2/test_cmakeconfigdeps_new_cpp_linkage.py b/test/functional/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps_new_cpp_linkage.py similarity index 100% rename from test/functional/toolchains/cmake/cmakedeps2/test_cmakeconfigdeps_new_cpp_linkage.py rename to test/functional/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps_new_cpp_linkage.py diff --git a/test/functional/toolchains/cmake/cmakedeps2/test_cmakeconfigdeps_new_paths.py b/test/functional/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps_new_paths.py similarity index 100% rename from test/functional/toolchains/cmake/cmakedeps2/test_cmakeconfigdeps_new_paths.py rename to test/functional/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps_new_paths.py diff --git a/test/functional/toolchains/cmake/cmakedeps2/test_cmakeconfigdeps_sources.py b/test/functional/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps_sources.py similarity index 100% rename from test/functional/toolchains/cmake/cmakedeps2/test_cmakeconfigdeps_sources.py rename to test/functional/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps_sources.py diff --git a/test/functional/toolchains/cmake/cmakedeps/test_apple_frameworks.py b/test/functional/toolchains/cmake/cmakedeps/test_apple_frameworks.py index e00f857d02f..7c3e3956cc4 100644 --- a/test/functional/toolchains/cmake/cmakedeps/test_apple_frameworks.py +++ b/test/functional/toolchains/cmake/cmakedeps/test_apple_frameworks.py @@ -196,9 +196,9 @@ class HELLO_EXPORT Hello @pytest.mark.tool("cmake") @pytest.mark.skipif(platform.system() != "Darwin", reason="Only OSX") @pytest.mark.parametrize("settings", - [('',), - ('-pr:b default -s os=iOS -s os.sdk=iphoneos -s os.version=10.0 -s arch=armv8',), - ("-pr:b default -s os=tvOS -s os.sdk=appletvos -s os.version=11.0 -s arch=armv8",)]) + ['', + '-s os=iOS -s os.sdk=iphoneos -s os.version=10.0 -s arch=armv8', + "-s os=tvOS -s os.sdk=appletvos -s os.version=11.0 -s arch=armv8"]) def test_apple_own_framework_cross_build(settings): client = TestClient() @@ -429,7 +429,7 @@ def package_info(self): self.cpp_info.components["libhello"].frameworks.extend(["CoreFoundation"]) """) - hello_cpp = textwrap.dedent(""" + hello_cpp_core = textwrap.dedent(""" #include void hello_api() @@ -441,7 +441,7 @@ def package_info(self): CFRelease(dict); } """) - hello_h = textwrap.dedent(""" + hello_h_core = textwrap.dedent(""" void hello_api(); """) cmakelists_txt = textwrap.dedent(""" @@ -511,8 +511,8 @@ def test(self): """) t = TestClient() t.save({'conanfile.py': conanfile_py, - 'hello.cpp': hello_cpp, - 'hello.h': hello_h, + 'hello.cpp': hello_cpp_core, + 'hello.h': hello_h_core, 'CMakeLists.txt': cmakelists_txt, 'test_package/conanfile.py': test_conanfile_py, 'test_package/CMakeLists.txt': test_cmakelists_txt, @@ -554,7 +554,7 @@ def test_iphoneos_crossbuild(): target_link_libraries(main hello::hello) """) - conanfile = textwrap.dedent(""" + hello = textwrap.dedent(""" from conan import ConanFile from conan.tools.cmake import CMake @@ -570,7 +570,7 @@ def build(self): cmake.build() """) - client.save({"conanfile.py": conanfile, + client.save({"conanfile.py": hello, "CMakeLists.txt": cmakelists, "main.cpp": main, "ios-armv8": profile}, clean_first=True) diff --git a/test/functional/toolchains/cmake/test_cmake.py b/test/functional/toolchains/cmake/test_cmake.py index cc01c55ed64..f7caf919625 100644 --- a/test/functional/toolchains/cmake/test_cmake.py +++ b/test/functional/toolchains/cmake/test_cmake.py @@ -428,18 +428,16 @@ def test_toolchain_apple(self, build_type, cppstd, shared): "CMAKE_INSTALL_NAME_DIR": "" } + arch_flags = { + "CMAKE_C_FLAGS": "-m64", + "CMAKE_CXX_FLAGS": "-m64 -stdlib=libc++", + "CMAKE_SHARED_LINKER_FLAGS": "-m64", + "CMAKE_EXE_LINKER_FLAGS": "-m64", + } host_profile = self.client.get_default_host_profile() - if host_profile.settings.get("arch") == "x86_64": - vals.update({ - "CMAKE_C_FLAGS": "-m64", - "CMAKE_CXX_FLAGS": "-m64 -stdlib=libc++", - "CMAKE_SHARED_LINKER_FLAGS": "-m64", - "CMAKE_EXE_LINKER_FLAGS": "-m64", - }) - else: - vals.update({ - "CMAKE_CXX_FLAGS": "-stdlib=libc++", - }) + if host_profile.settings.get("arch") != "x86_64": + arch_flags = {"CMAKE_CXX_FLAGS": "-stdlib=libc++"} + vals.update(arch_flags) def _verify_out(marker=">>"): if shared: diff --git a/test/functional/toolchains/cmake/test_cmake_multi.py b/test/functional/toolchains/cmake/test_cmake_multi.py index 0407765420b..bff9cd45f66 100644 --- a/test/functional/toolchains/cmake/test_cmake_multi.py +++ b/test/functional/toolchains/cmake/test_cmake_multi.py @@ -8,21 +8,19 @@ def test_multi_cmake(): conanfile = textwrap.dedent(""" from conan import ConanFile - from conan.tools.cmake import CMakeToolchain, CMake, cmake_layout, CMakeDeps + from conan.tools.cmake import CMakeToolchain, CMake, cmake_layout import os - class multiRecipe(ConanFile): settings = "os", "compiler", "build_type", "arch" - exports_sources = "cmake_one/CMakeLists.txt", "cmake_two/CMakeLists.txt", "src_one/*", "src_two/*" + exports_sources = ("cmake_one/CMakeLists.txt", "cmake_two/CMakeLists.txt", + "src_one/*", "src_two/*") def layout(self): cmake_layout(self) def generate(self): - deps = CMakeDeps(self) - deps.generate() tc = CMakeToolchain(self) tc.generate() @@ -30,11 +28,9 @@ def build_one(self): cmake = CMake(self) cmake.configure(build_script_folder="cmake_one", subfolder="one") cmake.build(subfolder="one") - # cmake.install(subfolder="one") def build_two(self): cmake = CMake(self) - # CMAKE_PREFIX_PATH cmake.configure(build_script_folder="cmake_two", subfolder="two") cmake.build(subfolder="two") @@ -62,12 +58,8 @@ def package_info(self): """) hello_h = textwrap.dedent(""" - #ifndef HELLO_{name}_H - #define HELLO_{name}_H - + #pragma once void hello_{name}(); - - #endif """) cmakelist = textwrap.dedent(""" @@ -91,13 +83,13 @@ def package_info(self): "src_two/hello_two.cpp": hello_cpp.format(name="two")}) client.run("create . --name=multi --version=0.1") - file_ext = '.a' if platform.system() != "Windows" else '.lib' - lib_prefix = 'lib' if platform.system() != "Windows" else '' + ext = '.a' if platform.system() != "Windows" else '.lib' + prefix = 'lib' if platform.system() != "Windows" else '' assert "multi/0.1: package(): Packaged 1 '.h' file: hello_two.h" in client.out - assert f"multi/0.1: package(): Packaged 1 '{file_ext}' file: {lib_prefix}hello_two{file_ext}" in client.out + assert f"multi/0.1: package(): Packaged 1 '{ext}' file: {prefix}hello_two{ext}" in client.out package_folder = client.created_layout().package() assert not os.path.exists(os.path.join(package_folder, "one", "include", "hello_one.h")) - assert not os.path.exists(os.path.join(package_folder, "one", "lib", f"{lib_prefix}hello_one{file_ext}")) + assert not os.path.exists(os.path.join(package_folder, "one", "lib", f"{prefix}hello_one{ext}")) assert os.path.exists(os.path.join(package_folder, "two", "include", "hello_two.h")) - assert os.path.exists(os.path.join(package_folder, "two", "lib", f"{lib_prefix}hello_two{file_ext}")) + assert os.path.exists(os.path.join(package_folder, "two", "lib", f"{prefix}hello_two{ext}")) diff --git a/test/functional/toolchains/cmake/test_cmake_transitive_rpath.py b/test/functional/toolchains/cmake/test_cmake_transitive_rpath.py index 4eb5381896a..f649cff6251 100644 --- a/test/functional/toolchains/cmake/test_cmake_transitive_rpath.py +++ b/test/functional/toolchains/cmake/test_cmake_transitive_rpath.py @@ -2,18 +2,16 @@ import textwrap import pytest -from conan.test.utils.mocks import ConanFileMock -from conan.tools.env.environment import environment_wrap_command -from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import TestClient -@pytest.mark.skipif(platform.system() != "Linux", reason="Linux/gcc required for -rpath/-rpath-link testing") + +@pytest.mark.skipif(platform.system() != "Linux", + reason="Linux/gcc required for -rpath/-rpath-link testing") @pytest.mark.tool("cmake", "3.27") @pytest.mark.parametrize("use_cmake_config_deps", [True, False]) def test_cmake_sysroot_transitive_rpath(use_cmake_config_deps): c = TestClient() - extra_profile = textwrap.dedent(""" [conf] tools.build:sysroot=/path/to/nowhere @@ -78,8 +76,8 @@ def test_cmake_sysroot_transitive_rpath(use_cmake_config_deps): c.run(f"create . -o '*:shared=True' -pr=default -pr=../extra_profile {extra_conf}") - -@pytest.mark.skipif(platform.system() != "Linux", reason="Linux/gcc required for -rpath/-rpath-link testing") +@pytest.mark.skipif(platform.system() != "Linux", + reason="Linux/gcc required for -rpath/-rpath-link testing") @pytest.mark.tool("cmake", "3.27") @pytest.mark.parametrize("use_cmake_config_deps", [True, False]) def test_cmake_transitive_rpath_private_internal(use_cmake_config_deps): @@ -123,7 +121,8 @@ def test_cmake_transitive_rpath_private_internal(use_cmake_config_deps): install(TARGETS foo bar) """) - foobar_conanfile = textwrap.dedent(""" + cmake_deps_gen = "CMakeConfigDeps" if use_cmake_config_deps else "CMakeDeps" + foobar_conanfile = textwrap.dedent(f""" from conan import ConanFile from conan.tools.cmake import CMake, cmake_layout @@ -133,12 +132,12 @@ class foobarRecipe(ConanFile): version = "1.0" package_type = "library" settings = "os", "compiler", "build_type", "arch" - options = {"shared": [True, False]} - default_options = {"shared": True} + options = {{"shared": [True, False]}} + default_options = {{"shared": True}} exports_sources = "CMakeLists.txt", "src/*", "include/*" - generators = "CMakeDeps", "CMakeToolchain" + generators = "{cmake_deps_gen}", "CMakeToolchain" def layout(self): cmake_layout(self) @@ -158,7 +157,7 @@ def package_info(self): self.cpp_info.components["bar"].requires = ["foo"] """) - consumer_conanfile = textwrap.dedent(""" + consumer_conanfile = textwrap.dedent(f""" from conan import ConanFile from conan.tools.cmake import CMake, cmake_layout @@ -167,14 +166,14 @@ class consumerRecipe(ConanFile): version = "1.0" package_type = "library" settings = "os", "compiler", "build_type", "arch" - options = {"shared": [True, False]} - default_options = {"shared": True} - generators = "CMakeDeps", "CMakeToolchain" + options = {{"shared": [True, False]}} + default_options = {{"shared": True}} + generators = "{cmake_deps_gen}", "CMakeToolchain" exports_sources = "CMakeLists.txt", "src/*", "include/*" def layout(self): cmake_layout(self) - + def requirements(self): self.requires("foobar/1.0") @@ -223,8 +222,7 @@ def package_info(self): int main() { return consumer(2, 3) == 20 ? 0 : 1; } """) - extra_conf = "-c tools.cmake.cmakedeps:new=will_break_next" if use_cmake_config_deps else "" - extra_conf += " -c tools.build:add_rpath_link=True" # removing this should break the test + extra_conf = "-c tools.build:add_rpath_link=True" # removing this should break the test with c.chdir("foobar"): c.save({"include/foo.h": foo_h, diff --git a/test/functional/toolchains/env/test_virtualenv_powershell.py b/test/functional/toolchains/env/test_virtualenv_powershell.py index 25119d30501..15d192a53ee 100644 --- a/test/functional/toolchains/env/test_virtualenv_powershell.py +++ b/test/functional/toolchains/env/test_virtualenv_powershell.py @@ -43,7 +43,6 @@ class ConanFileToolsTest(ConanFile): name = "app" version = "0.1" requires = "pkg/0.1" - apply_env = False def build(self): self.output.info("----------BUILD----------------") diff --git a/test/functional/toolchains/scons/test_sconsdeps.py b/test/functional/toolchains/scons/test_sconsdeps.py index f7d9760fdd3..e30f64fc013 100644 --- a/test/functional/toolchains/scons/test_sconsdeps.py +++ b/test/functional/toolchains/scons/test_sconsdeps.py @@ -136,8 +136,6 @@ def package_info(self): class helloTestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "SConsDeps" - apply_env = False - test_type = "explicit" def requirements(self): self.requires(self.tested_reference_str) diff --git a/test/functional/toolchains/test_premake.py b/test/functional/toolchains/test_premake.py index 85921c6eb9a..e9022008090 100644 --- a/test/functional/toolchains/test_premake.py +++ b/test/functional/toolchains/test_premake.py @@ -11,7 +11,7 @@ from conan.test.assets.sources import gen_function_cpp -@pytest.mark.skipif(platform.machine() != "x86_64", +@pytest.mark.skipif(platform.machine() not in ("x86_64", "AMD64"), reason="Premake Legacy generator only supports x86_64 machines") @pytest.mark.tool("premake") def test_premake_legacy(matrix_client): @@ -71,10 +71,7 @@ def build(self): c.run("build .") assert "main: Release!" in c.out assert "matrix/1.0: Hello World Release!" in c.out - if platform.system() == "Windows": - assert "main _M_X64 defined" in c.out - else: - assert "main __x86_64__ defined" in c.out + assert "main _M_X64 defined" in c.out or "main __x86_64__ defined" in c.out c.run("build . -s build_type=Debug --build=missing") assert "main: Debug!" in c.out assert "matrix/1.0: Hello World Debug!" in c.out @@ -257,6 +254,7 @@ def build(self): c.run("build .", assert_error=True) # Error should be about not finding matrix + @pytest.mark.tool("premake") def test_premake_custom_configuration(transitive_libraries): c = transitive_libraries @@ -307,7 +305,7 @@ def build(self): ], configurations=["Debug", "Release", "DebugSanitizer", "ReleaseSanitizer"], ), - "conanfile.py": conanfile, - }) + "conanfile.py": conanfile + }) # Test it builds successfully with the custom configuration c.run("build . -o sanitizer=True") diff --git a/test/integration/build_requires/build_requires_test.py b/test/integration/build_requires/build_requires_test.py index fbf394fdde6..36c54e177fe 100644 --- a/test/integration/build_requires/build_requires_test.py +++ b/test/integration/build_requires/build_requires_test.py @@ -464,8 +464,6 @@ def build_requirements(self): test = textwrap.dedent(""" from conan import ConanFile class Test(ConanFile): - test_type = "explicit" - def build_requirements(self): self.tool_requires(self.tested_reference_str) def test(self): diff --git a/test/integration/cache/test_home_special_char.py b/test/integration/cache/test_home_special_char.py index 0ec98ebcba7..b6234c304cd 100644 --- a/test/integration/cache/test_home_special_char.py +++ b/test/integration/cache/test_home_special_char.py @@ -46,7 +46,6 @@ class App(ConanFile): settings = 'os', 'arch', 'compiler', 'build_type' generators = "VirtualBuildEnv" tool_requires = "mytool/1.0" - apply_env = False # SUPER IMPORTANT, DO NOT REMOVE def build(self): mycmd = "mytool.bat" if platform.system() == "Windows" else "mytool.sh" diff --git a/test/integration/command/test_package_test.py b/test/integration/command/test_package_test.py index b9b3ea7a8d9..a68cf3b1b43 100644 --- a/test/integration/command/test_package_test.py +++ b/test/integration/command/test_package_test.py @@ -323,13 +323,6 @@ def test_tested_reference_str(): """ At the test_package/conanfile the variable `self.tested_reference_str` is injected with the str of the reference being tested. It is available in all the methods. - - Compatibility with Conan 2.0: - If the 'test_type' is set to "explicit" the require won't be automatically injected and has to - be the user the one injecting the require or the build require using the - `self.tested_reference_str`. This 'test_type' can be removed in 2.0 if we consider it has - to be always explicit. The recipes will still work in Conan 2.0 because the 'test_type' will be - ignored. """ client = TestClient() test_conanfile = textwrap.dedent(""" diff --git a/test/integration/editable/editable_add_test.py b/test/integration/editable/editable_add_test.py index cb418090c27..0c6225ae95e 100644 --- a/test/integration/editable/editable_add_test.py +++ b/test/integration/editable/editable_add_test.py @@ -50,7 +50,6 @@ def test_editable_no_name_version_test_package(): tc = TestClient() tc.save({"conanfile.py": GenConanfile(), "test_package/conanfile.py": GenConanfile("test_package") - .with_class_attribute("test_type = 'explicit'") .with_test("self.output.info('Testing the package')")}) tc.run("editable add . --name=foo", assert_error=True) assert "ERROR: Editable package recipe should declare its name and version" in tc.out diff --git a/test/integration/environment/test_env.py b/test/integration/environment/test_env.py index 03bc0f2ea65..8a3aa3aedab 100644 --- a/test/integration/environment/test_env.py +++ b/test/integration/environment/test_env.py @@ -895,7 +895,6 @@ def package_info(self): from conan import ConanFile class TestTool(ConanFile): settings = "build_type" - test_type = "explicit" generators = "VirtualBuildEnv" def build_requirements(self): self.tool_requires(self.tested_reference_str) diff --git a/test/integration/graph/test_replace_requires.py b/test/integration/graph/test_replace_requires.py index 3197ba2bf95..d4499254f10 100644 --- a/test/integration/graph/test_replace_requires.py +++ b/test/integration/graph/test_replace_requires.py @@ -425,7 +425,7 @@ class App(ConanFile): settings = "build_type", "arch" requires = "openssl/0.1", {zlib} package_type = "application" - generators = "CMakeDeps", "PkgConfigDeps", "MSBuildDeps" + generators = "CMakeConfigDeps", "PkgConfigDeps", "MSBuildDeps" """) profile = textwrap.dedent(""" [settings] @@ -442,7 +442,7 @@ class App(ConanFile): c.run("create zlibng") c.run("create openssl -pr=profile") - c.run("install app -pr=profile -c tools.cmake.cmakedeps:new=will_break_next") + c.run("install app -pr=profile") assert "zlib/0.1: zlib-ng/0.1" in c.out pc_content = c.load("app/ZLIB.pc") @@ -505,7 +505,7 @@ class App(ConanFile): settings = "build_type", "arch" requires = "openssl/0.1", {zlib} package_type = "application" - generators = "CMakeDeps", "PkgConfigDeps", "MSBuildDeps" + generators = "CMakeConfigDeps", "PkgConfigDeps", "MSBuildDeps" """) profile = textwrap.dedent(""" [settings] @@ -522,7 +522,7 @@ class App(ConanFile): c.run("create zlibng") c.run("create openssl -pr=profile") - c.run("install app -pr=profile -c tools.cmake.cmakedeps:new=will_break_next") + c.run("install app -pr=profile") assert "zlib/0.1: zlib-ng/0.1" in c.out pc_content = c.load("app/ZLIB.pc") @@ -589,7 +589,7 @@ class App(ConanFile): settings = "build_type", "arch" requires = "openssl/0.1", {zlib} package_type = "application" - generators = "CMakeDeps", "PkgConfigDeps", "MSBuildDeps" + generators = "CMakeConfigDeps", "PkgConfigDeps", "MSBuildDeps" """) profile = textwrap.dedent(""" [settings] @@ -606,7 +606,7 @@ class App(ConanFile): c.run("create zlibng") c.run("create openssl -pr=profile") - c.run("install app -pr=profile -c tools.cmake.cmakedeps:new=will_break_next") + c.run("install app -pr=profile") assert "zlib/0.1: zlib-ng/0.1" in c.out pc_content = c.load("app/zlib-ng.pc") @@ -679,7 +679,7 @@ class App(ConanFile): settings = "build_type", "arch" requires = "openssl/0.1", {zlib} package_type = "application" - generators = "CMakeDeps", "PkgConfigDeps", "MSBuildDeps" + generators = "CMakeConfigDeps", "PkgConfigDeps", "MSBuildDeps" """) profile = textwrap.dedent(""" [settings] @@ -696,7 +696,7 @@ class App(ConanFile): c.run("create zlibng") c.run("create openssl -pr=profile") - c.run("install app -pr=profile -c tools.cmake.cmakedeps:new=will_break_next") + c.run("install app -pr=profile") assert "zlib/0.1: zlib-ng/0.1" in c.out pc_content = c.load("app/zlib-ng.pc") diff --git a/test/integration/lockfile/test_lock_requires.py b/test/integration/lockfile/test_lock_requires.py index 1fb8ff353de..6df9bcc119f 100644 --- a/test/integration/lockfile/test_lock_requires.py +++ b/test/integration/lockfile/test_lock_requires.py @@ -665,7 +665,7 @@ def test_conanfile_txt_deps_ranges(self, requires): def test_error_test_explicit(): # https://github.com/conan-io/conan/issues/14833 client = TestClient(light=True) - test = GenConanfile().with_test("pass").with_class_attribute("test_type = 'explicit'") + test = GenConanfile().with_test("pass") client.save({"conanfile.py": GenConanfile("pkg", "0.1"), "test_package/conanfile.py": test}) client.run("lock create conanfile.py --lockfile-out=my.lock") @@ -718,7 +718,6 @@ def requirements(self): class TestPackageConan(ConanFile): settings = "build_type" - test_type = "explicit" def requirements(self): self.requires(self.tested_reference_str) diff --git a/test/integration/package_id/package_id_requires_modes_test.py b/test/integration/package_id/package_id_requires_modes_test.py index d01fa89f657..dbffa8ad608 100644 --- a/test/integration/package_id/package_id_requires_modes_test.py +++ b/test/integration/package_id/package_id_requires_modes_test.py @@ -41,6 +41,8 @@ def test(self, mode, accepted_version, rejected_version, pattern): ("major_mode", "6ac597ffb99c3747ed78699f206dc1041537a8df"), # This is equal to semver_mode for 0.X.Y.Z.. ("full_version_mode", "13b9e753af3958dd1b2d4b3f935b04b8fb6b6760"), + ("full_recipe_mode", "13b9e753af3958dd1b2d4b3f935b04b8fb6b6760"), + ("full_package_mode", "19906d8a245d9f466d7d7f697c666222a9854a1a"), ("revision_mode", "ae5b9eeb74880aeb1cfa3db7f84c007a05ce3a76"), ("full_mode", "d1b2a9538cd69363b4bae7e66c9f900b8f4c58bb")]) def test_modes(mode, pkg_id): diff --git a/test/integration/package_id/python_requires_package_id_test.py b/test/integration/package_id/python_requires_package_id_test.py index 0ad771ae905..0bfabef1fc3 100644 --- a/test/integration/package_id/python_requires_package_id_test.py +++ b/test/integration/package_id/python_requires_package_id_test.py @@ -167,6 +167,9 @@ class Pkg(ConanFile): ("minor_mode", "c53bd9e48dd09ceeaa1bb425830490d8b243e39c"), ("major_mode", "331c17383dcdf37f79bc2b86fa55ac56afdc6fec"), ("full_version_mode", "1f0070b00ccebfec93dc90854a163c7af229f587"), + ("full_recipe_mode", "1f0070b00ccebfec93dc90854a163c7af229f587"), + # Doesn't make much sense, but was doable, not worth removing it + ("full_package_mode", "1f0070b00ccebfec93dc90854a163c7af229f587"), ("revision_mode", "0071dd0296afa0db533e21f924273485c87f0d32"), ("full_mode", "0071dd0296afa0db533e21f924273485c87f0d32")]) def test_modes(mode, pkg_id): @@ -178,3 +181,32 @@ def test_modes(mode, pkg_id): c.run("create pkg") pkgid = c.created_package_id("pkg/0.1") assert pkgid == pkg_id + + +@pytest.mark.parametrize("mode, pkg_id", + [("unrelated_mode", "da39a3ee5e6b4b0d3255bfef95601890afd80709"), + ("semver_mode", "1f0070b00ccebfec93dc90854a163c7af229f587"), + ("patch_mode", "b9ca872dfd5b48f5f1f69d66f0950fc35469d0cd"), + ("minor_mode", "c53bd9e48dd09ceeaa1bb425830490d8b243e39c"), + ("major_mode", "331c17383dcdf37f79bc2b86fa55ac56afdc6fec"), + ("full_version_mode", "1f0070b00ccebfec93dc90854a163c7af229f587"), + ("full_recipe_mode", "1f0070b00ccebfec93dc90854a163c7af229f587"), + ("revision_mode", "0071dd0296afa0db533e21f924273485c87f0d32"), + ("full_mode", "0071dd0296afa0db533e21f924273485c87f0d32")]) +def test_modes_recipe(mode, pkg_id): + c = TestClient(light=True) + + conanfile = textwrap.dedent(f""" + from conan import ConanFile + class Pkg(ConanFile): + name = "pkg" + version = "0.1" + python_requires ="dep/[*]" + def package_id(self): + self.info.python_requires.{mode}() + """) + c.save({"dep/conanfile.py": GenConanfile("dep", "0.1.1.1"), + "pkg/conanfile.py": conanfile}) + c.run("create dep") + c.run("create pkg") + c.assert_listed_binary({"pkg/0.1": (pkg_id, "Build")}) diff --git a/test/integration/remote/server_error_test.py b/test/integration/remote/server_error_test.py index e39117a6097..b7a093e9ae6 100644 --- a/test/integration/remote/server_error_test.py +++ b/test/integration/remote/server_error_test.py @@ -1,3 +1,5 @@ +from requests import Response + from conan.internal import REVISIONS from conan.test.utils.tools import TestClient, TestServer, TestRequester from collections import namedtuple @@ -54,3 +56,17 @@ def get(self, *args, **kwargs): # @UnusedVariable client.run("install --requires=pkg/ref@user/testing", assert_error=True) assert "Unexpected server response [1, 2, 3]" in client.out + + +def test_unrecongized_exception(): + class BuggyRequester(TestRequester): + def get(self, *args, **kwargs): + resp = Response() + resp.status_code = 444 + resp._content = 'some 444 error message' + return resp + + c = TestClient(default_server_user=True, requester_class=BuggyRequester) + c.run("install --requires=zlib/1.2 -r=default", assert_error=True) + assert ("ERROR: Package 'zlib/1.2' not resolved: Server exception 444:" + " some 444 error message") in c.out diff --git a/test/integration/sbom/test_cyclonedx.py b/test/integration/sbom/test_cyclonedx.py index d29744b110d..220a3cbc8aa 100644 --- a/test/integration/sbom/test_cyclonedx.py +++ b/test/integration/sbom/test_cyclonedx.py @@ -88,16 +88,16 @@ def test_sbom_generation_skipped_dependencies(self, hook_setup_post_package): # A skipped dependency also shows up in the sbom assert "pkg:conan/dep@1.0?rref=6a99f55e933fb6feeb96df134c33af44" in content - @pytest.mark.parametrize("l, n", [('"simple"', 1), ('"multi1", "multi2"', 2), - ('("tuple1", "tuple2")', 2)]) - def test_multi_license(self, hook_setup_post_package, l, n): + @pytest.mark.parametrize("lic, n", [('"simple"', 1), ('"multi1", "multi2"', 2), + ('("tuple1", "tuple2")', 2)]) + def test_multi_license(self, hook_setup_post_package, lic, n): tc = hook_setup_post_package conanfile = textwrap.dedent(f""" from conan import ConanFile class HelloConan(ConanFile): name = 'foo' version = '1.0' - license = {l} + license = {lic} """) tc.save({"conanfile.py": conanfile}) tc.run("create .") @@ -106,16 +106,16 @@ class HelloConan(ConanFile): content = json.loads(tc.load(cyclone_path)) assert len(content["components"][0]["licenses"]) == n - @pytest.mark.parametrize("l, keys", [('"Mit"', ["id"]), ('"custom_license name"', ["name"]), - ('("mIT", "custom")', ["id", "name"])]) - def test_license_spdx_valid(self, hook_setup_post_package, l, keys): + @pytest.mark.parametrize("lic, keys", [('"Mit"', ["id"]), ('"custom_license name"', ["name"]), + ('("mIT", "custom")', ["id", "name"])]) + def test_license_spdx_valid(self, hook_setup_post_package, lic, keys): tc = hook_setup_post_package conanfile = textwrap.dedent(f""" from conan import ConanFile class HelloConan(ConanFile): name = 'foo' version = '1.0' - license = {l} + license = {lic} """) tc.save({"conanfile.py": conanfile}) tc.run("create .") diff --git a/test/integration/workspace/test_workspace.py b/test/integration/workspace/test_workspace.py index edbeaa6aea0..3bbf6955a34 100644 --- a/test/integration/workspace/test_workspace.py +++ b/test/integration/workspace/test_workspace.py @@ -581,7 +581,8 @@ def packages(self): result = [] for f in os.listdir(self.folder): if os.path.isdir(os.path.join(self.folder, f)): - result.append({"path": f, "ref": f"{f}/0.1"}) + result.append({"path": f, "ref": f"{f}/0.1", + "output_folder": f"{f}/myout"}) return result """) c = TestClient(light=True) @@ -590,6 +591,7 @@ def packages(self): c.run("workspace info") assert "pkga/0.1" in c.out c.run("workspace build") + assert "metadata" in os.listdir(os.path.join(c.current_folder, "pkga", "myout")) assert "conanfile.py (pkga/0.1): Building pkga AND 0.1!!!" in c.out def test_build_with_external_editable_python_requires(self): diff --git a/test/unittests/tools/system/python_manager_test.py b/test/unittests/tools/system/python_manager_test.py index 7b969887171..7c52d081b65 100644 --- a/test/unittests/tools/system/python_manager_test.py +++ b/test/unittests/tools/system/python_manager_test.py @@ -12,8 +12,13 @@ def test_pyenv_conf(mock_shutil_which): conanfile.settings = Settings() conanfile.conf.define("tools.system.pyenv:python_interpreter", "/python/interpreter/from/config") + + def fake_run(command, win_bash=False, subsystem=None, env=None, ignore_errors=False, # noqa + quiet=False): # noqa + assert "/python/interpreter/from/config" in command + + conanfile.run = fake_run PyEnv(conanfile, "testenv") - conanfile.run = None mock_shutil_which.assert_not_called() @@ -23,8 +28,13 @@ def test_pyenv_deprecated_conf(mock_shutil_which): conanfile.settings = Settings() conanfile.conf.define("tools.system.pipenv:python_interpreter", "/python/interpreter/from/config") + + def fake_run(command, win_bash=False, subsystem=None, env=None, ignore_errors=False, # noqa + quiet=False): # noqa + assert "/python/interpreter/from/config" in command + + conanfile.run = fake_run PyEnv(conanfile, "testenv") - conanfile.run = None mock_shutil_which.assert_not_called() From c5e278feee15760a76dcdac320e24f90a2ed226a Mon Sep 17 00:00:00 2001 From: James Date: Tue, 10 Mar 2026 10:00:36 +0100 Subject: [PATCH 044/110] Tests/slow (#19720) * wip * conditional on comment * fix * failing test * wip * wip * more slows --------- Co-authored-by: Carlos Zoido --- .github/workflows/linux-tests.yml | 6 ++++- .github/workflows/main.yml | 26 +++++++++++++++++++ .github/workflows/osx-tests.yml | 6 ++++- .github/workflows/win-tests.yml | 6 ++++- pytest.ini | 1 + .../toolchains/android/test_using_cmake.py | 1 + .../toolchains/emscripten/test_emcc.py | 4 +++ .../toolchains/gnu/autotools/test_win_bash.py | 1 + .../gnu/test_v2_autotools_template.py | 1 + .../toolchains/google/test_bazel.py | 7 ++++- .../test_bazeltoolchain_cross_compilation.py | 1 + .../toolchains/ios/test_using_cmake.py | 1 + .../toolchains/test_nmake_toolchain.py | 2 ++ test/functional/toolchains/test_premake.py | 6 +++++ 14 files changed, 65 insertions(+), 4 deletions(-) diff --git a/.github/workflows/linux-tests.yml b/.github/workflows/linux-tests.yml index 95b9fd36366..a95669f2300 100644 --- a/.github/workflows/linux-tests.yml +++ b/.github/workflows/linux-tests.yml @@ -6,6 +6,10 @@ on: python-versions: required: true type: string + run-slow-tests: + required: false + default: 'false' + type: string jobs: build_container: @@ -91,7 +95,7 @@ jobs: with: python-version: ${{ matrix.python-version }} test-type: ${{ matrix.test-type }} - tests: test/${{ matrix.test-type }} + tests: ${{ matrix.test-type == 'functional' && (inputs.run-slow-tests == 'true' && 'test/functional' || 'test/functional -m "not slow"') || format('test/{0}', matrix.test-type) }} linux_runner_tests: needs: build_container diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1e8abfe2e16..811d24649fe 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -35,8 +35,22 @@ jobs: outputs: python_versions_linux_windows: ${{ steps.set_versions.outputs.python_versions_linux_windows }} python_versions_macos: ${{ steps.set_versions.outputs.python_versions_macos }} + run_slow_tests: ${{ steps.set_versions.outputs.run_slow_tests }} name: Determine Python versions steps: + - name: Fetch current PR body and check for slow-tests marker + if: github.event_name == 'pull_request' + id: fetch_pr + run: | + BODY=$(curl -s -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ + "https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}" \ + | jq -r '.body // ""') + if echo "$BODY" | grep -Fq '$runslowtests'; then + echo "run_slow_tests=true" >> $GITHUB_OUTPUT + else + echo "run_slow_tests=false" >> $GITHUB_OUTPUT + fi + - name: Determine Python versions id: set_versions run: | @@ -48,12 +62,22 @@ jobs: echo "python_versions_macos=['3.10']" >> $GITHUB_OUTPUT fi + # Run slow tests on develop2, or when PR description contains $runslowtests (checked in fetch_pr step) + if [[ "${{ github.ref }}" == "refs/heads/develop2" ]]; then + echo "run_slow_tests=true" >> $GITHUB_OUTPUT + elif [[ "${{ github.event_name }}" == "pull_request" ]]; then + echo "run_slow_tests=${{ steps.fetch_pr.outputs.run_slow_tests }}" >> $GITHUB_OUTPUT + else + echo "run_slow_tests=false" >> $GITHUB_OUTPUT + fi + linux_suite: needs: [ensure_latest_tag_merged, set_python_versions] uses: ./.github/workflows/linux-tests.yml name: Linux test suite with: python-versions: ${{ needs.set_python_versions.outputs.python_versions_linux_windows }} + run-slow-tests: ${{ needs.set_python_versions.outputs.run_slow_tests }} osx_suite: needs: [ensure_latest_tag_merged, set_python_versions] @@ -61,6 +85,7 @@ jobs: name: OSX test suite with: python-versions: ${{ needs.set_python_versions.outputs.python_versions_macos }} + run-slow-tests: ${{ needs.set_python_versions.outputs.run_slow_tests }} windows_suite: needs: [ensure_latest_tag_merged, set_python_versions] @@ -68,6 +93,7 @@ jobs: name: Windows test suite with: python-versions: ${{ needs.set_python_versions.outputs.python_versions_linux_windows }} + run-slow-tests: ${{ needs.set_python_versions.outputs.run_slow_tests }} code_coverage: runs-on: ubuntu-latest diff --git a/.github/workflows/osx-tests.yml b/.github/workflows/osx-tests.yml index b98cb026f2b..e44c3c90eba 100644 --- a/.github/workflows/osx-tests.yml +++ b/.github/workflows/osx-tests.yml @@ -6,6 +6,10 @@ on: python-versions: required: true type: string + run-slow-tests: + required: false + default: 'false' + type: string jobs: osx_setup: @@ -177,4 +181,4 @@ jobs: with: python-version: ${{ matrix.python-version }} test-type: ${{ matrix.test-type }} - tests: test/${{ matrix.test-type }} + tests: ${{ matrix.test-type == 'functional' && (inputs.run-slow-tests == 'true' && 'test/functional' || 'test/functional -m "not slow"') || format('test/{0}', matrix.test-type) }} diff --git a/.github/workflows/win-tests.yml b/.github/workflows/win-tests.yml index 0cf044b2522..9b76a55cdba 100644 --- a/.github/workflows/win-tests.yml +++ b/.github/workflows/win-tests.yml @@ -6,6 +6,10 @@ on: python-versions: required: true type: string + run-slow-tests: + required: false + default: 'false' + type: string jobs: unit_integration_tests: @@ -255,4 +259,4 @@ jobs: with: python-version: ${{ matrix.python-version }} test-type: functional - tests: test/functional + tests: ${{ inputs.run-slow-tests == 'true' && 'test/functional' || 'test/functional -m "not slow"' }} diff --git a/pytest.ini b/pytest.ini index bf8bb6be5d0..275c77374c0 100644 --- a/pytest.ini +++ b/pytest.ini @@ -4,5 +4,6 @@ testpaths = 'test' markers = docker_runner: Mark tests that require Docker to run. artifactory_ready: These tests can be run against a full Artifactory + slow: These tests can executed more occasionally, slow, but unlikely to fail in normal PRs filterwarnings = ignore:'cgi' is deprecated:DeprecationWarning diff --git a/test/functional/toolchains/android/test_using_cmake.py b/test/functional/toolchains/android/test_using_cmake.py index 3b09d62c827..bf650e1289f 100644 --- a/test/functional/toolchains/android/test_using_cmake.py +++ b/test/functional/toolchains/android/test_using_cmake.py @@ -8,6 +8,7 @@ from conan.test.utils.tools import TestClient +@pytest.mark.slow @pytest.mark.tool("cmake", "3.23") # Android complains if <3.19 @pytest.mark.tool("ninja") # so it easily works in Windows too @pytest.mark.tool("android_ndk") diff --git a/test/functional/toolchains/emscripten/test_emcc.py b/test/functional/toolchains/emscripten/test_emcc.py index f68646fdc54..62402d53cc0 100644 --- a/test/functional/toolchains/emscripten/test_emcc.py +++ b/test/functional/toolchains/emscripten/test_emcc.py @@ -79,6 +79,7 @@ ) +@pytest.mark.slow @pytest.mark.tool("cmake") @pytest.mark.tool("emcc") @pytest.mark.tool("node") @@ -106,6 +107,7 @@ def test_cmake_emscripten(): assert "Hello World Release!" in client.out +@pytest.mark.slow @pytest.mark.tool("meson") @pytest.mark.tool("emcc") @pytest.mark.tool("node") @@ -138,6 +140,7 @@ def test_meson_emscripten(): assert "Hello World Release!" in client.out +@pytest.mark.slow @pytest.mark.tool("autotools") @pytest.mark.tool("emcc") @pytest.mark.tool("node") @@ -168,6 +171,7 @@ def test_autotools_emscripten(): assert "Hello World Release!" in client.out +@pytest.mark.slow @pytest.mark.tool("premake") @pytest.mark.tool("emcc") @pytest.mark.tool("node") diff --git a/test/functional/toolchains/gnu/autotools/test_win_bash.py b/test/functional/toolchains/gnu/autotools/test_win_bash.py index 455acc79b3e..e98059317a4 100644 --- a/test/functional/toolchains/gnu/autotools/test_win_bash.py +++ b/test/functional/toolchains/gnu/autotools/test_win_bash.py @@ -64,6 +64,7 @@ def build(self): assert "conanvcvars.bat" in bat_contents +@pytest.mark.slow @pytest.mark.skipif(platform.system() != "Windows", reason="Requires Windows") @pytest.mark.tool("msys2") @pytest.mark.tool("clang", "20") diff --git a/test/functional/toolchains/gnu/test_v2_autotools_template.py b/test/functional/toolchains/gnu/test_v2_autotools_template.py index 0f6c3092451..e2db1fc6be3 100644 --- a/test/functional/toolchains/gnu/test_v2_autotools_template.py +++ b/test/functional/toolchains/gnu/test_v2_autotools_template.py @@ -347,6 +347,7 @@ def package_info(self): assert "Bye, bye!" in client.out +@pytest.mark.slow @pytest.mark.skipif(platform.system() != "Windows", reason="Using msys2") @pytest.mark.tool("msys2") class TestAutotoolsTemplateWindows: diff --git a/test/functional/toolchains/google/test_bazel.py b/test/functional/toolchains/google/test_bazel.py index c9d28c47220..e349a5b77ba 100644 --- a/test/functional/toolchains/google/test_bazel.py +++ b/test/functional/toolchains/google/test_bazel.py @@ -38,6 +38,7 @@ def base_profile(): """) +@pytest.mark.slow @pytest.mark.parametrize("build_type", ["Debug", "Release", "RelWithDebInfo", "MinSizeRel"]) @pytest.mark.tool("bazel", "6.x") def test_basic_exe_6x(bazelrc, build_type, base_profile, bazel_output_root_dir): @@ -56,6 +57,7 @@ def test_basic_exe_6x(bazelrc, build_type, base_profile, bazel_output_root_dir): assert "myapp/1.0: Hello World Debug!" in client.out +@pytest.mark.slow @pytest.mark.parametrize("build_type", ["Debug", "Release", "RelWithDebInfo", "MinSizeRel"]) @pytest.mark.tool("bazel", "7.x") def test_basic_exe(bazelrc, build_type, base_profile, bazel_output_root_dir): @@ -74,6 +76,7 @@ def test_basic_exe(bazelrc, build_type, base_profile, bazel_output_root_dir): assert "myapp/1.0: Hello World Debug!" in client.out +@pytest.mark.slow @pytest.mark.tool("bazel", "8.x") def test_basic_lib(bazelrc, base_profile, bazel_output_root_dir): """ @@ -84,7 +87,7 @@ def test_basic_lib(bazelrc, base_profile, bazel_output_root_dir): client.run("create .") assert "mylib/1.0: Hello World Release!" in client.out - +@pytest.mark.slow @pytest.mark.parametrize("shared", [False, True]) @pytest.mark.tool("bazel", "6.x") def test_transitive_libs_consuming_6x(shared, bazel_output_root_dir): @@ -211,6 +214,7 @@ def test_transitive_libs_consuming_6x(shared, bazel_output_root_dir): assert "myfirstlib/1.2.11: Hello World Release!" +@pytest.mark.slow @pytest.mark.parametrize("shared", [False, True]) @pytest.mark.tool("bazel", "7.x") @pytest.mark.skipif(platform.system() == "Linux", @@ -344,6 +348,7 @@ def test_transitive_libs_consuming_7x(shared, bazel_output_root_dir): assert "myfirstlib/1.2.11: Hello World Release!" +@pytest.mark.slow @pytest.mark.tool("bazel", "8.x") def test_empty_bazel_query(): """ diff --git a/test/functional/toolchains/google/test_bazeltoolchain_cross_compilation.py b/test/functional/toolchains/google/test_bazeltoolchain_cross_compilation.py index e848e82e5be..95feba6363e 100644 --- a/test/functional/toolchains/google/test_bazeltoolchain_cross_compilation.py +++ b/test/functional/toolchains/google/test_bazeltoolchain_cross_compilation.py @@ -9,6 +9,7 @@ from conan.test.utils.tools import TestClient +@pytest.mark.slow @pytest.mark.skipif(platform.system() != "Darwin", reason="Only for Darwin") @pytest.mark.tool("bazel", "6.x") # not working for Bazel 7.x def test_bazel_simple_cross_compilation(): diff --git a/test/functional/toolchains/ios/test_using_cmake.py b/test/functional/toolchains/ios/test_using_cmake.py index 203d85f43c8..de78d4601f9 100644 --- a/test/functional/toolchains/ios/test_using_cmake.py +++ b/test/functional/toolchains/ios/test_using_cmake.py @@ -8,6 +8,7 @@ from ._utils import create_library +@pytest.mark.slow @pytest.mark.skipif(platform.system() != "Darwin", reason="Requires XCode") @pytest.mark.tool("cmake", "3.19") def test_xcode_ios_generator(): diff --git a/test/functional/toolchains/test_nmake_toolchain.py b/test/functional/toolchains/test_nmake_toolchain.py index b1267378c57..3d4e32acadc 100644 --- a/test/functional/toolchains/test_nmake_toolchain.py +++ b/test/functional/toolchains/test_nmake_toolchain.py @@ -8,6 +8,7 @@ from conan.test.utils.tools import TestClient +@pytest.mark.slow @pytest.mark.parametrize( "compiler, version, runtime, cppstd, build_type, defines, cflags, cxxflags, sharedlinkflags, exelinkflags", [ @@ -89,6 +90,7 @@ def build(self): conf_preprocessors) +@pytest.mark.slow @pytest.mark.skipif(platform.system() != "Windows", reason="Requires Windows") @pytest.mark.tool("cmake", "3.23") # This test uses clang inside Visual Studio, not managed by mark.tool diff --git a/test/functional/toolchains/test_premake.py b/test/functional/toolchains/test_premake.py index e9022008090..8f8155fe455 100644 --- a/test/functional/toolchains/test_premake.py +++ b/test/functional/toolchains/test_premake.py @@ -11,6 +11,7 @@ from conan.test.assets.sources import gen_function_cpp +@pytest.mark.slow @pytest.mark.skipif(platform.machine() not in ("x86_64", "AMD64"), reason="Premake Legacy generator only supports x86_64 machines") @pytest.mark.tool("premake") @@ -77,6 +78,7 @@ def build(self): assert "matrix/1.0: Hello World Debug!" in c.out +@pytest.mark.slow @pytest.mark.tool("premake") def test_premake_new_generator(): c = TestClient() @@ -90,6 +92,7 @@ def test_premake_new_generator(): assert "example/1.0: Hello World Release!" in c.out +@pytest.mark.slow @pytest.mark.tool("premake") def test_premake_shared_lib(): c = TestClient() @@ -99,6 +102,7 @@ def test_premake_shared_lib(): assert "lib/0.1: package(): Packaged 1 '.a' file: liblib.a" not in c.out +@pytest.mark.slow @pytest.mark.tool("premake") @pytest.mark.parametrize("transitive_libs", [True, False]) def test_premake_components(transitive_libs): @@ -202,6 +206,7 @@ def package_info(self): c.run("build consumer", assert_error=not transitive_libs) +@pytest.mark.slow @pytest.mark.tool("premake") def test_transitive_headers_not_public(transitive_libraries): c = transitive_libraries @@ -255,6 +260,7 @@ def build(self): # Error should be about not finding matrix +@pytest.mark.slow @pytest.mark.tool("premake") def test_premake_custom_configuration(transitive_libraries): c = transitive_libraries From aa4addca641ca8408ae1ac024b3945a94cd168f1 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 11 Mar 2026 07:31:36 +0100 Subject: [PATCH 045/110] Fix/source creds msg (#19737) * improve source creds msg * improve source creds msg --- conan/api/subapi/upload.py | 4 ++-- conan/internal/rest/caching_file_downloader.py | 6 +++--- test/integration/cache/backup_sources_test.py | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/conan/api/subapi/upload.py b/conan/api/subapi/upload.py index 752d8c68c77..53eeb38d7fc 100644 --- a/conan/api/subapi/upload.py +++ b/conan/api/subapi/upload.py @@ -183,7 +183,7 @@ def upload_backup_sources(self, files: List) -> None: "Skipping updating file but continuing with upload. " f"Missing permissions?: {e}") else: - raise ConanException(f"The source backup server '{url}' needs authentication" - f"/permissions, please provide 'source_credentials.json': {e}") + raise ConanException(f"Authentication to source backup server '{url}' failed, " + f"please check your 'source_credentials.json': {e}") output.success("Upload backup sources complete\n") diff --git a/conan/internal/rest/caching_file_downloader.py b/conan/internal/rest/caching_file_downloader.py index b2b50a8522b..297a0698d21 100644 --- a/conan/internal/rest/caching_file_downloader.py +++ b/conan/internal/rest/caching_file_downloader.py @@ -121,9 +121,9 @@ def _backup_download(self, backup_url, backups_urls, sha256, cached_path, urls, else: self._output.warning(f"File {urls} not found in {backup_url}") except (AuthenticationException, ForbiddenException) as e: - raise ConanException(f"The source backup server '{backup_url}' " - f"needs authentication: {e}. " - f"Please provide 'source_credentials.json'") + raise ConanException(f"Authentication to source backup server '{backup_url}' " + f"failed: {e}. " + f"Please check your 'source_credentials.json'") def _download_from_urls(self, urls, file_path, retry, retry_wait, verify_ssl, auth, headers, md5, sha1, sha256): diff --git a/test/integration/cache/backup_sources_test.py b/test/integration/cache/backup_sources_test.py index c0b6f66e44c..c00c7de750f 100644 --- a/test/integration/cache/backup_sources_test.py +++ b/test/integration/cache/backup_sources_test.py @@ -414,8 +414,8 @@ def source(self): client.save({"conanfile.py": conanfile}) client.run("create .", assert_error=True) - assert f"ConanException: The source backup server '{http_server.fake_url}" \ - f"/downloader/' needs authentication" in client.out + assert f"ConanException: Authentication to source backup server '{http_server.fake_url}" \ + f"/downloader/' failed" in client.out content = {"credentials": [ {"url": f"{http_server.fake_url}", "token": "mytoken"} ]} @@ -424,8 +424,8 @@ def source(self): client.run("create .") assert "CONTENT: Hello, world!" in client.out client.run("upload * -c -r=default", assert_error=True) - assert f"The source backup server '{http_server.fake_url}" \ - f"/uploader/' needs authentication" in client.out + assert f"Authentication to source backup server '{http_server.fake_url}" \ + f"/uploader/' failed" in client.out content = {"credentials": [ {"url": f"{http_server.fake_url}", "token": "myuploadtoken"} ]} From 488308aa84343c9cc7b3fec47a5a32198bd55830 Mon Sep 17 00:00:00 2001 From: stevenwdv Date: Wed, 11 Mar 2026 10:34:56 +0100 Subject: [PATCH 046/110] Fix detect_emcc_compiler (#19735) on first run & on Windows See #19677 --- conan/internal/api/detect/detect_api.py | 25 +++++++++++-------------- test/unittests/util/detect_test.py | 4 ++++ 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/conan/internal/api/detect/detect_api.py b/conan/internal/api/detect/detect_api.py index 6148d9cbdb4..e39b81132ac 100644 --- a/conan/internal/api/detect/detect_api.py +++ b/conan/internal/api/detect/detect_api.py @@ -616,21 +616,18 @@ def detect_cl_compiler(compiler_exe="cl"): def detect_emcc_compiler(compiler_exe="emcc"): - try: - ret, out = detect_runner(f'"{compiler_exe}" --version') - if ret != 0: - return None, None, None - first_line = out.splitlines()[0] - if "Emscripten" not in first_line: - return None, None, None - compiler = "emcc" - version_match = re.search(r"[0-9]+\.[0-9]+\.[0-9]+", first_line) - if version_match: - version = version_match.group() - ConanOutput(scope="detect_api").info("Found %s %s" % (compiler, version)) - return compiler, Version(version), compiler_exe - except (Exception,): # to disable broad-except + ret, out = detect_runner(f'"{compiler_exe}" --version') + if ret != 0: + return None, None, None + if "Emscripten" not in out: + return None, None, None + compiler = "emcc" + version_match = re.search(r"[0-9]+\.[0-9]+\.[0-9]+", out) + if not version_match: return None, None, None + version = version_match.group() + ConanOutput(scope="detect_api").info("Found %s %s" % (compiler, version)) + return compiler, Version(version), compiler_exe def default_compiler_version(compiler, version): diff --git a/test/unittests/util/detect_test.py b/test/unittests/util/detect_test.py index 62cb4aac291..17d478ef5d2 100644 --- a/test/unittests/util/detect_test.py +++ b/test/unittests/util/detect_test.py @@ -91,6 +91,10 @@ def test_detect_cc_versioning(detect_runner_mock, version_return, expected_versi [detect_emcc_compiler, "emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 4.0.22 (0f3d2e62bccf8e14497ff19e05a1202c51eb0c65)", ('emcc', Version("4.0.22"), 'emcc')], + [detect_emcc_compiler, + "shared:INFO: (Emscripten: Running sanity checks)\n" + + "emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 4.0.22 (0f3d2e62bccf8e14497ff19e05a1202c51eb0c65)", + ('emcc', Version("4.0.22"), 'emcc')], ]) def test_detect_compiler(function, version_return, expected_version): """ From 37b9e8201bd421992c9455b8794614565c3e9395 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 11 Mar 2026 14:31:55 +0100 Subject: [PATCH 047/110] remove alias support (#19740) --- conan/internal/graph/graph_builder.py | 4 ++++ test/functional/revisions_test.py | 4 ++++ test/integration/command/alias_test.py | 3 +++ test/integration/graph/core/test_alias.py | 3 +++ test/integration/lockfile/test_lock_alias.py | 3 +++ 5 files changed, 17 insertions(+) diff --git a/conan/internal/graph/graph_builder.py b/conan/internal/graph/graph_builder.py index c3638a8bd11..993a4e2fd27 100644 --- a/conan/internal/graph/graph_builder.py +++ b/conan/internal/graph/graph_builder.py @@ -1,3 +1,4 @@ +import os from collections import deque from conan.internal.cache.conan_reference_layout import BasicLayout @@ -20,6 +21,7 @@ class DepsGraphBuilder: + ALLOW_ALIAS = False def __init__(self, proxy, loader, resolver, cache, remotes, update, check_update, global_conf): self._proxy = proxy @@ -219,6 +221,8 @@ def _initialize_requires(self, node, graph, graph_lock, profile_build, profile_h result.append(require) alias = require.alias # alias needs to be processed this early if alias is not None: + if not DepsGraphBuilder.ALLOW_ALIAS and os.getenv("CONAN_ALLOW_ALIAS") != "will_break_next": + raise ConanException(f"Alias requirements have been removed: '{node}' requiring: '{alias}'") resolved = False if graph_lock is not None: resolved = graph_lock.replace_alias(require, alias) diff --git a/test/functional/revisions_test.py b/test/functional/revisions_test.py index 8b699af35f6..6c8bc4d9adb 100644 --- a/test/functional/revisions_test.py +++ b/test/functional/revisions_test.py @@ -5,6 +5,7 @@ import pytest from unittest.mock import patch +from conan.internal.graph.graph_builder import DepsGraphBuilder from conan.test.utils.env import environment_update from conan.internal.errors import RecipeNotFoundException from conan.api.model import RecipeReference @@ -24,6 +25,9 @@ def _create(c_v2, ref, conanfile=None, args=None, assert_error=False): return pref +DepsGraphBuilder.ALLOW_ALIAS = True + + @pytest.mark.artifactory_ready class TestInstallingPackagesWithRevisions: diff --git a/test/integration/command/alias_test.py b/test/integration/command/alias_test.py index c43a707e917..2f94df5e65f 100644 --- a/test/integration/command/alias_test.py +++ b/test/integration/command/alias_test.py @@ -2,8 +2,11 @@ import textwrap from conan.api.model import RecipeReference +from conan.internal.graph.graph_builder import DepsGraphBuilder from conan.test.utils.tools import TestClient, GenConanfile +DepsGraphBuilder.ALLOW_ALIAS = True + class TestConanAlias: diff --git a/test/integration/graph/core/test_alias.py b/test/integration/graph/core/test_alias.py index bf7eb87b0cf..4c1df85fa6d 100644 --- a/test/integration/graph/core/test_alias.py +++ b/test/integration/graph/core/test_alias.py @@ -1,8 +1,11 @@ +from conan.internal.graph.graph_builder import DepsGraphBuilder from conan.test.assets.genconanfile import GenConanfile from test.integration.graph.core.graph_manager_base import GraphManagerTest from test.integration.graph.core.graph_manager_test import _check_transitive from conan.test.utils.tools import TestClient +DepsGraphBuilder.ALLOW_ALIAS = True + class TestAlias(GraphManagerTest): diff --git a/test/integration/lockfile/test_lock_alias.py b/test/integration/lockfile/test_lock_alias.py index 0b2752a0064..c37d0746896 100644 --- a/test/integration/lockfile/test_lock_alias.py +++ b/test/integration/lockfile/test_lock_alias.py @@ -2,9 +2,12 @@ import pytest +from conan.internal.graph.graph_builder import DepsGraphBuilder from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient +DepsGraphBuilder.ALLOW_ALIAS = True + @pytest.mark.parametrize("requires", ["requires", "tool_requires"]) def test_conanfile_txt_deps_ranges(requires): From 0115fa1906930b88199b266f6bf10c3deb7a54e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=A1nchez=20Falero?= Date: Fri, 13 Mar 2026 12:29:59 +0100 Subject: [PATCH 048/110] PyEnv output based on verbosity level (#19731) * PyEnv output based on verbosity level * wip * wip * wip * wip --- conan/tools/system/python_manager.py | 51 +++++++++++++++++-- .../tools/system/python_manager_test.py | 22 ++++++++ .../tools/system/python_manager_test.py | 47 +++++++++++++++++ 3 files changed, 115 insertions(+), 5 deletions(-) diff --git a/conan/tools/system/python_manager.py b/conan/tools/system/python_manager.py index 62b6ac93da8..1cccf1359e0 100644 --- a/conan/tools/system/python_manager.py +++ b/conan/tools/system/python_manager.py @@ -7,11 +7,42 @@ from conan.tools.env.environment import Environment from conan.errors import ConanException -from conan.api.output import ConanOutput +from conan.api.output import ( + ConanOutput, + LEVEL_QUIET, + LEVEL_ERROR, + LEVEL_WARNING, + LEVEL_STATUS, + LEVEL_VERBOSE, + LEVEL_DEBUG, + LEVEL_TRACE, +) from conan.internal.util.files import rmdir +def _get_pip_verbosity(): + return { + LEVEL_QUIET: "-qqq", + LEVEL_ERROR: "-qq", + LEVEL_WARNING: "-q", + LEVEL_VERBOSE: "-v", + LEVEL_DEBUG: "-vv", + LEVEL_TRACE: "-vvv", + }.get(ConanOutput.get_output_level(), "") + + +def _get_uv_verbosity(): + return { + LEVEL_QUIET: "-qq", + LEVEL_ERROR: "-qq", + LEVEL_WARNING: "-q", + LEVEL_VERBOSE: "--verbose", + LEVEL_DEBUG: "--verbose", + LEVEL_TRACE: "--verbose", + }.get(ConanOutput.get_output_level(), "") + + class PyEnv: def __init__(self, conanfile, folder=None, name="", py_version=None): @@ -97,6 +128,9 @@ def install(self, packages, pip_args=None): :return: the return code of the executed pip command. """ args = [self.env_exe, "-m", "pip", "install", "--disable-pip-version-check"] + pip_verbosity = _get_pip_verbosity() + if pip_verbosity: + args.append(pip_verbosity) if pip_args: args.extend(pip_args) args += [f'"{p}"' for p in packages] @@ -124,12 +158,19 @@ def _create_uv_venv(self, base_env_dir, py_version): ) python_exe = self._get_env_python(uv_env_dir) - self._conanfile.run(cmd_args_to_string( - [python_exe, "-m", "pip", "install", "--disable-pip-version-check", "uv"]) - ) + pip_args = [python_exe, "-m", "pip", "install", "--disable-pip-version-check"] + pip_verbosity = _get_pip_verbosity() + if pip_verbosity: + pip_args.append(pip_verbosity) + pip_args.append("uv") + self._conanfile.run(cmd_args_to_string(pip_args)) uv_cmd = [python_exe, "-m", "uv"] - self._conanfile.run(cmd_args_to_string(uv_cmd + ['venv', '--seed', '--python', py_version, self._env_dir])) + uv_venv_args = uv_cmd + ['venv', '--seed', '--python', py_version, self._env_dir] + uv_verbosity = _get_uv_verbosity() + if uv_verbosity: + uv_venv_args.append(uv_verbosity) + self._conanfile.run(cmd_args_to_string(uv_venv_args)) self._conanfile.output.info(f"Virtual environment for Python " f"{py_version} created successfully using UV.") except Exception as e: diff --git a/test/functional/tools/system/python_manager_test.py b/test/functional/tools/system/python_manager_test.py index 030ef5cb0be..21997af7af6 100644 --- a/test/functional/tools/system/python_manager_test.py +++ b/test/functional/tools/system/python_manager_test.py @@ -89,6 +89,9 @@ def build(self): assert "Found existing installation: hello 0.1.0" in client.out assert "Hello Test World!" in client.out + client.run("build pip -verror") + assert "Found existing installation" not in client.out + def test_install_version_range(): c = TestClient(path_with_spaces=False) @@ -343,6 +346,25 @@ def build(self): assert "Hello Test World!" in client.out +@pytest.mark.parametrize("verbosity", ["-verror", "-vstatus"]) +def test_pyenv_install_error_always_shown(verbosity): + conanfile_pyenv = textwrap.dedent(""" + from conan import ConanFile + from conan.tools.system import PyEnv + + class PyenvPackage(ConanFile): + def generate(self): + pyenv = PyEnv(self) + pyenv.install(["package_does_not_exist"]) + """) + + client = TestClient(path_with_spaces=False) + client.save({"conanfile.py": conanfile_pyenv}) + client.run(f"build . {verbosity}", assert_error=True) + assert "package_does_not_exist" in client.out + assert "ERROR" in client.out + + def test_cmake_toolchain_configure_find_python(): client = TestClient(path_with_spaces=False) conanfile = textwrap.dedent(""" diff --git a/test/unittests/tools/system/python_manager_test.py b/test/unittests/tools/system/python_manager_test.py index 7c52d081b65..a0719308fd1 100644 --- a/test/unittests/tools/system/python_manager_test.py +++ b/test/unittests/tools/system/python_manager_test.py @@ -1,6 +1,8 @@ from conan.tools.system import PyEnv from unittest.mock import patch import pytest +from conan.api.output import ConanOutput, LEVEL_QUIET, LEVEL_ERROR, LEVEL_WARNING, \ + LEVEL_STATUS, LEVEL_VERBOSE, LEVEL_DEBUG, LEVEL_TRACE from conan.errors import ConanException from conan.internal.model.settings import Settings from conan.test.utils.mocks import ConanFileMock @@ -62,3 +64,48 @@ def fake_run(command, win_bash=False, subsystem=None, env=None, ignore_errors=Fa with pytest.raises(ConanException) as exc_info: PyEnv(conanfile, "testenv") assert "using '/python/interpreter/from/config': fake error message" in exc_info.value.args[0] + + +@pytest.mark.parametrize("level, expected_pip_flag", [ + (LEVEL_QUIET, "-qqq"), + (LEVEL_ERROR, "-qq"), + (LEVEL_WARNING, "-q"), + (LEVEL_STATUS, None), + (LEVEL_VERBOSE, "-v"), + (LEVEL_DEBUG, "-vv"), + (LEVEL_TRACE, "-vvv"), +]) +def test_pyenv_pip_verbosity(level, expected_pip_flag): + """ + https://github.com/conan-io/conan/issues/19729 + PyEnv.install() should map Conan verbosity levels to pip's native -q/-v flags. + """ + conanfile = ConanFileMock() + conanfile.settings = Settings() + conanfile.conf.define("tools.system.pyenv:python_interpreter", + "/python/interpreter/from/config") + + calls = [] + + def fake_run(command, win_bash=False, subsystem=None, env=None, ignore_errors=False, # noqa + quiet=False): # noqa + calls.append(command) + + conanfile.run = fake_run + + old_level = ConanOutput.get_output_level() + try: + ConanOutput.set_output_level(level) + pyenv = PyEnv(conanfile, f"testenv_{level}") + calls.clear() + + pyenv.install(["some_package"]) + assert len(calls) == 1 + assert "pip install" in calls[0] + if expected_pip_flag: + assert f" {expected_pip_flag} " in calls[0] + else: + assert " -q" not in calls[0] + assert " -v " not in calls[0] + finally: + ConanOutput.set_output_level(old_level) From 995eadcea80b346bf2717fc909823e43e375c6be Mon Sep 17 00:00:00 2001 From: James Date: Fri, 13 Mar 2026 12:31:48 +0100 Subject: [PATCH 049/110] Feature/build arg context (pure refactor) (#19746) * build arg context * wip * wip --- conan/internal/graph/build_mode.py | 41 +++++++++++-------- conan/internal/graph/graph_binaries.py | 5 --- .../command/install/install_cascade_test.py | 1 + .../unittests/client/graph/build_mode_test.py | 17 -------- 4 files changed, 26 insertions(+), 38 deletions(-) diff --git a/conan/internal/graph/build_mode.py b/conan/internal/graph/build_mode.py index 042652007f8..6d964d2fcca 100644 --- a/conan/internal/graph/build_mode.py +++ b/conan/internal/graph/build_mode.py @@ -1,3 +1,4 @@ +from conan.api.output import ConanOutput from conan.errors import ConanException from conan.internal.model.recipe_ref import ref_matches @@ -9,12 +10,12 @@ class BuildMode: => ["!foo"] or ["~foo"] means exclude when building all from sources """ def __init__(self, params): - self.missing = False - self.never = False + self._missing = False + self._never = False self.cascade = False - self.editable = False - self.patterns = [] - self.build_missing_patterns = [] + self._editable = False + self._patterns = [] + self._build_missing_patterns = [] self._build_missing_excluded = [] self._build_compatible_patterns = [] self._build_compatible_excluded = [] @@ -27,20 +28,23 @@ def __init__(self, params): for param in params: if param == "missing": - self.missing = True + self._missing = True elif param == "editable": - self.editable = True + self._editable = True elif param == "never": - self.never = True + self._never = True elif param == "cascade": self.cascade = True + ConanOutput().warning("Using build-mode 'cascade' is generally inefficient and it " + "shouldn't be used. Use 'package_id' and 'package_id_modes' " + "for more efficient re-builds") else: if param.startswith("missing:"): clean_pattern = param[len("missing:"):] if clean_pattern and clean_pattern[0] in ["!", "~"]: self._build_missing_excluded.append(clean_pattern[1:]) else: - self.build_missing_patterns.append(clean_pattern) + self._build_missing_patterns.append(clean_pattern) elif param == "compatible": self._build_compatible_patterns = ["*"] elif param.startswith("compatible:"): @@ -54,11 +58,16 @@ def __init__(self, params): if clean_pattern and clean_pattern[0] in ["!", "~"]: self._excluded_patterns.append(clean_pattern[1:]) else: - self.patterns.append(clean_pattern) + self._patterns.append(clean_pattern) - if self.never and (self.missing or self.patterns or self.cascade): + if self._never and (self._missing or self._patterns or self.cascade): raise ConanException("--build=never not compatible with other options") + @property + def editable(self): + # we can make this conditional on the context in the future + return self._editable + def forced(self, conan_file, ref, with_deps_to_build=False): # TODO: ref can be obtained from conan_file @@ -70,7 +79,7 @@ def forced(self, conan_file, ref, with_deps_to_build=False): if conan_file.build_policy == "never": # this package has been export-pkg return False - if self.never: + if self._never: return False if conan_file.build_policy == "always": @@ -81,15 +90,15 @@ def forced(self, conan_file, ref, with_deps_to_build=False): return True # Patterns to match, if package matches pattern, build is forced - for pattern in self.patterns: + for pattern in self._patterns: if ref_matches(ref, pattern, is_consumer=conan_file._conan_is_consumer): # noqa return True return False def allowed(self, conan_file): - if self.never or conan_file.build_policy == "never": # this package has been export-pkg + if self._never or conan_file.build_policy == "never": # this package has been export-pkg return False - if self.missing: + if self._missing: return True if conan_file.build_policy == "missing": conan_file.output.info("Building package from source as defined by " @@ -119,6 +128,6 @@ def should_build_missing(self, conanfile): return False return True # If it has not been excluded by the negated patterns, it is included - for pattern in self.build_missing_patterns: + for pattern in self._build_missing_patterns: if ref_matches(conanfile.ref, pattern, is_consumer=conanfile._conan_is_consumer): # noqa return True diff --git a/conan/internal/graph/graph_binaries.py b/conan/internal/graph/graph_binaries.py index ffd361a2e84..afa14528557 100644 --- a/conan/internal/graph/graph_binaries.py +++ b/conan/internal/graph/graph_binaries.py @@ -476,11 +476,6 @@ def evaluate_graph(self, deps_graph, build_mode, lockfile, remotes, update, buil mainprefs = [str(n.pref) for n in tested_graph.nodes if n.recipe not in (RECIPE_CONSUMER, RECIPE_VIRTUAL)] - if main_mode.cascade: - ConanOutput().warning("Using build-mode 'cascade' is generally inefficient and it " - "shouldn't be used. Use 'package_id' and 'package_id_modes' for" - "more efficient re-builds") - def _evaluate_single(n): mode = main_mode if mainprefs is None or str(n.pref) in mainprefs else test_mode if lockfile: diff --git a/test/integration/command/install/install_cascade_test.py b/test/integration/command/install/install_cascade_test.py index 04114740b5e..f4f6445a900 100644 --- a/test/integration/command/install/install_cascade_test.py +++ b/test/integration/command/install/install_cascade_test.py @@ -31,6 +31,7 @@ def _assert_built(refs): # Building A everything is built c.run("install app --build=liba* --build cascade") + assert "Using build-mode 'cascade' is generally inefficient" in c.out _assert_built(["liba/1.0", "libb/1.0", "libc/1.0", "libd/1.0", "libe/1.0", "libf/1.0"]) c.run("install app --build=libd* --build cascade") diff --git a/test/unittests/client/graph/build_mode_test.py b/test/unittests/client/graph/build_mode_test.py index 7ecf1e47163..72893698940 100644 --- a/test/unittests/client/graph/build_mode_test.py +++ b/test/unittests/client/graph/build_mode_test.py @@ -26,23 +26,6 @@ def test_skip_package(conanfile): assert not build_mode.forced(conanfile, RecipeReference.loads("other/1.2")) -def test_valid_params(): - build_mode = BuildMode(["missing"]) - assert build_mode.missing is True - assert build_mode.never is False - assert build_mode.cascade is False - - build_mode = BuildMode(["never"]) - assert build_mode.missing is False - assert build_mode.never is True - assert build_mode.cascade is False - - build_mode = BuildMode(["cascade"]) - assert build_mode.missing is False - assert build_mode.never is False - assert build_mode.cascade is True - - def test_invalid_configuration(): for mode in ["missing", "cascade"]: with pytest.raises(ConanException, match=r"--build=never not compatible " From e5b01b26d10bec75ba361e4401a42bc2730feb54 Mon Sep 17 00:00:00 2001 From: James Date: Fri, 13 Mar 2026 12:37:16 +0100 Subject: [PATCH 050/110] fix platform_requires definition (#19750) --- conan/internal/api/profile/profile_loader.py | 12 ++++++++++-- test/integration/graph/test_system_tools.py | 20 ++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/conan/internal/api/profile/profile_loader.py b/conan/internal/api/profile/profile_loader.py index 61342c293e2..5de78063094 100644 --- a/conan/internal/api/profile/profile_loader.py +++ b/conan/internal/api/profile/profile_loader.py @@ -245,8 +245,16 @@ def get_profile(profile_text, base_profile=None): if doc.system_tools: ConanOutput().warning("Profile [system_tools] is deprecated," " please use [platform_tool_requires]") - platform_tool_requires = [RecipeReference.loads(r) for r in doc_platform_tool_requires.splitlines()] - platform_requires = [RecipeReference.loads(r) for r in doc_platform_requires.splitlines()] + + def parse_replaces(replaces): + result = [RecipeReference.loads(r) for r in replaces.splitlines()] + errors = [r for r in result if str(r.version).startswith("[")] + if errors: + raise ConanException("Profile [platform_requires]/[platform_tool_requires] must " + f"be exact versions, not version ranges: {errors}") + return result + platform_tool_requires = parse_replaces(doc_platform_tool_requires) + platform_requires = parse_replaces(doc_platform_requires) def load_replace(doc_replace_requires): result = {} diff --git a/test/integration/graph/test_system_tools.py b/test/integration/graph/test_system_tools.py index f24baf89070..e72b1ab88e3 100644 --- a/test/integration/graph/test_system_tools.py +++ b/test/integration/graph/test_system_tools.py @@ -157,6 +157,26 @@ def test_require_build_context(self): c.run("install app -pr:b=profile_build --build=missing") assert "Install finished successfully" in c.out + def test_platform_requires_error(self): + """ + https://github.com/conan-io/conan/issues/19745 + """ + c = TestClient(light=True) + profile = textwrap.dedent("""\ + include(default) + + [platform_tool_requires] + cmake/[*] + """) + c.save({"conanfile.py": GenConanfile("catch2").with_tool_requires("cmake/[*]"), + "profile": profile}) + + c.run("create --version=3.6.0 -pr=profile", assert_error=True) + assert "AssertionError" not in c.out + assert ("ERROR: Error reading 'profile' profile: Profile [platform_requires]/" + "[platform_tool_requires] must be exact versions, " + "not version ranges: [cmake/[*]]") in c.out + class TestToolRequiresLock: From b498010ed6411a167fafc5d00ffb2ae8fd7f53fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= <5364255+AbrilRBS@users.noreply.github.com> Date: Fri, 13 Mar 2026 13:41:08 +0100 Subject: [PATCH 051/110] Only group build packages in `conan graph info .. -f=html` (#19744) * Only group build packages * Group per context --- conan/cli/formatters/graph/info_graph_html.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/conan/cli/formatters/graph/info_graph_html.py b/conan/cli/formatters/graph/info_graph_html.py index bf67480a5c0..e2466d225b9 100644 --- a/conan/cli/formatters/graph/info_graph_html.py +++ b/conan/cli/formatters/graph/info_graph_html.py @@ -87,7 +87,7 @@ function define_data(){ let nodes = []; let edges = []; - let collapsed_packages = {}; + let collapsed_packages = {"build": {}, "host": {}}; let targets = {}; global_edges = {}; let edge_counter = 0; @@ -115,10 +115,10 @@ else label = node.recipe == "Consumer"? "conanfile": "CLI"; if (collapse_packages) { - let existing = collapsed_packages[label]; + let existing = collapsed_packages[node.context][label]; targets[node_id] = existing; if (existing) continue; - collapsed_packages[label] = node_id; + collapsed_packages[node.context][label] = node_id; } if (excluded_pkgs) { let patterns = excluded_pkgs.split(',') From fe60b2047ad3cdf8c0d075d14718ae9bbd99ed0c Mon Sep 17 00:00:00 2001 From: James Date: Sun, 15 Mar 2026 13:55:01 +0100 Subject: [PATCH 052/110] fix CMakeConfigDeps .set_property context (#19760) --- .../cmake/cmakeconfigdeps/cmakeconfigdeps.py | 6 +++- .../cmakeconfigdeps/test_cmakeconfigdeps.py | 28 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/conan/tools/cmake/cmakeconfigdeps/cmakeconfigdeps.py b/conan/tools/cmake/cmakeconfigdeps/cmakeconfigdeps.py index cc15cf8e98d..1a483bf0405 100644 --- a/conan/tools/cmake/cmakeconfigdeps/cmakeconfigdeps.py +++ b/conan/tools/cmake/cmakeconfigdeps/cmakeconfigdeps.py @@ -165,7 +165,11 @@ def set_property(self, dep, prop, value, build_context=False): def get_property(self, prop, dep, comp_name=None, check_type=None): dep_name = dep.ref.name - build_suffix = "&build" if dep.context == "build" else "" + # Find the requirement that points to this "dep". + # TODO: It would probably be more explicit if it was an argument as "dep", but to keep + # diff minimal + require = next(iter(r for r, d in self._conanfile.dependencies.items() if d is dep)) + build_suffix = "&build" if require.build else "" dep_comp = f"{str(dep_name)}::{comp_name}" if comp_name else f"{str(dep_name)}" try: value = self._properties[f"{dep_comp}{build_suffix}"][prop] diff --git a/test/integration/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps.py b/test/integration/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps.py index ebb98ba3813..9e6d913c2f6 100644 --- a/test/integration/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps.py +++ b/test/integration/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps.py @@ -802,6 +802,34 @@ def test_legacy_defines(): assert 'set(mypkg_DEFINITIONS "-DMY_DEFINE;-DMYVAR=1" )' in mypkg_config +class TestPropertiesBuildContext: + def test_property_build_context(self): + c = TestClient() + conanfile = textwrap.dedent(""" + from conan import ConanFile + from conan.tools.cmake import CMakeConfigDeps + + class PackageConan(ConanFile): + name = "package" + settings = "os", "arch", "compiler", "build_type" + + def requirements(self): + self.requires("zlib/1.3.1") + + def generate(self): + deps = CMakeConfigDeps(self) + deps.set_property("zlib", "cmake_file_name", "MyZlibName") + deps.generate() + """) + c.save({"zlib/conanfile.py": GenConanfile("zlib", "1.3.1"), + "pkg/conanfile.py": conanfile}) + c.run("create zlib") + c.run("install pkg --build-require") + assert "find_package(MyZlibName)" in c.out + config = c.load("pkg/MyZlibNameConfig.cmake") + assert 'set(MyZlibName_VERSION_STRING "1.3.1")' in config + + class TestExtraFindExtraVariants: def test_generated_dir_entries(self): tc = TestClient() From 41a4d71d48ce21744aaf8ef7073ebf385a00e83b Mon Sep 17 00:00:00 2001 From: James Date: Mon, 16 Mar 2026 08:00:25 +0100 Subject: [PATCH 053/110] Fix/overrides lockfiles (#19739) * fix lockfile overrides * wip * fix test --- conan/internal/graph/graph_builder.py | 2 +- conan/internal/model/lockfile.py | 5 +- .../lockfile/test_graph_overrides.py | 54 ++++++++++++++++++- 3 files changed, 57 insertions(+), 4 deletions(-) diff --git a/conan/internal/graph/graph_builder.py b/conan/internal/graph/graph_builder.py index 993a4e2fd27..2e2c416771f 100644 --- a/conan/internal/graph/graph_builder.py +++ b/conan/internal/graph/graph_builder.py @@ -231,7 +231,7 @@ def _initialize_requires(self, node, graph, graph_lock, profile_build, profile_h self._resolve_alias(node, require, alias, graph) self._resolve_replace_requires(node, require, profile_build, profile_host, graph) if graph_lock: - graph_lock.resolve_overrides(require) + graph_lock.resolve_overrides(require, node.context) node.transitive_deps[require] = TransitiveRequirement(require, node=None) return result diff --git a/conan/internal/model/lockfile.py b/conan/internal/model/lockfile.py index c66a6f2e09a..f6811344af8 100644 --- a/conan/internal/model/lockfile.py +++ b/conan/internal/model/lockfile.py @@ -293,7 +293,7 @@ def resolve_locked(self, node, require, resolve_prereleases): ConanOutput().error(msg, error_type="exception") raise - def resolve_overrides(self, require): + def resolve_overrides(self, require, context): """ The lockfile contains the overrides to be able to inject them when the lockfile is applied to upstream dependencies, that have the overrides downstream """ @@ -303,6 +303,9 @@ def resolve_overrides(self, require): overriden = self._overrides.get(require.ref) if overriden and len(overriden) == 1: override_ref = next(iter(overriden)) + locked_refs = self._build_requires.refs() if context == "build" else self._requires.refs() + if override_ref not in locked_refs: + return # The override came from the other context require.overriden_ref = require.overriden_ref or require.ref.copy() require.override_ref = override_ref require.ref = override_ref diff --git a/test/integration/lockfile/test_graph_overrides.py b/test/integration/lockfile/test_graph_overrides.py index 2212e298921..3010ebf143b 100644 --- a/test/integration/lockfile/test_graph_overrides.py +++ b/test/integration/lockfile/test_graph_overrides.py @@ -65,6 +65,8 @@ def test_overrides_half_diamond_ranges(override, force): c.run("graph info pkgc --lockfile=pkgc/conan.lock") assert "pkga/0.2" in c.out assert "pkga/0.1" not in c.out + c.run("graph info pkgb --lockfile=pkgc/conan.lock") + # should work @pytest.mark.parametrize("override, force", [(True, False), (False, True)]) @@ -94,6 +96,8 @@ def test_overrides_half_diamond_ranges_inverted(override, force): c.run("graph info pkgc --lockfile=pkgc/conan.lock") assert "pkga/0.1" in c.out assert "pkga/0.2" not in c.out + c.run("graph info pkgb --lockfile=pkgc/conan.lock") + # should work @pytest.mark.parametrize("override, force", [(True, False), (False, True)]) @@ -224,6 +228,8 @@ def test_overrides_multiple(override1, force1, override2, force2): assert "pkga/0.3" in c.out assert "pkga/0.2#" not in c.out assert "pkga/0.1#" not in c.out # appears in override information + c.run("graph info pkgb --lockfile=pkgd/conan.lock") + # should work def test_graph_different_overrides(): @@ -351,8 +357,12 @@ def test_command_line_lockfile_overrides(): c.run('install pkgc --lockfile-overrides="{\'pkga/0.1\': [\'pkga/0.2\']}"', assert_error=True) assert "Cannot define overrides without a lockfile" in c.out c.run('lock create pkgc') - c.run('install pkgc --lockfile-overrides="{\'pkga/0.1\': [\'pkga/0.2\']}"', assert_error=True) - assert "Requirement 'pkga/0.2' not in lockfile" in c.out + c.run('install pkgc --lockfile-overrides="{\'pkga/0.1\': [\'pkga/0.2\']}"') + # From https://github.com/conan-io/conan/issues/19738 it is simply ignored + # Not a hard "protection" error, still users can't inject a dependency to pkga/0.2 if not + # in the lockfile already + assert "pka/0.2" not in c.out + assert "pkga/0.1" in c.out def test_consecutive_installs(): @@ -371,3 +381,43 @@ def test_consecutive_installs(): # This used to crash when overrides were not managed c.run("install pkgc --build=missing --lockfile=conan.lock --lockfile-out=conan.lock") c.assert_overrides({"pkga/0.1": ["pkga/0.2"]}) + + +class TestOverrideContextError: + # https://github.com/conan-io/conan/issues/19738 + def test_error_lockfile_override_build_require(self): + # The override that comes from the host context is breaking the build locking + c = TestClient(light=True) + c.save({"abseil/conanfile.py": GenConanfile("abseil"), + "protobuf/conanfile.py": GenConanfile("protobuf", "0.1").with_requires("abseil/[*]"), + "app/conanfile.py": GenConanfile("pkgd", "0.1").with_requirement("protobuf/0.1") + .with_requirement("abseil/0.1", + override=True) + .with_tool_requires("protobuf/0.1") + }) + c.run("create abseil --version=0.1") + c.run("create abseil --version=0.2") + c.run("create protobuf") + c.run("lock create app") + c.run("install app --build=missing --lockfile=app/conan.lock") + # It doesnt fail + + def test_error_lockfile_override_build_require_build(self): + # Same as the above, but now the override is in the "build" context, affecting the + # host one that shouldn't be overriden + c = TestClient(light=True) + c.save({"pkga/conanfile.py": GenConanfile("pkga"), + "pkgb/conanfile.py": GenConanfile("pkgb", "0.1").with_requires("pkga/[*]"), + "toolb/conanfile.py": GenConanfile("toolb", "0.1").with_requirement("pkgb/0.1") + .with_requirement("pkga/0.1", + override=True), + "app/conanfile.py": GenConanfile("pkgd", "0.1").with_requirement("pkgb/0.1") + .with_tool_requires("toolb/0.1") + }) + c.run("create pkga --version=0.1") + c.run("create pkga --version=0.2") + c.run("create pkgb") + c.run("create toolb --build=missing") + c.run("lock create app") + c.run("install app --lockfile=app/conan.lock") + # It doesnt fail From f253d3c2eab75b493cabeda7d4fd6607e80ca1bd Mon Sep 17 00:00:00 2001 From: James Date: Mon, 16 Mar 2026 16:04:16 +0100 Subject: [PATCH 054/110] fix issue with --build=editable with download source (#19758) --- conan/internal/graph/installer.py | 2 +- .../command/test_forced_download_source.py | 29 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/conan/internal/graph/installer.py b/conan/internal/graph/installer.py index 5748e4dd611..230f61e668f 100644 --- a/conan/internal/graph/installer.py +++ b/conan/internal/graph/installer.py @@ -182,7 +182,7 @@ def _install_source(self, node, remotes, need_conf=False): return conanfile = node.conanfile - if node.binary == BINARY_EDITABLE: + if node.binary in (BINARY_EDITABLE, BINARY_EDITABLE_BUILD): return recipe_layout = self._cache.recipe_layout(node.ref) diff --git a/test/integration/command/test_forced_download_source.py b/test/integration/command/test_forced_download_source.py index cc06ceae63c..bd48ff76fad 100644 --- a/test/integration/command/test_forced_download_source.py +++ b/test/integration/command/test_forced_download_source.py @@ -75,3 +75,32 @@ def source(self): assert "RUNNING SOURCE" not in c.out c.run("graph info --requires=dep/0.1 -c tools.build:download_source=True") assert "RUNNING SOURCE" not in c.out # BUT it doesn't crash, it used to crash + + +def test_build_editable_with_download_source(): + """ conan build with -b=editable and tools.build:download_source=True should not crash + # https://github.com/conan-io/conan/issues/19757 + """ + c = TestClient() + liba = textwrap.dedent(""" + from conan import ConanFile + + class LibA(ConanFile): + name = "liba" + version = "0.1" + + def source(self): + self.output.info("RUNNING SOURCE!!") + """) + consumer = textwrap.dedent(""" + from conan import ConanFile + + class Consumer(ConanFile): + requires = "liba/0.1" + """) + + c.save({"liba/conanfile.py": liba, + "consumer/conanfile.py": consumer}) + c.run("editable add liba") + c.run("build consumer -b=editable -c tools.build:download_source=True") + assert "RUNNING SOURCE" not in c.out From b254f3c9ae7d509fd71bc34c30ca3a381d107828 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 19 Mar 2026 11:14:23 +0100 Subject: [PATCH 055/110] moving collaborators from ConanApp to ApiHelpers (#19755) * moving collaborators from ConanApp to ApiHelpers * wip * wip * fix test --- conan/api/conan_api.py | 30 +++++++++-- conan/api/subapi/cache.py | 4 +- conan/api/subapi/config.py | 3 +- conan/api/subapi/download.py | 31 ++++++------ conan/api/subapi/export.py | 2 +- conan/api/subapi/graph.py | 11 ++-- conan/api/subapi/install.py | 6 +-- conan/api/subapi/list.py | 53 +++++++++---------- conan/api/subapi/remotes.py | 7 +-- conan/api/subapi/remove.py | 34 ++++++------- conan/api/subapi/report.py | 3 +- conan/api/subapi/upload.py | 15 +++--- conan/api/subapi/workspace.py | 3 +- conan/internal/api/uploader.py | 28 ++++++----- conan/internal/conan_app.py | 56 +++------------------ conan/internal/graph/graph_binaries.py | 10 ++-- conan/internal/graph/installer.py | 21 ++++---- conan/internal/graph/proxy.py | 6 +-- conan/internal/graph/python_requires.py | 6 +-- conan/internal/graph/range_resolver.py | 6 +-- conan/internal/model/workspace.py | 2 +- conan/internal/rest/remote_manager.py | 2 +- conan/test/utils/mocks.py | 2 +- test/integration/cache/storage_path_test.py | 2 +- 24 files changed, 163 insertions(+), 180 deletions(-) diff --git a/conan/api/conan_api.py b/conan/api/conan_api.py index 6ba8b30ff18..85d7a616856 100644 --- a/conan/api/conan_api.py +++ b/conan/api/conan_api.py @@ -21,6 +21,8 @@ from conan.api.subapi.remove import RemoveAPI from conan.api.subapi.upload import UploadAPI from conan.errors import ConanException +from conan.internal.api.remotes.localdb import LocalDB +from conan.internal.cache.cache import PkgCache from conan.internal.cache.home_paths import HomePaths from conan.internal.hook_manager import HookManager from conan.internal.loader import load_python_file @@ -29,7 +31,9 @@ from conan.internal.paths import get_conan_user_home from conan.internal.api.migrations import ClientMigrator from conan.internal.model.version_range import validate_conan_version +from conan.internal.rest.auth_manager import ConanApiAuthManager from conan.internal.rest.conan_requester import ConanRequester +from conan.internal.rest.remote_manager import RemoteManager class ConanAPI: @@ -63,19 +67,19 @@ def __init__(self, cache_folder=None): self.remotes: RemotesAPI = RemotesAPI(self, self._api_helpers) self.command = CommandAPI(self) #: Used to get latest refs and list refs of recipes and packages - self.list: ListAPI = ListAPI(self) + self.list: ListAPI = ListAPI(self, self._api_helpers) self.profiles = ProfilesAPI(self, self._api_helpers) #: Used to install binaries, sources, deploy packages and more self.install: InstallAPI = InstallAPI(self, self._api_helpers) self.graph = GraphAPI(self, self._api_helpers) #: Used to export recipes and pre-compiled package binaries to the Conan cache self.export: ExportAPI = ExportAPI(self, self._api_helpers) - self.remove = RemoveAPI(self) + self.remove = RemoveAPI(self, self._api_helpers) self.new = NewAPI(self) #: Used to upload recipes and packages to remotes self.upload: UploadAPI = UploadAPI(self, self._api_helpers) #: Used to download recipes and packages from remotes - self.download: DownloadAPI = DownloadAPI(self) + self.download: DownloadAPI = DownloadAPI(self, self._api_helpers) #: Used to interact wit the packages storage cache self.cache: CacheAPI = CacheAPI(self, self._api_helpers) #: Used to read and manage lockfile files @@ -108,18 +112,22 @@ def migrate(self): # Migration system # TODO: A prettier refactoring of migrators would be nice from conan import conan_version - migrator = ClientMigrator(self.cache_folder, conan_version) + migrator = ClientMigrator(self._home_folder, conan_version) migrator.migrate() class _ApiHelpers: + # This is an internal implementation detail of Conan, DO NOT USE def __init__(self, conan_api): self._conan_api = conan_api self._cli_core_confs = None self._init_global_conf() + # TODO: Make uniform lazy vs non lazy collaborators self.hook_manager = HookManager(HomePaths(self._conan_api.home_folder).hooks_path) # Wraps an http_requester to inject proxies, certs, etc self._requester = ConanRequester(self.global_conf, self._conan_api.home_folder) + self.cache = PkgCache(self._conan_api.home_folder, self.global_conf) self._settings_yml = None + self._remote_manager = None self._compression_plugin = None @property @@ -165,6 +173,8 @@ def reinit(self): self.hook_manager.reinit() self._requester = ConanRequester(self.global_conf, self._conan_api.home_folder) self._settings_yml = None + self.cache = PkgCache(self._conan_api.home_folder, self.global_conf) + self._remote_manager = None self._compression_plugin = None @property @@ -173,6 +183,18 @@ def settings_yml(self): self._settings_yml = load_settings_yml(self._conan_api.home_folder) return self._settings_yml + @property + def remote_manager(self): + if self._remote_manager is None: + home_folder = self._conan_api.home_folder + localdb = LocalDB(home_folder) + requester = self._conan_api._api_helpers.requester # noqa + auth_manager = ConanApiAuthManager(requester, self._conan_api.home_folder, localdb, + self.global_conf) + self._remote_manager = RemoteManager(self.cache, auth_manager, home_folder, + self.compression_plugin) + return self._remote_manager + @property def requester(self): return self._requester diff --git a/conan/api/subapi/cache.py b/conan/api/subapi/cache.py index bad3f9473ba..f2bf74e136c 100644 --- a/conan/api/subapi/cache.py +++ b/conan/api/subapi/cache.py @@ -195,7 +195,9 @@ def sign(self, package_list): "https://docs.conan.io/2/reference/extensions/package_signing.html.") app = ConanApp(self._conan_api) - preparator = PackagePreparator(app, self._api_helpers.global_conf) + preparator = PackagePreparator(app, self._api_helpers.cache, + self._api_helpers.remote_manager, + self._api_helpers.global_conf) # Some packages can have missing sources/exports_sources enabled_remotes = self._conan_api.remotes.list() preparator.prepare(package_list, enabled_remotes, None, force=True) diff --git a/conan/api/subapi/config.py b/conan/api/subapi/config.py index bcc252832a8..eca212d3534 100644 --- a/conan/api/subapi/config.py +++ b/conan/api/subapi/config.py @@ -213,6 +213,7 @@ def fetch_packages(self, requires, lockfile=None, remotes=None, profile=None): profile_host = profile_build = profile or conan_api.profiles.get_profile([]) app = ConanApp(self._conan_api) + cache = self._helpers.cache ConanOutput().title("Fetching requested configuration packages") result = [] @@ -225,7 +226,7 @@ def fetch_packages(self, requires, lockfile=None, remotes=None, profile=None): recipe=RECIPE_VIRTUAL) root_node.is_conf = True update = ["*"] - builder = DepsGraphBuilder(app.proxy, app.loader, app.range_resolver, app.cache, remotes, + builder = DepsGraphBuilder(app.proxy, app.loader, app.range_resolver, cache, remotes, update, update, self._helpers.global_conf) deps_graph = builder.load_graph(root_node, profile_host, profile_build, lockfile) diff --git a/conan/api/subapi/download.py b/conan/api/subapi/download.py index d67ff123720..b136a307aec 100644 --- a/conan/api/subapi/download.py +++ b/conan/api/subapi/download.py @@ -4,7 +4,6 @@ from conan.api.model import Remote, PackagesList from conan.api.output import ConanOutput -from conan.internal.conan_app import ConanBasicApp from conan.errors import ConanException from conan.api.model import PkgReference from conan.api.model import RecipeReference @@ -13,38 +12,39 @@ class DownloadAPI: """ This API is used to download recipes and packages from a remote server.""" - def __init__(self, conan_api): + def __init__(self, conan_api, api_helpers): self._conan_api = conan_api + self._api_helpers = api_helpers def recipe(self, ref: RecipeReference, remote: Remote, metadata: Optional[List[str]] = None): """Download the recipe specified in the ref from the remote. If the recipe is already in the cache it will be skipped, but the specified metadata will be downloaded.""" output = ConanOutput() - app = ConanBasicApp(self._conan_api) assert ref.revision, f"Reference '{ref}' must have revision" try: - recipe_layout = app.cache.recipe_layout(ref) # raises if not found + recipe_layout = self._api_helpers.cache.recipe_layout(ref) # raises if not found except ConanException: pass else: output.info(f"Skip recipe {ref.repr_notime()} download, already in cache") if metadata: - app.remote_manager.get_recipe_metadata(recipe_layout, ref, remote, metadata) + self._api_helpers.remote_manager.get_recipe_metadata(recipe_layout, ref, remote, + metadata) return False output.info(f"Downloading recipe '{ref.repr_notime()}'") if ref.timestamp is None: # we didnt obtain the timestamp before (in general it should be) # Respect the timestamp of the server, the ``get_recipe()`` doesn't do it internally # Best would be that ``get_recipe()`` returns the timestamp in the same call - server_ref = app.remote_manager.get_recipe_revision(ref, remote) + server_ref = self._api_helpers.remote_manager.get_recipe_revision(ref, remote) assert server_ref == ref ref.timestamp = server_ref.timestamp - recipe_layout = app.remote_manager.get_recipe(ref, remote, metadata) + recipe_layout = self._api_helpers.remote_manager.get_recipe(ref, remote, metadata) # Download the sources too, don't be lazy output.info(f"Downloading '{str(ref)}' sources") - app.remote_manager.get_recipe_sources(ref, recipe_layout, remote) + self._api_helpers.remote_manager.get_recipe_sources(ref, recipe_layout, remote) return True def package(self, pref: PkgReference, remote: Remote, metadata: Optional[List[str]] = None): @@ -53,28 +53,28 @@ def package(self, pref: PkgReference, remote: Remote, metadata: Optional[List[st If the package is already in the cache it will be skipped, but the specified metadata will be downloaded.""" output = ConanOutput() - app = ConanBasicApp(self._conan_api) + try: - app.cache.recipe_layout(pref.ref) # raises if not found + self._api_helpers.cache.recipe_layout(pref.ref) # raises if not found except ConanException: raise ConanException("The recipe of the specified package " "doesn't exist, download it first") - skip_download = app.cache.exists_prev(pref) + skip_download = self._api_helpers.cache.exists_prev(pref) if skip_download: output.info(f"Skip package {pref.repr_notime()} download, already in cache") if metadata: - app.remote_manager.get_package_metadata(pref, remote, metadata) + self._api_helpers.remote_manager.get_package_metadata(pref, remote, metadata) return False if pref.timestamp is None: # we didn't obtain the timestamp before (in general it should be) # Respect the timestamp of the server - server_pref = app.remote_manager.get_package_revision(pref, remote) + server_pref = self._api_helpers.remote_manager.get_package_revision(pref, remote) assert server_pref == pref pref.timestamp = server_pref.timestamp output.info(f"Downloading package '{pref.repr_notime()}'") - app.remote_manager.get_package(pref, remote, metadata) + self._api_helpers.remote_manager.get_package(pref, remote, metadata) return True def download_full(self, package_list: PackagesList, remote: Remote, @@ -94,7 +94,8 @@ def _download_pkglist(pkglist): pkg_dict.pop("upload-urls", None) t = time.time() - parallel = self._conan_api.config.get("core.download:parallel", default=1, check_type=int) + parallel = self._api_helpers.global_conf.get("core.download:parallel", default=1, + check_type=int) thread_pool = ThreadPool(parallel) if parallel > 1 else None if not thread_pool or len(package_list._data) <= 1: # FIXME: Iteration when multiple rrevs _download_pkglist(package_list) diff --git a/conan/api/subapi/export.py b/conan/api/subapi/export.py index 797364c1574..3901c3c0b8c 100644 --- a/conan/api/subapi/export.py +++ b/conan/api/subapi/export.py @@ -46,7 +46,7 @@ def export(self, path, name: str = None, version: str = None, user: str = None, ConanOutput().title("Exporting recipe to the cache") app = ConanApp(self._conan_api) hook_manager = self._helpers.hook_manager - return cmd_export(app.loader, app.cache, hook_manager, self._helpers.global_conf, path, + return cmd_export(app.loader,self._helpers.cache, hook_manager, self._helpers.global_conf, path, name, version, user, channel, graph_lock=lockfile, remotes=remotes) def export_pkg_graph(self, path, ref: RecipeReference, profile_host, profile_build, diff --git a/conan/api/subapi/graph.py b/conan/api/subapi/graph.py index 3fb81c3621a..24200b455ba 100644 --- a/conan/api/subapi/graph.py +++ b/conan/api/subapi/graph.py @@ -1,5 +1,5 @@ from conan.api.output import ConanOutput -from conan.internal.conan_app import ConanApp, ConanBasicApp +from conan.internal.conan_app import ConanApp from conan.internal.model.recipe_ref import ref_matches from conan.internal.graph.graph import Node, RECIPE_CONSUMER, CONTEXT_HOST, RECIPE_VIRTUAL, \ CONTEXT_BUILD, BINARY_MISSING, DepsGraph @@ -188,7 +188,8 @@ 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, app.cache, remotes, + cache = self._conan_api._api_helpers.cache # noqa + builder = DepsGraphBuilder(app.proxy, app.loader, app.range_resolver, cache, remotes, update, check_update, self._conan_api._api_helpers.global_conf) deps_graph = builder.load_graph(root_node, profile_host, profile_build, lockfile) return deps_graph @@ -213,8 +214,10 @@ def analyze_binaries(self, graph, build_mode=None, remotes=None, update=None, :param tested_graph: In case of a "test_package", the graph being tested """ ConanOutput().title("Computing necessary packages") - conan_app = ConanBasicApp(self._conan_api) - binaries_analyzer = GraphBinariesAnalyzer(conan_app, self._conan_api._api_helpers.global_conf, + binaries_analyzer = GraphBinariesAnalyzer(self._helpers.cache, + self._helpers.remote_manager, + self._conan_api.home_folder, + self._helpers.global_conf, self._helpers.hook_manager) binaries_analyzer.evaluate_graph(graph, build_mode, lockfile, remotes, update, build_modes_test, tested_graph) diff --git a/conan/api/subapi/install.py b/conan/api/subapi/install.py index cf4cb60cc3e..806ec3f9bb5 100644 --- a/conan/api/subapi/install.py +++ b/conan/api/subapi/install.py @@ -40,7 +40,7 @@ def install_binaries(self, deps_graph, remotes: List[Remote] = None, return_inst :param return_install_error: If ``True``, do not raise an exception, but return it """ app = ConanBasicApp(self._conan_api) - installer = BinaryInstaller(app, self._helpers.global_conf, app.editable_packages, + installer = BinaryInstaller(self._conan_api, self._helpers.global_conf, app.editable_packages, self._helpers.hook_manager) install_graph = InstallGraph(deps_graph) install_graph.raise_errors() @@ -69,7 +69,7 @@ def install_system_requires(self, graph, only_info=False): :param only_info: If ``True``, only reporting and checking of whether the system requirements are installed is performed. """ app = ConanBasicApp(self._conan_api) - installer = BinaryInstaller(app, self._helpers.global_conf, app.editable_packages, + installer = BinaryInstaller(self._conan_api, self._helpers.global_conf, app.editable_packages, self._helpers.hook_manager) installer.install_system_requires(graph, only_info) @@ -90,7 +90,7 @@ def install_sources(self, graph, remotes: List[Remote]): :param graph: Dependency graph to download sources from """ app = ConanBasicApp(self._conan_api) - installer = BinaryInstaller(app, self._helpers.global_conf, app.editable_packages, + installer = BinaryInstaller(self._conan_api, self._helpers.global_conf, app.editable_packages, self._helpers.hook_manager) installer.install_sources(graph, remotes) diff --git a/conan/api/subapi/list.py b/conan/api/subapi/list.py index 1d3a0ddf8bb..724c449d0a4 100644 --- a/conan/api/subapi/list.py +++ b/conan/api/subapi/list.py @@ -5,7 +5,6 @@ from conan.api.model import PackagesList, MultiPackagesList, ListPattern, Remote from conan.api.output import ConanOutput, TimedOutput from conan.internal.api.list.query_parse import filter_package_configs -from conan.internal.conan_app import ConanBasicApp from conan.internal.model.recipe_ref import ref_matches from conan.internal.paths import CONANINFO from conan.internal.errors import NotFoundException @@ -47,18 +46,18 @@ class ListAPI: """ Get references from the recipes and packages in the cache or a remote """ - def __init__(self, conan_api): + def __init__(self, conan_api, api_helpers): self._conan_api = conan_api + self._api_helpers = api_helpers def latest_recipe_revision(self, ref: RecipeReference, remote: Remote = None): """ For a given recipe reference, return the latest revision of the recipe in the remote, or in the local cache if no remote is specified, or ``None`` if the recipe does not exist.""" assert ref.revision is None, "latest_recipe_revision: ref already have a revision" - app = ConanBasicApp(self._conan_api) if remote: - ret = app.remote_manager.get_latest_recipe_revision(ref, remote=remote) + ret = self._api_helpers.remote_manager.get_latest_recipe_revision(ref, remote=remote) else: - ret = app.cache.get_latest_recipe_revision(ref) + ret = self._api_helpers.cache.get_latest_recipe_revision(ref) return ret @@ -66,11 +65,10 @@ def recipe_revisions(self, ref: RecipeReference, remote: Remote = None): """ For a given recipe reference, return all the revisions of the recipe in the remote, or in the local cache if no remote is specified""" assert ref.revision is None, "recipe_revisions: ref already have a revision" - app = ConanBasicApp(self._conan_api) if remote: - results = app.remote_manager.get_recipe_revisions(ref, remote=remote) + results = self._api_helpers.remote_manager.get_recipe_revisions(ref, remote=remote) else: - results = app.cache.get_recipe_revisions(ref) + results = self._api_helpers.cache.get_recipe_revisions(ref) return results @@ -80,33 +78,30 @@ def latest_package_revision(self, pref: PkgReference, remote=None): # is used as an "exists" check too in other places, lets respect the None return assert pref.revision is None, "latest_package_revision: ref already have a revision" assert pref.package_id is not None, "package_id must be defined" - app = ConanBasicApp(self._conan_api) if remote: - ret = app.remote_manager.get_latest_package_revision(pref, remote=remote) + ret = self._api_helpers.remote_manager.get_latest_package_revision(pref, remote=remote) else: - ret = app.cache.get_latest_package_revision(pref) + ret = self._api_helpers.cache.get_latest_package_revision(pref) return ret def package_revisions(self, pref: PkgReference, remote=None): assert pref.ref.revision is not None, "package_revisions requires a recipe revision, " \ "check latest first if needed" - app = ConanBasicApp(self._conan_api) if remote: - results = app.remote_manager.get_package_revisions(pref, remote=remote) + results = self._api_helpers.remote_manager.get_package_revisions(pref, remote=remote) else: - results = app.cache.get_package_revisions(pref) + results = self._api_helpers.cache.get_package_revisions(pref) return results def _packages_configurations(self, ref: RecipeReference, remote=None) -> Dict[PkgReference, dict]: assert ref.revision is not None and ref.revision != "latest", \ "packages: ref should have a revision. Check latest if needed." - app = ConanBasicApp(self._conan_api) if not remote: - prefs = app.cache.get_package_references(ref) - packages = _get_cache_packages_binary_info(app.cache, prefs) + prefs = self._api_helpers.cache.get_package_references(ref) + packages = _get_cache_packages_binary_info(self._api_helpers.cache, prefs) else: - packages = app.remote_manager.search_packages(remote, ref) + packages = self._api_helpers.remote_manager.search_packages(remote, ref) return packages @staticmethod @@ -182,13 +177,13 @@ def select(self, pattern: ListPattern, package_query=None, remote: Remote = None select_bundle = PackagesList() # Avoid doing a ``search`` of recipes if it is an exact ref and it will be used later search_ref = pattern.search_ref - app = ConanBasicApp(self._conan_api) + cache = self._api_helpers.cache limit_time = _timelimit(lru) if lru else None out = ConanOutput() remote_name = "local cache" if not remote else remote.name if search_ref: - refs = _search_recipes(app, search_ref, remote=remote) - global_conf = self._conan_api._api_helpers.global_conf # noqa + refs = _search_recipes(self._api_helpers, search_ref, remote=remote) + global_conf = self._api_helpers.global_conf # noqa resolve_prereleases = global_conf.get("core.version_ranges:resolve_prereleases") refs = pattern.filter_versions(refs, resolve_prereleases) pattern.check_refs(refs) @@ -219,7 +214,7 @@ def msg_format(msg, item, total): rrevs = list(reversed(rrevs)) # Order older revisions first if lru and pattern.package_id is None: # Filter LRUs - rrevs = [r for r in rrevs if app.cache.get_recipe_lru(r) < limit_time] + rrevs = [r for r in rrevs if cache.get_recipe_lru(r) < limit_time] for rr in rrevs: select_bundle.add_ref(rr) @@ -261,7 +256,7 @@ def msg_format(msg, item, total): prefs = new_prefs if lru: # Filter LRUs - prefs = [r for r in prefs if app.cache.get_package_lru(r) < limit_time] + prefs = [r for r in prefs if cache.get_package_lru(r) < limit_time] # Packages dict has been listed, even if empty select_bundle.recipe_dict(rrev)["packages"] = {} @@ -317,13 +312,13 @@ def find_remotes(self, package_list, remotes): (Experimental) Find the remotes where the current package lists can be found """ result = MultiPackagesList() - app = ConanBasicApp(self._conan_api) + remote_manager = self._api_helpers.remote_manager for r in remotes: result_pkg_list = PackagesList() for ref, ref_contents in package_list.serialize().items(): ref = RecipeReference.loads(ref) try: - remote_rrevs = app.remote_manager.get_recipe_revisions(ref, remote=r) + remote_rrevs = remote_manager.get_recipe_revisions(ref, remote=r) except NotFoundException: continue revisions = ref_contents.get("revisions") @@ -345,7 +340,7 @@ def find_remotes(self, package_list, remotes): for pkgid, pkgcontent in packages.items(): pref = PkgReference(ref, pkgid) try: - remote_prefs = app.remote_manager.get_package_revisions(pref, remote=r) + remote_prefs = remote_manager.get_package_revisions(pref, remote=r) except NotFoundException: continue pkg_revisions = pkgcontent.get("revisions") @@ -551,16 +546,16 @@ def _get_cache_packages_binary_info(cache, prefs) -> Dict[PkgReference, dict]: return result -def _search_recipes(app, query: str, remote=None): +def _search_recipes(api_helpers, query: str, remote=None): only_none_user_channel = False if query and query.endswith("@"): only_none_user_channel = True query = query[:-1] if remote: - refs = app.remote_manager.search_recipes(remote, query) + refs = api_helpers.remote_manager.search_recipes(remote, query) else: - refs = app.cache.search_recipes(query) + refs = api_helpers.cache.search_recipes(query) ret = [] for r in refs: if not only_none_user_channel or (r.user is None and r.channel is None): diff --git a/conan/api/subapi/remotes.py b/conan/api/subapi/remotes.py index 30e754b0694..51dd9f5be94 100644 --- a/conan/api/subapi/remotes.py +++ b/conan/api/subapi/remotes.py @@ -7,7 +7,6 @@ from conan.api.model import Remote, LOCAL_RECIPES_INDEX from conan.api.output import ConanOutput from conan.internal.cache.home_paths import HomePaths -from conan.internal.conan_app import ConanBasicApp from conan.internal.rest.remote_credentials import RemoteCredentials from conan.internal.rest.rest_client_local_recipe_index import add_local_recipes_index_remote, \ remove_local_recipes_index_remote @@ -228,8 +227,7 @@ def user_login(self, remote: Remote, username: str, password: str): :param username: the user login as ``str`` :param password: password ``str`` """ - app = ConanBasicApp(self._conan_api) - app.remote_manager.authenticate(remote, username, password) + self._api_helpers.remote_manager.authenticate(remote, username, password) def login(self, remotes, username=None, password=None): creds = RemoteCredentials(self._conan_api.cache_folder, self._api_helpers.global_conf) @@ -271,7 +269,6 @@ def user_set(self, remote: Remote, username): def user_auth(self, remote: Remote, with_user=False, force=False): # TODO: Review localdb = LocalDB(self._home_folder) - app = ConanBasicApp(self._conan_api) if with_user: user, token, _ = localdb.get_login(remote.url) if not user: @@ -279,7 +276,7 @@ def user_auth(self, remote: Remote, with_user=False, force=False): user = os.getenv(var_name, None) or os.getenv("CONAN_LOGIN_USERNAME", None) if not user: return - app.remote_manager.check_credentials(remote, force) + self._api_helpers.remote_manager.check_credentials(remote, force) user, token, _ = localdb.get_login(remote.url) return user diff --git a/conan/api/subapi/remove.py b/conan/api/subapi/remove.py index 1abde464766..89f5132f2aa 100644 --- a/conan/api/subapi/remove.py +++ b/conan/api/subapi/remove.py @@ -1,53 +1,51 @@ from typing import Optional from conan.api.model import Remote -from conan.internal.conan_app import ConanBasicApp from conan.api.model import PkgReference from conan.api.model import RecipeReference class RemoveAPI: - def __init__(self, conan_api): + def __init__(self, conan_api, api_helpers): self._conan_api = conan_api + self._api_helpers = api_helpers def recipe(self, ref: RecipeReference, remote: Optional[Remote] = None): assert ref.revision, "Recipe revision cannot be None to remove a recipe" """Removes the recipe (or recipe revision if present) and all the packages (with all prev)""" - app = ConanBasicApp(self._conan_api) + if remote: - app.remote_manager.remove_recipe(ref, remote) + self._api_helpers.remote_manager.remove_recipe(ref, remote) else: self.all_recipe_packages(ref) - recipe_layout = app.cache.recipe_layout(ref) - app.cache.remove_recipe_layout(recipe_layout) + recipe_layout = self._api_helpers.cache.recipe_layout(ref) + self._api_helpers.cache.remove_recipe_layout(recipe_layout) def all_recipe_packages(self, ref: RecipeReference, remote: Optional[Remote] = None): assert ref.revision, "Recipe revision cannot be None to remove a recipe" """Removes all the packages from the provided reference""" - app = ConanBasicApp(self._conan_api) + if remote: - app.remote_manager.remove_all_packages(ref, remote) + self._api_helpers.remote_manager.remove_all_packages(ref, remote) else: # Remove all the prefs with all the prevs - self._remove_all_local_packages(app, ref) + self._remove_all_local_packages(ref) - @staticmethod - def _remove_all_local_packages(app, ref): + def _remove_all_local_packages(self, ref): # Get all the prefs and all the prevs - pkg_ids = app.cache.get_package_references(ref, only_latest_prev=False) + pkg_ids = self._api_helpers.cache.get_package_references(ref, only_latest_prev=False) for pref in pkg_ids: - package_layout = app.cache.pkg_layout(pref) - app.cache.remove_package_layout(package_layout) + package_layout = self._api_helpers.cache.pkg_layout(pref) + self._api_helpers.cache.remove_package_layout(package_layout) def package(self, pref: PkgReference, remote: Optional[Remote]): assert pref.ref.revision, "Recipe revision cannot be None to remove a package" assert pref.revision, "Package revision cannot be None to remove a package" - app = ConanBasicApp(self._conan_api) if remote: # FIXME: Create a "packages" method to optimize remote remove? - app.remote_manager.remove_packages([pref], remote) + self._api_helpers.remote_manager.remove_packages([pref], remote) else: - package_layout = app.cache.pkg_layout(pref) - app.cache.remove_package_layout(package_layout) + package_layout = self._api_helpers.cache.pkg_layout(pref) + self._api_helpers.cache.remove_package_layout(package_layout) diff --git a/conan/api/subapi/report.py b/conan/api/subapi/report.py index 134a3f479dc..7fe903411f2 100644 --- a/conan/api/subapi/report.py +++ b/conan/api/subapi/report.py @@ -98,7 +98,8 @@ def _configure_source(conan_api, hook_manager, conanfile_path, ref, remotes): with conanfile_exception_formatter(conanfile, "layout"): conanfile.layout() - recipe_layout = app.cache.recipe_layout(ref) + cache = conan_api._api_helpers.cache # noqa + recipe_layout = cache.recipe_layout(ref) export_source_folder = recipe_layout.export_sources() source_folder = recipe_layout.source() diff --git a/conan/api/subapi/upload.py b/conan/api/subapi/upload.py index 53eeb38d7fc..1fc18bcd593 100644 --- a/conan/api/subapi/upload.py +++ b/conan/api/subapi/upload.py @@ -38,7 +38,7 @@ def check_upstream(self, package_list: PackagesList, remote: Remote, """ app = ConanApp(self._conan_api) for ref, _ in package_list.items(): - layout = app.cache.recipe_layout(ref) + layout = self._api_helpers.cache.recipe_layout(ref) conanfile_path = layout.conanfile() conanfile = app.loader.load_basic(conanfile_path, remotes=enabled_remotes) if conanfile.upload_policy == "skip": @@ -46,7 +46,7 @@ def check_upstream(self, package_list: PackagesList, remote: Remote, "because upload_policy='skip'") package_list.recipe_dict(ref)["packages"] = {} - UploadUpstreamChecker(app).check(package_list, remote, force) + UploadUpstreamChecker(self._api_helpers.remote_manager).check(package_list, remote, force) def prepare(self, package_list: PackagesList, enabled_remotes: List[Remote], metadata: List[str] = None): @@ -64,18 +64,19 @@ def prepare(self, package_list: PackagesList, enabled_remotes: List[Remote], if metadata and metadata != [''] and '' in metadata: raise ConanException("Empty string and patterns can not be mixed for metadata.") app = ConanApp(self._conan_api) - preparator = PackagePreparator(app, self._api_helpers.global_conf) + preparator = PackagePreparator(app, self._api_helpers.cache, + self._api_helpers.remote_manager, + self._api_helpers.global_conf) preparator.prepare(package_list, enabled_remotes, metadata) - signer = PkgSignaturesPlugin(app.cache, app.cache_folder) + signer = PkgSignaturesPlugin(self._api_helpers.cache, self._conan_api.home_folder) if signer.is_sign_configured: ConanOutput().warning("[Package sign] Implicitly signing packages in the upload " "command has been removed. Use 'conan cache sign' command before " "uploading instead.", warn_tag="deprecated") def _upload(self, package_list, remote): - app = ConanApp(self._conan_api) - app.remote_manager.check_credentials(remote) - executor = UploadExecutor(app) + self._api_helpers.remote_manager.check_credentials(remote) + executor = UploadExecutor(self._api_helpers.remote_manager) executor.upload(package_list, remote) def upload_full(self, package_list: PackagesList, remote: Remote, enabled_remotes: List[Remote], diff --git a/conan/api/subapi/workspace.py b/conan/api/subapi/workspace.py index d2f56bac545..0394f1829cd 100644 --- a/conan/api/subapi/workspace.py +++ b/conan/api/subapi/workspace.py @@ -159,7 +159,8 @@ def open(self, ref, remotes, cwd=None): conanfile.output.warning("conandata doesn't contain 'scm' information\n" "doing a local copy!!!") shutil.copytree(layout.export(), dst_path) - retrieve_exports_sources(app.remote_manager, layout, conanfile, ref, remotes) + remote_manager = self._conan_api._api_helpers.remote_manager # noqa + retrieve_exports_sources(remote_manager, layout, conanfile, ref, remotes) export_sources = layout.export_sources() if os.path.exists(export_sources): conanfile.output.warning("There are export-sources, copying them, but the location" diff --git a/conan/internal/api/uploader.py b/conan/internal/api/uploader.py index 6186a31f4fa..a5b70603224 100644 --- a/conan/internal/api/uploader.py +++ b/conan/internal/api/uploader.py @@ -26,8 +26,8 @@ class UploadUpstreamChecker: This is completely irrespective of the actual package contents, it only uses the local computed revision and the remote one """ - def __init__(self, app: ConanApp): - self._app = app + def __init__(self, remote_manager): + self._remote_manager = remote_manager def check(self, package_list, remote, force): for ref, packages in package_list.items(): @@ -43,7 +43,7 @@ def _check_upstream_recipe(self, ref, ref_bundle, remote, force): try: assert ref.revision # TODO: It is a bit ugly, interface-wise to ask for revisions to check existence - server_ref = self._app.remote_manager.get_recipe_revision(ref, remote) + server_ref = self._remote_manager.get_recipe_revision(ref, remote) assert server_ref # If successful (not raising NotFoundException), this will exist except NotFoundException: ref_bundle["force_upload"] = False @@ -64,7 +64,7 @@ def _check_upstream_package(self, pref, prev_bundle, remote, force): try: # TODO: It is a bit ugly, interface-wise to ask for revisions to check existence - server_revisions = self._app.remote_manager.get_package_revision(pref, remote) + server_revisions = self._remote_manager.get_package_revision(pref, remote) assert server_revisions except NotFoundException: prev_bundle["force_upload"] = False @@ -104,8 +104,10 @@ def get_compress_level(compressformat, global_conf): class PackagePreparator: - def __init__(self, app: ConanApp, global_conf): + def __init__(self, app: ConanApp, cache, remote_manager, global_conf): self._app = app + self._remote_manager = remote_manager + self._cache = cache self._global_conf = global_conf compressformat = self._global_conf.get("core.upload:compression_format", default="gz", choices=COMPRESSIONS) @@ -118,7 +120,7 @@ def __init__(self, app: ConanApp, global_conf): def prepare(self, pkg_list, enabled_remotes, metadata, force=False): local_url = self._global_conf.get("core.scm:local_url", choices=["allow", "block"]) for ref, packages in pkg_list.items(): - recipe_layout = self._app.cache.recipe_layout(ref) + recipe_layout = self._cache.recipe_layout(ref) conanfile_path = recipe_layout.conanfile() conanfile = self._app.loader.load_basic(conanfile_path) url = conanfile.conan_data.get("scm", {}).get("url") if conanfile.conan_data else None @@ -156,7 +158,7 @@ def _prepare_recipe(self, recipe_layout, ref, ref_bundle, conanfile, remotes): - compress the artifacts in conan_export.tgz and conan_export_sources.tgz """ try: - retrieve_exports_sources(self._app.remote_manager, recipe_layout, conanfile, ref, + retrieve_exports_sources(self._remote_manager, recipe_layout, conanfile, ref, remotes) cache_files = self._compress_recipe_files(recipe_layout, ref) ref_bundle["files"] = cache_files @@ -201,7 +203,7 @@ def _compress_recipe_files(self, layout, ref): def _prepare_package(self, pref, prev_bundle, metadata, force=False): pkg_layout = None if prev_bundle.get("upload") or force: - pkg_layout = self._app.cache.pkg_layout(pref) + pkg_layout = self._cache.pkg_layout(pref) if pkg_layout.package_is_dirty(): raise ConanException(f"Package {pref} is corrupted, aborting upload.\n" f"Remove it with 'conan remove {pref}'") @@ -210,7 +212,7 @@ def _prepare_package(self, pref, prev_bundle, metadata, force=False): # Package metadata files too if metadata != [""] and (metadata or prev_bundle.get("upload")): - pkg_layout = pkg_layout or self._app.cache.pkg_layout(pref) + pkg_layout = pkg_layout or self._cache.pkg_layout(pref) metadata_folder = pkg_layout.metadata() files = _metadata_files(metadata_folder, metadata) if files: @@ -283,8 +285,8 @@ class UploadExecutor: been computed and are passed in the ``upload_data`` parameter, so this executor is also agnostic about which files are transferred """ - def __init__(self, app: ConanApp): - self._app = app + def __init__(self, remote_manager): + self._remote_manager = remote_manager def upload(self, upload_data, remote): for ref, packages in upload_data.items(): @@ -303,7 +305,7 @@ def upload_recipe(self, ref, bundle, remote): output.info(f"Uploading recipe '{ref.repr_notime()}' ({_total_size(cache_files)})") t1 = time.time() - self._app.remote_manager.upload_recipe(ref, cache_files, remote) + self._remote_manager.upload_recipe(ref, cache_files, remote) duration = time.time() - t1 output.debug(f"Upload {ref} in {duration} time") @@ -318,7 +320,7 @@ def upload_package(self, pref, prev_bundle, remote): output.info(f"Uploading package '{pref.repr_notime()}' ({_total_size(cache_files)})") t1 = time.time() - self._app.remote_manager.upload_package(pref, cache_files, remote) + self._remote_manager.upload_package(pref, cache_files, remote) duration = time.time() - t1 output.debug(f"Upload {pref} in {duration} time") diff --git a/conan/internal/conan_app.py b/conan/internal/conan_app.py index 77f458f07a2..3dba2e69b31 100644 --- a/conan/internal/conan_app.py +++ b/conan/internal/conan_app.py @@ -4,13 +4,11 @@ from conan.internal.cache.cache import PkgCache from conan.internal.cache.home_paths import HomePaths from conan.internal.model.conf import ConfDefinition -from conan.internal.rest.auth_manager import ConanApiAuthManager from conan.internal.graph.proxy import ConanProxy from conan.internal.graph.python_requires import PyRequireLoader from conan.internal.graph.range_resolver import RangeResolver from conan.internal.loader import ConanFileLoader, load_python_file from conan.internal.rest.remote_manager import RemoteManager -from conan.internal.api.remotes.localdb import LocalDB class CmdWrapper: @@ -28,54 +26,13 @@ def wrap(self, cmd, conanfile, **kwargs): class ConanFileHelpers: - def __init__(self, requester, cmd_wrapper, global_conf, cache, home_folder): + def __init__(self, requester, cmd_wrapper, global_conf, cache, home_folder, conan_api): self.requester = requester self.cmd_wrapper = cmd_wrapper self.global_conf = global_conf self.cache = cache self.home_folder = home_folder - - -class ConanBasicApp: - def __init__(self, conan_api): - """ Needs: - - Global configuration - - Cache home folder - """ - # TODO: Remove this global_conf from here - global_conf = conan_api._api_helpers.global_conf # noqa - self._global_conf = global_conf - self.conan_api = conan_api - cache_folder = conan_api.home_folder - self.cache_folder = cache_folder - self.cache = PkgCache(self.cache_folder, global_conf) - # Wraps RestApiClient to add authentication support (same interface) - localdb = LocalDB(cache_folder) - requester = conan_api._api_helpers.requester # noqa - auth_manager = ConanApiAuthManager(requester, cache_folder, localdb, global_conf) - # Handle remote connections - compress = conan_api._api_helpers.compression_plugin # noqa - self.remote_manager = RemoteManager(self.cache, auth_manager, cache_folder, compress) - global_editables = conan_api.local.editable_packages - ws_editables = conan_api.workspace.packages() - self.editable_packages = global_editables.update_copy(ws_editables) - - -class ConanApp(ConanBasicApp): - def __init__(self, conan_api): - """ Needs: - - LocalAPI to read editable packages - """ - super().__init__(conan_api) - legacy_update = self._global_conf.get("core:update_policy", choices=["legacy"]) - self.proxy = ConanProxy(self, self.editable_packages, legacy_update=legacy_update) - self.range_resolver = RangeResolver(self, self._global_conf, self.editable_packages) - - self.pyreq_loader = PyRequireLoader(self, self._global_conf) - cmd_wrap = CmdWrapper(HomePaths(self.cache_folder).wrapper_path) - requester = conan_api._api_helpers.requester # noqa - conanfile_helpers = ConanFileHelpers(requester, cmd_wrap, self._global_conf, self.cache, self.cache_folder) - self.loader = ConanFileLoader(self.pyreq_loader, conanfile_helpers) + self.conan_api = conan_api # Might be None for local-recipes-index class LocalRecipesIndexApp: @@ -90,8 +47,9 @@ def __init__(self, cache_folder): self.cache = PkgCache(cache_folder, self.global_conf) self.remote_manager = RemoteManager(self.cache, auth_manager=None, home_folder=cache_folder) editable_packages = EditablePackages() - self.proxy = ConanProxy(self, editable_packages) - self.range_resolver = RangeResolver(self, self.global_conf, editable_packages) - pyreq_loader = PyRequireLoader(self, self.global_conf) - helpers = ConanFileHelpers(None, CmdWrapper(""), self.global_conf, self.cache, cache_folder) + self.proxy = ConanProxy(self.cache, self.remote_manager, editable_packages) + self.range_resolver = RangeResolver(self.cache, self.remote_manager, self.global_conf, + editable_packages) + pyreq_loader = PyRequireLoader(self.proxy, self.range_resolver, self.global_conf) + helpers = ConanFileHelpers(None, CmdWrapper(""), self.global_conf, self.cache, cache_folder, None) self.loader = ConanFileLoader(pyreq_loader, helpers) diff --git a/conan/internal/graph/graph_binaries.py b/conan/internal/graph/graph_binaries.py index afa14528557..09889f638f6 100644 --- a/conan/internal/graph/graph_binaries.py +++ b/conan/internal/graph/graph_binaries.py @@ -21,15 +21,15 @@ class GraphBinariesAnalyzer: - def __init__(self, conan_app, global_conf, hook_manager): - self._cache = conan_app.cache - self._home_folder = conan_app.cache_folder + def __init__(self, cache, remote_manager, home_folder, global_conf, hook_manager): + self._cache = cache + self._home_folder = home_folder self._global_conf = global_conf - self._remote_manager = conan_app.remote_manager + self._remote_manager = remote_manager self._hook_manager = hook_manager # These are the nodes with pref (not including PREV) that have been evaluated self._evaluated = {} # {pref: [nodes]} - compat_folder = HomePaths(conan_app.cache_folder).compatibility_plugin_path + compat_folder = HomePaths(home_folder).compatibility_plugin_path self._compatibility = BinaryCompatibility(compat_folder, hook_manager) unknown_mode = global_conf.get("core.package_id:default_unknown_mode", default="semver_mode") non_embed = global_conf.get("core.package_id:default_non_embed_mode", default="minor_mode") diff --git a/conan/internal/graph/installer.py b/conan/internal/graph/installer.py index 230f61e668f..384519d4449 100644 --- a/conan/internal/graph/installer.py +++ b/conan/internal/graph/installer.py @@ -34,11 +34,11 @@ def build_id(conan_file): class _PackageBuilder: - def __init__(self, app, hook_manager): - self._cache = app.cache + def __init__(self, cache, remote_manager, cache_folder, hook_manager): + self._cache = cache self._hook_manager = hook_manager - self._remote_manager = app.remote_manager - self._home_folder = app.cache_folder + self._remote_manager = remote_manager + self._home_folder = cache_folder def _get_build_folder(self, conanfile, package_layout): # Build folder can use a different package_ID if build_id() is defined. @@ -165,14 +165,14 @@ class BinaryInstaller: locally in case they are not found in remotes """ - def __init__(self, app, global_conf, editable_packages, hook_manager): - self._app = app + def __init__(self, api, global_conf, editable_packages, hook_manager): + helpers = api._api_helpers # noqa self._editable_packages = editable_packages - self._cache = app.cache - self._remote_manager = app.remote_manager + self._cache = helpers.cache + self._remote_manager = helpers.remote_manager self._hook_manager = hook_manager self._global_conf = global_conf - self._home_folder = app.cache_folder + self._home_folder = api.home_folder def _install_source(self, node, remotes, need_conf=False): conanfile = node.conanfile @@ -394,7 +394,8 @@ def _handle_node_build(self, package, recipe_layout, pkg_layout): with pkg_layout.package_lock(): pkg_layout.package_remove() with pkg_layout.set_dirty_context_manager(): - builder = _PackageBuilder(self._app, self._hook_manager) + builder = _PackageBuilder(self._cache, self._remote_manager, self._home_folder, + self._hook_manager) pref = builder.build_package(node, recipe_layout, pkg_layout) assert node.prev, "Node PREV shouldn't be empty" assert node.pref.revision, "Node PREF revision shouldn't be empty" diff --git a/conan/internal/graph/proxy.py b/conan/internal/graph/proxy.py index f47c11f23e7..2fa1fcb3b73 100644 --- a/conan/internal/graph/proxy.py +++ b/conan/internal/graph/proxy.py @@ -8,11 +8,11 @@ class ConanProxy: - def __init__(self, conan_app, editable_packages, legacy_update=None): + def __init__(self, cache, remote_manager, editable_packages, legacy_update=None): # collaborators self._editable_packages = editable_packages - self._cache = conan_app.cache - self._remote_manager = conan_app.remote_manager + self._cache = cache + self._remote_manager = remote_manager self._resolved = {} # Cache of the requested recipes to optimize calls self._legacy_update = legacy_update diff --git a/conan/internal/graph/python_requires.py b/conan/internal/graph/python_requires.py index 689bbb9f868..517f6c1ce1a 100644 --- a/conan/internal/graph/python_requires.py +++ b/conan/internal/graph/python_requires.py @@ -65,9 +65,9 @@ def add_pyrequire(self, py_require): class PyRequireLoader: - def __init__(self, conan_app, global_conf): - self._proxy = conan_app.proxy - self._range_resolver = conan_app.range_resolver + def __init__(self, proxy, range_resolver, global_conf): + self._proxy = proxy + self._range_resolver = range_resolver self._cached_py_requires = {} self._resolve_prereleases = global_conf.get("core.version_ranges:resolve_prereleases") diff --git a/conan/internal/graph/range_resolver.py b/conan/internal/graph/range_resolver.py index f6d3f5795df..f5938d489f5 100644 --- a/conan/internal/graph/range_resolver.py +++ b/conan/internal/graph/range_resolver.py @@ -7,10 +7,10 @@ class RangeResolver: - def __init__(self, conan_app, global_conf, editable_packages): - self._cache = conan_app.cache + def __init__(self, cache, remote_manager, global_conf, editable_packages): + self._cache = cache self._editable_packages = editable_packages - self._remote_manager = conan_app.remote_manager + self._remote_manager = remote_manager 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 = {} diff --git a/conan/internal/model/workspace.py b/conan/internal/model/workspace.py index 41c793ee4c0..80b96f64978 100644 --- a/conan/internal/model/workspace.py +++ b/conan/internal/model/workspace.py @@ -118,7 +118,7 @@ def load_conanfile(self, conanfile_path): from conan.internal.conan_app import ConanFileHelpers, CmdWrapper cmd_wrap = CmdWrapper(HomePaths(self._conan_api.home_folder).wrapper_path) helpers = ConanFileHelpers(None, cmd_wrap, self._conan_api._api_helpers.global_conf, - cache=None, home_folder=self._conan_api.home_folder) + cache=None, home_folder=self._conan_api.home_folder, conan_api=self._conan_api) loader = ConanFileLoader(pyreq_loader=None, conanfile_helpers=helpers) conanfile = loader.load_named(conanfile_path, name=None, version=None, user=None, channel=None, remotes=None, graph_lock=None) diff --git a/conan/internal/rest/remote_manager.py b/conan/internal/rest/remote_manager.py index 16ec33ab22f..c0cb085374b 100644 --- a/conan/internal/rest/remote_manager.py +++ b/conan/internal/rest/remote_manager.py @@ -12,7 +12,7 @@ from conan.internal.rest.rest_client_local_recipe_index import RestApiClientLocalRecipesIndex from conan.api.model import Remote from conan.api.output import ConanOutput -from conan.internal.cache.conan_reference_layout import METADATA, PackageLayout +from conan.internal.cache.conan_reference_layout import METADATA from conan.internal.rest.pkg_sign import PkgSignaturesPlugin from conan.internal.errors import ConanConnectionError, NotFoundException, PackageNotFoundException from conan.errors import ConanException diff --git a/conan/test/utils/mocks.py b/conan/test/utils/mocks.py index 0c16a7c4ecd..8d83b13720d 100644 --- a/conan/test/utils/mocks.py +++ b/conan/test/utils/mocks.py @@ -72,7 +72,7 @@ def __init__(self, settings=None, options=None, runner=None, display_name=""): self.win_bash = None self.command = None self._commands = [] - self._conan_helpers = ConanFileHelpers(None, None, self.conf, None, None) + self._conan_helpers = ConanFileHelpers(None, None, self.conf, None, None, None) def run(self, *args, **kwargs): self.command = args[0] diff --git a/test/integration/cache/storage_path_test.py b/test/integration/cache/storage_path_test.py index 2cac6d7b2ee..f326aab50e2 100644 --- a/test/integration/cache/storage_path_test.py +++ b/test/integration/cache/storage_path_test.py @@ -22,7 +22,7 @@ def test_storage_path(): def test_wrong_home_error(): client = TestClient(light=True) client.save_home({"global.conf": "core.cache:storage_path=//"}) - client.run("list *") + client.run("list *", assert_error=True) assert "Couldn't initialize storage in" in client.out From 462a5236b4dd6a8da20dd5e00121644c4a9c65f5 Mon Sep 17 00:00:00 2001 From: rikka0612 <104909326+ReinerBRO@users.noreply.github.com> Date: Fri, 20 Mar 2026 18:01:11 +0800 Subject: [PATCH 056/110] CMakeToolchain: disable CMake package registry exports (#19766) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * CMakeToolchain: disable package registry exports * Limit CMake package registry override to supported variable * Apply suggestions from code review Co-authored-by: Abril Rincón Blanco <5364255+AbrilRBS@users.noreply.github.com> * Move package registry handling to output dirs --------- Co-authored-by: Abril Rincón Blanco <5364255+AbrilRBS@users.noreply.github.com> --- conan/tools/cmake/toolchain/blocks.py | 7 +++++++ .../toolchains/cmake/test_cmaketoolchain.py | 12 ++++++++++++ 2 files changed, 19 insertions(+) diff --git a/conan/tools/cmake/toolchain/blocks.py b/conan/tools/cmake/toolchain/blocks.py index d798e9a8753..c10ff941305 100644 --- a/conan/tools/cmake/toolchain/blocks.py +++ b/conan/tools/cmake/toolchain/blocks.py @@ -1260,6 +1260,13 @@ def template(self): return textwrap.dedent("""\ # Definition of CMAKE_INSTALL_XXX folders + # Ensure export(PACKAGE) honors CMAKE_EXPORT_PACKAGE_REGISTRY even if the + # project sets cmake_minimum_required() lower than 3.15. + cmake_policy(SET CMP0090 NEW) + if(NOT DEFINED CMAKE_EXPORT_PACKAGE_REGISTRY) + set(CMAKE_EXPORT_PACKAGE_REGISTRY OFF) + endif() + {% if package_folder %} set(CMAKE_INSTALL_PREFIX "{{package_folder}}") {% endif %} diff --git a/test/integration/toolchains/cmake/test_cmaketoolchain.py b/test/integration/toolchains/cmake/test_cmaketoolchain.py index dccd2708243..94af6901812 100644 --- a/test/integration/toolchains/cmake/test_cmaketoolchain.py +++ b/test/integration/toolchains/cmake/test_cmaketoolchain.py @@ -427,6 +427,18 @@ def test_runtime_lib_dirs_multiconf(lib_dir_setup): assert "" in runtime_lib_dirs +def test_disable_package_registry(): + # https://github.com/conan-io/conan/issues/19749 + client = TestClient(light=True) + client.save({"conanfile.txt": "[generators]\nCMakeToolchain"}) + client.run("install .") + toolchain = client.load("conan_toolchain.cmake") + before_output_dirs, output_dirs_block = toolchain.split("########## 'output_dirs' block #############\n", 1) + assert "CMAKE_EXPORT_PACKAGE_REGISTRY" not in before_output_dirs + assert "cmake_policy(SET CMP0090 NEW)" in output_dirs_block + assert "set(CMAKE_EXPORT_PACKAGE_REGISTRY OFF)" in toolchain + + @pytest.mark.skipif(platform.system() != "Darwin", reason="Only OSX") def test_cmaketoolchain_cmake_system_processor_cross_apple(): """ From 217fc0011940272abdfa7f9de4d624207e6040f4 Mon Sep 17 00:00:00 2001 From: James Date: Fri, 20 Mar 2026 11:07:19 +0100 Subject: [PATCH 057/110] adding rcflags (#19693) --- conan/internal/model/conf.py | 1 + conan/tools/cmake/toolchain/blocks.py | 11 ++++++ conan/tools/gnu/autotoolstoolchain.py | 7 ++++ conan/tools/gnu/gnutoolchain.py | 7 ++++ conan/tools/microsoft/nmaketoolchain.py | 7 ++++ conan/tools/microsoft/toolchain.py | 10 ++++-- conan/tools/premake/toolchain.py | 8 +++++ conan/tools/qbs/qbsprofile.py | 1 + .../toolchains/cmake/test_cmaketoolchain.py | 25 +++++++++++++ .../toolchains/gnu/test_autotoolstoolchain.py | 27 ++++++++++++++ .../toolchains/gnu/test_gnutoolchain.py | 25 +++++++++++++ .../microsoft/test_msbuildtoolchain.py | 19 ++++++++++ .../microsoft/test_nmaketoolchain.py | 28 +++++++++++++++ .../premake/test_premaketoolchain.py | 26 ++++++++++++++ .../toolchains/qbs/test_qbsprofile.py | 35 +++++++++++++++++++ 15 files changed, 235 insertions(+), 2 deletions(-) create mode 100644 test/integration/toolchains/microsoft/test_nmaketoolchain.py create mode 100644 test/integration/toolchains/qbs/test_qbsprofile.py diff --git a/conan/internal/model/conf.py b/conan/internal/model/conf.py index 4aa6ff6d365..0ca7848a434 100644 --- a/conan/internal/model/conf.py +++ b/conan/internal/model/conf.py @@ -154,6 +154,7 @@ "tools.build:defines": "List of extra definition flags used by different toolchains like CMakeToolchain, AutotoolsToolchain and MesonToolchain", "tools.build:sharedlinkflags": "List of extra flags used by different toolchains like CMakeToolchain, AutotoolsToolchain and MesonToolchain", "tools.build:exelinkflags": "List of extra flags used by different toolchains like CMakeToolchain, AutotoolsToolchain and MesonToolchain", + "tools.build:rcflags": "List of extra RC (resource compiler) flags used by different toolchains like CMakeToolchain, MSBuildToolchain and MesonToolchain", "tools.build:linker_scripts": "List of linker script files to pass to the linker used by different toolchains like CMakeToolchain, AutotoolsToolchain, and MesonToolchain", # Toolchain installation "tools.build:install_strip": "(boolean) Strip the binaries when installing them with CMake, Meson and Autotools", diff --git a/conan/tools/cmake/toolchain/blocks.py b/conan/tools/cmake/toolchain/blocks.py index c10ff941305..357e8f5d7c4 100644 --- a/conan/tools/cmake/toolchain/blocks.py +++ b/conan/tools/cmake/toolchain/blocks.py @@ -787,6 +787,9 @@ class ExtraFlagsBlock(Block): {% if exelinkflags %} string(APPEND CONAN_EXE_LINKER_FLAGS{{suffix}} "{% for exelinkflag in exelinkflags %} {{ exelinkflag }}{% endfor %}") {% endif %} + {% if rcflags %} + string(APPEND CONAN_RC_FLAGS{{suffix}} "{% for rcflag in rcflags %} {{ rcflag }}{% endfor %}") + {% endif %} {% if defines %} {% if config %} {% for define in defines %} @@ -837,6 +840,7 @@ def context(self): cflags = self._toolchain.extra_cflags + self._conanfile.conf.get("tools.build:cflags", default=[], check_type=list) sharedlinkflags = self._toolchain.extra_sharedlinkflags + self._conanfile.conf.get("tools.build:sharedlinkflags", default=[], check_type=list) exelinkflags = self._toolchain.extra_exelinkflags + self._conanfile.conf.get("tools.build:exelinkflags", default=[], check_type=list) + rcflags = self._conanfile.conf.get("tools.build:rcflags", default=[], check_type=list) defines = self._conanfile.conf.get("tools.build:defines", default=[], check_type=list) # See https://github.com/conan-io/conan/issues/13374 @@ -859,6 +863,7 @@ def context(self): "cflags": cflags, "sharedlinkflags": sharedlinkflags, "exelinkflags": exelinkflags, + "rcflags": rcflags, "defines": [define.replace('"', '\\"') for define in defines], } @@ -881,6 +886,9 @@ class CMakeFlagsInitBlock(Block): if(DEFINED CONAN_EXE_LINKER_FLAGS_${config}) string(APPEND CMAKE_EXE_LINKER_FLAGS_${config}_INIT " ${CONAN_EXE_LINKER_FLAGS_${config}}") endif() + if(DEFINED CONAN_RC_FLAGS_${config}) + string(APPEND CMAKE_RC_FLAGS_${config}_INIT " ${CONAN_RC_FLAGS_${config}}") + endif() endforeach() if(DEFINED CONAN_CXX_FLAGS) @@ -895,6 +903,9 @@ class CMakeFlagsInitBlock(Block): if(DEFINED CONAN_EXE_LINKER_FLAGS) string(APPEND CMAKE_EXE_LINKER_FLAGS_INIT " ${CONAN_EXE_LINKER_FLAGS}") endif() + if(DEFINED CONAN_RC_FLAGS) + string(APPEND CMAKE_RC_FLAGS_INIT " ${CONAN_RC_FLAGS}") + endif() if(DEFINED CONAN_OBJCXX_FLAGS) string(APPEND CMAKE_OBJCXX_FLAGS_INIT " ${CONAN_OBJCXX_FLAGS}") endif() diff --git a/conan/tools/gnu/autotoolstoolchain.py b/conan/tools/gnu/autotoolstoolchain.py index 52b644382a9..4f8b3899c03 100644 --- a/conan/tools/gnu/autotoolstoolchain.py +++ b/conan/tools/gnu/autotoolstoolchain.py @@ -258,6 +258,11 @@ def defines(self): ret = [self.ndebug, self.gcc_cxx11_abi] + self.extra_defines + conf_flags return self._filter_list_empty_fields(ret) + @property + def rcflags(self): + conf_flags = self._conanfile.conf.get("tools.build:rcflags", default=[], check_type=list) + return self._filter_list_empty_fields(conf_flags) + def _include_obj_arc_flags(self, env): enable_arc = self._conanfile.conf.get("tools.apple:enable_arc", check_type=bool) fobj_arc = "" @@ -301,6 +306,8 @@ def environment(self): env.append("CXXFLAGS", self.cxxflags) env.append("CFLAGS", self.cflags) env.append("LDFLAGS", self.ldflags) + if self.rcflags: + env.append("RCFLAGS", self.rcflags) env.prepend_path("PKG_CONFIG_PATH", self._conanfile.generators_folder) # Objective C/C++ self._include_obj_arc_flags(env) diff --git a/conan/tools/gnu/gnutoolchain.py b/conan/tools/gnu/gnutoolchain.py index 5a1701d9b58..78607ad19d2 100644 --- a/conan/tools/gnu/gnutoolchain.py +++ b/conan/tools/gnu/gnutoolchain.py @@ -325,6 +325,11 @@ def defines(self): ret = [self.ndebug, self.gcc_cxx11_abi] + self.extra_defines + conf_flags return self._filter_list_empty_fields(ret) + @property + def rcflags(self): + conf_flags = self._conanfile.conf.get("tools.build:rcflags", default=[], check_type=list) + return self._filter_list_empty_fields(conf_flags) + def _get_default_configure_shared_flags(self): args = {} # Just add these flags if there's a shared option defined (never add to exe's) @@ -373,6 +378,8 @@ def _environment(self): env.append("CXXFLAGS", self.cxxflags) env.append("CFLAGS", self.cflags) env.append("LDFLAGS", self.ldflags) + if self.rcflags: + env.append("RCFLAGS", self.rcflags) env.prepend_path("PKG_CONFIG_PATH", self._conanfile.generators_folder) # Objective C/C++ self._include_obj_arc_flags(env) diff --git a/conan/tools/microsoft/nmaketoolchain.py b/conan/tools/microsoft/nmaketoolchain.py index c20b575f11a..ff205af8411 100644 --- a/conan/tools/microsoft/nmaketoolchain.py +++ b/conan/tools/microsoft/nmaketoolchain.py @@ -88,6 +88,11 @@ def _link(self): return ["/nologo"] + self._format_options(ldflags) + @property + def _rcflags(self): + rcflags = self._conanfile.conf.get("tools.build:rcflags", default=[], check_type=list) + return self._format_options(rcflags) if rcflags else [] + def environment(self): env = Environment() # Injection of compile flags in CL env-var: @@ -96,6 +101,8 @@ def environment(self): # Injection of link flags in _LINK_ env-var: # https://learn.microsoft.com/en-us/cpp/build/reference/linking env.append("_LINK_", self._link) + if self._rcflags: + env.append("RCFLAGS", self._rcflags) # Also define some special env-vars which can override special NMake macros: # https://learn.microsoft.com/en-us/cpp/build/reference/special-nmake-macros conf_compilers = self._conanfile.conf.get("tools.build:compiler_executables", default={}, diff --git a/conan/tools/microsoft/toolchain.py b/conan/tools/microsoft/toolchain.py index e1f58bcf283..1086f4062d7 100644 --- a/conan/tools/microsoft/toolchain.py +++ b/conan/tools/microsoft/toolchain.py @@ -40,6 +40,7 @@ class MSBuildToolchain: {{ defines }}%(PreprocessorDefinitions) + {% if rc_flags %}{{ rc_flags }} %(AdditionalOptions){% endif %} @@ -69,6 +70,8 @@ def __init__(self, conanfile): self.cflags = [] #: List of all the LD linker flags self.ldflags = [] + #: List of all the RC (resource compiler) flags + self.rcflags = [] #: The build type. By default, the ``conanfile.settings.build_type`` value self.configuration = conanfile.settings.build_type #: The runtime flag. By default, it'll be based on the `compiler.runtime` setting. @@ -123,13 +126,14 @@ def context_config_toolchain(self): def format_macro(key, value): return '%s=%s' % (key, value) if value is not None else key - cxxflags, cflags, defines, sharedlinkflags, exelinkflags = self._get_extra_flags() + cxxflags, cflags, defines, sharedlinkflags, exelinkflags, rcflags = self._get_extra_flags() preprocessor_definitions = "".join(["%s;" % format_macro(k, v) for k, v in self.preprocessor_definitions.items()]) defines = preprocessor_definitions + "".join("%s;" % d for d in defines) self.cxxflags.extend(cxxflags) self.cflags.extend(cflags) self.ldflags.extend(sharedlinkflags + exelinkflags) + self.rcflags.extend(rcflags) cppstd = "stdcpp%s" % self.cppstd if self.cppstd else "" cstd = f"stdc{self.cstd}" if self.cstd else "" @@ -154,6 +158,7 @@ def format_macro(key, value): 'defines': defines, 'compiler_flags': " ".join(self.cxxflags + self.cflags), 'linker_flags': " ".join(self.ldflags), + 'rc_flags': " ".join(self.rcflags), "cppstd": cppstd, "cstd": cstd, "runtime_library": runtime_library, @@ -223,8 +228,9 @@ def _get_extra_flags(self): check_type=list) exelinkflags = self._conanfile.conf.get("tools.build:exelinkflags", default=[], check_type=list) + rcflags = self._conanfile.conf.get("tools.build:rcflags", default=[], check_type=list) defines = self._conanfile.conf.get("tools.build:defines", default=[], check_type=list) - return cxxflags, cflags, defines, sharedlinkflags, exelinkflags + return cxxflags, cflags, defines, sharedlinkflags, exelinkflags, rcflags def _get_toolset_props(conanfile): diff --git a/conan/tools/premake/toolchain.py b/conan/tools/premake/toolchain.py index 0131cf6dae8..5905d3fa614 100644 --- a/conan/tools/premake/toolchain.py +++ b/conan/tools/premake/toolchain.py @@ -31,6 +31,12 @@ def _generate_flags(self, conanfile): -- Link flags retrieved from LDFLAGS environment, conan.conf(tools.build:sharedlinkflags), conan.conf(tools.build:exelinkflags), extra_cxxflags and compiler settings linkoptions { {{ extra_ldflags }} } {% endif %} + {% if extra_rcflags %} + -- RC flags retrieved from conan.conf(tools.build:rcflags) + filter { files { "**.rc" } } + buildoptions { {{ extra_rcflags }} } + filter {} + {% endif %} {% if extra_defines %} -- Defines retrieved from DEFINES environment, conan.conf(tools.build:defines) and extra_defines defines { {{ extra_defines }} } @@ -75,6 +81,7 @@ def to_list(value): + arch_link_flags + thread_flags_list ) + extra_rc_flags = format_list(conanfile.conf.get("tools.build:rcflags", default=[], check_type=list)) return ( Template(template, trim_blocks=True, lstrip_blocks=True) @@ -83,6 +90,7 @@ def to_list(value): extra_cflags=extra_c_flags, extra_cxxflags=extra_cxx_flags, extra_ldflags=extra_ld_flags, + extra_rcflags=extra_rc_flags, ) .strip() ) diff --git a/conan/tools/qbs/qbsprofile.py b/conan/tools/qbs/qbsprofile.py index 8ad984a5380..a1c0abd3364 100644 --- a/conan/tools/qbs/qbsprofile.py +++ b/conan/tools/qbs/qbsprofile.py @@ -287,6 +287,7 @@ def map_list_property(key, qbs_property, extra): map_list_property("tools.build:cflags", "cpp.cFlags", self.extra_cflags) map_list_property("tools.build:cxxflags", "cpp.cxxFlags", self.extra_cxxflags) + map_list_property("tools.build:rcflags", "cpp.rcFlags", []) map_list_property("tools.build:defines", "cpp.defines", self.extra_defines) def ldflags(): diff --git a/test/integration/toolchains/cmake/test_cmaketoolchain.py b/test/integration/toolchains/cmake/test_cmaketoolchain.py index 94af6901812..4768835d989 100644 --- a/test/integration/toolchains/cmake/test_cmaketoolchain.py +++ b/test/integration/toolchains/cmake/test_cmaketoolchain.py @@ -535,6 +535,31 @@ def test_extra_flags_via_conf(): assert 'add_compile_definitions( "D1" "D2")' in toolchain +def test_cmaketoolchain_rcflags(): + """Test that tools.build:rcflags is applied to CONAN_RC_FLAGS and CMAKE_RC_FLAGS_INIT""" + profile = textwrap.dedent(""" + [settings] + os=Linux + arch=x86_64 + compiler=gcc + compiler.version=6 + compiler.libcxx=libstdc++11 + build_type=Release + + [conf] + tools.build:rcflags=["/nologo", "/flag-rc"] + """) + + client = TestClient() + conanfile = GenConanfile().with_settings("os", "arch", "compiler", "build_type")\ + .with_generator("CMakeToolchain") + client.save({"conanfile.py": conanfile, "profile": profile}) + client.run("install . --profile:host=profile") + toolchain = client.load("conan_toolchain.cmake") + assert 'string(APPEND CONAN_RC_FLAGS " /nologo /flag-rc")' in toolchain + assert 'string(APPEND CMAKE_RC_FLAGS_INIT " ${CONAN_RC_FLAGS}")' in toolchain + + def test_bitcode_enable_flag(): profile = textwrap.dedent(""" [settings] diff --git a/test/integration/toolchains/gnu/test_autotoolstoolchain.py b/test/integration/toolchains/gnu/test_autotoolstoolchain.py index 4e2acb5df3b..a87e1ac38f0 100644 --- a/test/integration/toolchains/gnu/test_autotoolstoolchain.py +++ b/test/integration/toolchains/gnu/test_autotoolstoolchain.py @@ -84,6 +84,33 @@ def generate(self): assert 'extra_ldflags sharedlinkflags exelinkflags' in toolchain +def test_autotoolstoolchain_rcflags(): + """Test that tools.build:rcflags is applied to RCFLAGS in the generated script.""" + os_ = platform.system() + os_ = "Macos" if os_ == "Darwin" else os_ + profile = textwrap.dedent(""" + [settings] + os=%s + arch=x86_64 + compiler=gcc + compiler.version=6 + compiler.libcxx=libstdc++11 + build_type=Release + + [conf] + tools.build:rcflags=["--rcflag1", "--rcflag2"] + """ % os_) + client = TestClient() + conanfile = GenConanfile().with_settings("os", "arch", "compiler", "build_type").with_generator("AutotoolsToolchain") + client.save({"conanfile.py": conanfile, "profile": profile}) + client.run("install . --profile:build=profile --profile:host=profile") + ext = ".bat" if os_ == "Windows" else ".sh" + toolchain = client.load("conanautotoolstoolchain{}".format(ext)) + assert "RCFLAGS" in toolchain + assert "--rcflag1" in toolchain + assert "--rcflag2" in toolchain + + def test_autotools_custom_environment(): client = TestClient() conanfile = textwrap.dedent(""" diff --git a/test/integration/toolchains/gnu/test_gnutoolchain.py b/test/integration/toolchains/gnu/test_gnutoolchain.py index 312c7c978b7..155a02c2661 100644 --- a/test/integration/toolchains/gnu/test_gnutoolchain.py +++ b/test/integration/toolchains/gnu/test_gnutoolchain.py @@ -92,6 +92,31 @@ def generate(self): assert 'extra_ldflags sharedlinkflags exelinkflags' in toolchain +def test_gnutoolchain_rcflags(): + """Test that tools.build:rcflags is applied to RCFLAGS in the generated script.""" + profile = textwrap.dedent(""" + [settings] + os=Linux + arch=x86_64 + compiler=gcc + compiler.version=6 + compiler.libcxx=libstdc++11 + build_type=Release + + [conf] + tools.build:rcflags=["--rcflag1", "--rcflag2"] + """) + client = TestClient() + conanfile = (GenConanfile().with_settings("os", "arch", "compiler", "build_type") + .with_generator("GnuToolchain")) + client.save({"conanfile.py": conanfile, "profile": profile}) + client.run("install . --profile:build=profile --profile:host=profile") + toolchain = client.load("conangnutoolchain.sh") + assert "RCFLAGS" in toolchain + assert "--rcflag1" in toolchain + assert "--rcflag2" in toolchain + + def test_autotools_custom_environment(): client = TestClient() conanfile = textwrap.dedent(""" diff --git a/test/integration/toolchains/microsoft/test_msbuildtoolchain.py b/test/integration/toolchains/microsoft/test_msbuildtoolchain.py index 327e3ac1c01..f725c906599 100644 --- a/test/integration/toolchains/microsoft/test_msbuildtoolchain.py +++ b/test/integration/toolchains/microsoft/test_msbuildtoolchain.py @@ -43,3 +43,22 @@ def test_msbuildtoolchain_props_with_extra_flags(): assert expected_cl_compile in toolchain assert expected_link in toolchain assert expected_resource_compile in toolchain + + +def test_msbuildtoolchain_rcflags(): + """Test that tools.build:rcflags is applied to ResourceCompile AdditionalOptions""" + profile = textwrap.dedent("""\ + include(default) + [settings] + arch=x86_64 + [conf] + tools.build:rcflags=["/flag-rc1", "/flag-rc2"] + """) + client = TestClient() + client.run("new msbuild_lib -d name=hello -d version=0.1") + client.save({"myprofile": profile}) + client.run("install . -pr myprofile") + toolchain = client.load(os.path.join("conan", "conantoolchain_release_x64.props")) + expected_resource_compile = ("/flag-rc1 /flag-rc2 %(AdditionalOptions)" + "") + assert expected_resource_compile in toolchain diff --git a/test/integration/toolchains/microsoft/test_nmaketoolchain.py b/test/integration/toolchains/microsoft/test_nmaketoolchain.py new file mode 100644 index 00000000000..0cf6722b9b8 --- /dev/null +++ b/test/integration/toolchains/microsoft/test_nmaketoolchain.py @@ -0,0 +1,28 @@ +import os +import platform +import textwrap + +import pytest + +from conan.test.assets.genconanfile import GenConanfile +from conan.test.utils.tools import TestClient + + +@pytest.mark.skipif(platform.system() != "Windows", reason="NMake toolchain is Windows-only") +def test_nmaketoolchain_rcflags(): + """Test that tools.build:rcflags is applied to RCFLAGS in the NMake toolchain environment.""" + profile = textwrap.dedent("""\ + include(default) + [settings] + arch=x86_64 + [conf] + tools.build:rcflags=["/nologo", "/flag-rc1"] + """) + client = TestClient() + conanfile = GenConanfile().with_settings("os", "arch", "compiler", "build_type").with_generator("NMakeToolchain") + client.save({"conanfile.py": conanfile, "profile": profile}) + client.run("install . -pr profile") + script = client.load("conannmaketoolchain.bat") + assert "RCFLAGS" in script + assert "/nologo" in script or "nologo" in script + assert "/flag-rc1" in script or "flag-rc1" in script diff --git a/test/integration/toolchains/premake/test_premaketoolchain.py b/test/integration/toolchains/premake/test_premaketoolchain.py index b764579a021..498893e5c8a 100644 --- a/test/integration/toolchains/premake/test_premaketoolchain.py +++ b/test/integration/toolchains/premake/test_premaketoolchain.py @@ -60,6 +60,32 @@ def test_extra_flags_via_conf(): assert 'defines { "define1=0", "_GLIBCXX_USE_CXX11_ABI=0" }' in content +def test_premaketoolchain_rcflags(): + """Test that tools.build:rcflags is applied to buildoptions for **.rc files.""" + profile = textwrap.dedent(""" + [settings] + os=Linux + arch=x86_64 + compiler=gcc + compiler.version=9 + compiler.libcxx=libstdc++ + build_type=Release + + [conf] + tools.build:rcflags=["-rcflag1", "-rcflag2"] + """) + tc = TestClient() + tc.save({ + "conanfile.txt": "[generators]\nPremakeToolchain", + "profile": profile, + }) + tc.run("install . -pr:a=profile") + content = tc.load(PremakeToolchain.filename) + assert '**.rc' in content + assert "-rcflag1" in content + assert "-rcflag2" in content + + def test_project_configuration(): tc = TestClient(path_with_spaces=False) profile = textwrap.dedent( diff --git a/test/integration/toolchains/qbs/test_qbsprofile.py b/test/integration/toolchains/qbs/test_qbsprofile.py new file mode 100644 index 00000000000..485f8d9276c --- /dev/null +++ b/test/integration/toolchains/qbs/test_qbsprofile.py @@ -0,0 +1,35 @@ +import os +import platform +import textwrap + +import pytest + +from conan.test.assets.genconanfile import GenConanfile +from conan.test.utils.tools import TestClient + + +@pytest.mark.skipif(platform.system() == "Windows", reason="QbsProfile requires host compiler (gcc) to be found") +def test_qbsprofile_rcflags(): + """Test that tools.build:rcflags is applied to cpp.rcFlags in qbs_settings.txt.""" + profile = textwrap.dedent(""" + [settings] + os=Linux + arch=x86_64 + compiler=gcc + compiler.version=9 + compiler.libcxx=libstdc++ + build_type=Release + + [conf] + tools.build:rcflags=["-rcflag1", "-rcflag2"] + """) + client = TestClient() + conanfile = GenConanfile().with_settings("os", "arch", "compiler", "build_type").with_generator("QbsProfile") + client.save({"conanfile.py": conanfile, "profile": profile}) + client.run("install . --profile:build=profile --profile:host=profile") + settings_path = os.path.join(client.current_folder, "qbs_settings.txt") + assert os.path.exists(settings_path) + content = client.load("qbs_settings.txt") + assert "cpp.rcFlags" in content + assert "-rcflag1" in content + assert "-rcflag2" in content From a5cd2f578c614e9a2e64f1bff7cb34e42fb8813f Mon Sep 17 00:00:00 2001 From: James Date: Mon, 23 Mar 2026 14:55:06 +0100 Subject: [PATCH 058/110] CMake.configure() extra args, via CMakePresets (#19639) * CMake.configure() extra args, via CMakePresets * review * improve conf description --- conan/internal/model/conf.py | 3 ++- conan/tools/cmake/cmake.py | 4 ++++ test/unittests/tools/cmake/test_cmake_test.py | 17 +++++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/conan/internal/model/conf.py b/conan/internal/model/conf.py index 0ca7848a434..84a41389f4f 100644 --- a/conan/internal/model/conf.py +++ b/conan/internal/model/conf.py @@ -100,7 +100,8 @@ "tools.cmake.cmake_layout:test_folder": "(Experimental) Allow configuring the base folder of the build for test_package", "tools.cmake:cmake_program": "Path to CMake executable", "tools.cmake.cmakedeps:new": "Use the new CMakeDeps generator", - "tools.cmake:ctest_args": "To inject list of arguments to CMake.ctest() runner", + "tools.cmake:ctest_args": "Add extra arguments to CMake.ctest() runner command line", + "tools.cmake:configure_args": "Add extra arguments to CMake.configure() command line ", "tools.cmake:install_strip": "(Deprecated) Add --strip to cmake.install(). Use tools.build:install_strip instead", "tools.deployer:symlinks": "Set to False to disable deployers copying symlinks", "tools.files.download:retry": "Number of retries in case of failure when downloading", diff --git a/conan/tools/cmake/cmake.py b/conan/tools/cmake/cmake.py index 1d99223d068..46e8fc8bc94 100644 --- a/conan/tools/cmake/cmake.py +++ b/conan/tools/cmake/cmake.py @@ -121,6 +121,10 @@ def configure(self, variables=None, build_script_folder=None, cli_args=None, arg_list.append('"{}"'.format(cmakelist_folder)) + extra_args = self._conanfile.conf.get("tools.cmake:configure_args", check_type=list, + default=[]) + arg_list.extend(extra_args) + if not cli_args or ("--log-level" not in cli_args and "--loglevel" not in cli_args): arg_list.extend(self._cmake_log_levels_args) diff --git a/test/unittests/tools/cmake/test_cmake_test.py b/test/unittests/tools/cmake/test_cmake_test.py index 8ed861fd1aa..2be03004f2d 100644 --- a/test/unittests/tools/cmake/test_cmake_test.py +++ b/test/unittests/tools/cmake/test_cmake_test.py @@ -61,6 +61,23 @@ def test_cli_args_configure(): assert "--graphviz=foo.dot" in conanfile.command +def test_cli_args_configure_extra_args(): + settings = Settings.loads(default_settings_yml) + + conanfile = ConanFileMock() + conanfile.conf = Conf() + conanfile.conf.define("tools.cmake:configure_args", ["-DCMAKE_PROJECT_INCLUDE_BEFORE=MyFile", + "--fresh"]) + conanfile.folders.generators = "." + conanfile.folders.set_base_generators(temp_folder()) + conanfile.settings = settings + + write_cmake_presets(conanfile, "toolchain", "Unix Makefiles", {}) + cmake = CMake(conanfile) + cmake.configure() + assert '-DCMAKE_PROJECT_INCLUDE_BEFORE=MyFile --fresh' in conanfile.command + + def test_run_ctest(): settings = Settings.loads(default_settings_yml) settings.os = "Windows" From 15148c93a6eeb99d526e1d212f59e0d0835467f0 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 23 Mar 2026 15:32:44 +0100 Subject: [PATCH 059/110] Minor improvements (#19778) * fix minor issues * wip * cache RemoteCredentials * fixing tests * new test --- conan/internal/api/profile/profile_loader.py | 2 +- conan/internal/cache/db/table.py | 2 +- conan/internal/rest/auth_manager.py | 9 +++++- conan/tools/cmake/toolchain/blocks.py | 2 +- .../test_cmakeconfigdeps_new_paths.py | 32 +++++++++---------- .../test_cmakeconfigdeps_sources.py | 6 ++-- .../configuration/test_auth_remote_plugin.py | 21 +++++++++++- test/integration/graph/test_system_tools.py | 2 +- 8 files changed, 49 insertions(+), 27 deletions(-) diff --git a/conan/internal/api/profile/profile_loader.py b/conan/internal/api/profile/profile_loader.py index 5de78063094..addf1f71b06 100644 --- a/conan/internal/api/profile/profile_loader.py +++ b/conan/internal/api/profile/profile_loader.py @@ -244,7 +244,7 @@ def get_profile(profile_text, base_profile=None): doc_platform_tool_requires = doc.platform_tool_requires or doc.system_tools or "" if doc.system_tools: ConanOutput().warning("Profile [system_tools] is deprecated," - " please use [platform_tool_requires]") + " please use [platform_tool_requires]", warn_tag="deprecated") def parse_replaces(replaces): result = [RecipeReference.loads(r) for r in replaces.splitlines()] diff --git a/conan/internal/cache/db/table.py b/conan/internal/cache/db/table.py index 71e0ebf5813..0ff12443d08 100644 --- a/conan/internal/cache/db/table.py +++ b/conan/internal/cache/db/table.py @@ -29,7 +29,7 @@ def __init__(self, filename): def db_connection(self): if not self._lock.acquire(timeout=20): m = traceback.format_exc() + "\n" - ConanOutput().error(m) + ConanOutput().error("Error while acquiring lock for DB: " + m) raise ConanException("Conan failed to acquire database lock in 20s. Maybe the system is " "under very heavy load. Please report it to Github tickets") # isolation_level=None, puts it in regular SQLITE autocommit mode, every diff --git a/conan/internal/rest/auth_manager.py b/conan/internal/rest/auth_manager.py index 299274bb942..486fc82ebcf 100644 --- a/conan/internal/rest/auth_manager.py +++ b/conan/internal/rest/auth_manager.py @@ -45,6 +45,7 @@ class ConanApiAuthManager: def __init__(self, requester, cache_folder, localdb, global_conf): self._requester = requester self._creds = _RemoteCreds(localdb) + self._remote_creds = None self._global_conf = global_conf self._cache_folder = cache_folder @@ -69,11 +70,17 @@ def call_rest_api_method(self, remote, method_name, *args, **kwargs): if self._get_credentials_and_authenticate(rest_client, user, remote): return self.call_rest_api_method(remote, method_name, *args, **kwargs) + def _get_remote_creds(self): + if self._remote_creds is None: + self._remote_creds = RemoteCredentials(self._cache_folder, self._global_conf) + return self._remote_creds + def _get_credentials_and_authenticate(self, rest_client, user, remote): """Try LOGIN_RETRIES to obtain a password from user input for which we can get a valid token from api_client. If a token is returned, credentials are stored in localdb and rest method is called""" - creds = RemoteCredentials(self._cache_folder, self._global_conf) + creds = self._get_remote_creds() + for _ in range(LOGIN_RETRIES): input_user, input_password, interactive = creds.auth(remote) try: diff --git a/conan/tools/cmake/toolchain/blocks.py b/conan/tools/cmake/toolchain/blocks.py index 357e8f5d7c4..00e06db1746 100644 --- a/conan/tools/cmake/toolchain/blocks.py +++ b/conan/tools/cmake/toolchain/blocks.py @@ -568,7 +568,7 @@ class FindFiles(Block): template = textwrap.dedent("""\ # Define paths to find packages, programs, libraries, etc. if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/conan_cmakedeps_paths.cmake") - message(STATUS "Conan toolchain: Including CMakeDeps generated conan_cmakedeps_paths.cmake") + message(STATUS "Conan toolchain: Including CMakeConfigDeps generated conan_cmakedeps_paths.cmake") include("${CMAKE_CURRENT_LIST_DIR}/conan_cmakedeps_paths.cmake") else() diff --git a/test/functional/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps_new_paths.py b/test/functional/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps_new_paths.py index 5432437a9ba..67b585ef63e 100644 --- a/test/functional/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps_new_paths.py +++ b/test/functional/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps_new_paths.py @@ -6,8 +6,6 @@ from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient -new_value = "will_break_next" - @pytest.fixture def client(): @@ -19,7 +17,7 @@ def client(): class Pkg(ConanFile): settings = "build_type", "os", "arch", "compiler" requires = "dep/0.1" - generators = "CMakeDeps", "CMakeToolchain" + generators = "CMakeConfigDeps", "CMakeToolchain" def layout(self): # Necessary to force config files in another location cmake_layout(self) def build(self): @@ -41,8 +39,8 @@ def build(self): def test_cmake_generated(client): c = client c.run("create dep") - c.run(f"build pkg -c tools.cmake.cmakedeps:new={new_value}") - assert "Conan toolchain: Including CMakeDeps generated conan_cmakedeps_paths.cmake" in c.out + c.run(f"build pkg") + assert "Conan toolchain: Including CMakeConfigDeps generated conan_cmakedeps_paths.cmake" in c.out assert "Conan: Target declared imported INTERFACE library 'dep::dep'" in c.out @@ -70,8 +68,8 @@ def package_info(self): c.save({"dep/conanfile.py": dep}) c.run("create dep") - c.run(f"build pkg -c tools.cmake.cmakedeps:new={new_value}") - assert "Conan toolchain: Including CMakeDeps generated conan_cmakedeps_paths.cmake" in c.out + c.run(f"build pkg") + assert "Conan toolchain: Including CMakeConfigDeps generated conan_cmakedeps_paths.cmake" in c.out assert "Hello from dep dep-Config.cmake!!!!!" in c.out @@ -79,7 +77,7 @@ class TestRuntimeDirs: def test_runtime_lib_dirs_multiconf(self): client = TestClient() - app = GenConanfile().with_requires("dep/1.0").with_generator("CMakeDeps")\ + app = GenConanfile().with_requires("dep/1.0").with_generator("CMakeConfigDeps")\ .with_settings("build_type") client.save({"lib/conanfile.py": GenConanfile(), "dep/conanfile.py": GenConanfile("dep").with_requires("onelib/1.0", @@ -89,8 +87,8 @@ def test_runtime_lib_dirs_multiconf(self): client.run("create lib --name=twolib --version=1.0") client.run("create dep --version=1.0") - client.run(f'install app -s build_type=Release -c tools.cmake.cmakedeps:new={new_value}') - client.run(f'install app -s build_type=Debug -c tools.cmake.cmakedeps:new={new_value}') + client.run(f'install app -s build_type=Release') + client.run(f'install app -s build_type=Debug') contents = client.load("app/conan_cmakedeps_paths.cmake") pattern_lib_dirs = r"set\(CONAN_RUNTIME_LIB_DIRS ([^)]*)\)" @@ -102,7 +100,7 @@ def test_runtime_lib_dirs_multiconf(self): @pytest.mark.tool("cmake") -class TestCMakeDepsPaths: +class TestCMakeConfigDepsPaths: @pytest.mark.parametrize("requires, tool_requires", [(True, False), (False, True), (True, True)]) def test_find_program_path(self, requires, tool_requires): @@ -134,7 +132,7 @@ class PkgConan(ConanFile): {requires} {tool_requires} settings = "os", "arch", "compiler", "build_type" - generators = "CMakeToolchain", "CMakeDeps" + generators = "CMakeToolchain", "CMakeConfigDeps" def build(self): cmake = CMake(self) cmake.configure() @@ -148,7 +146,7 @@ def build(self): endif() """) c.save({"conanfile.py": conanfile, "CMakeLists.txt": consumer}, clean_first=True) - c.run(f"build . -c tools.cmake.cmakedeps:new={new_value}") + c.run(f"build .") assert "Found hello prog" in c.out if requires and tool_requires: assert "There is already a 'tool/1.0' package contributing to CMAKE_PROGRAM_PATH" in c.out @@ -180,7 +178,7 @@ def package(self): class PkgConan(ConanFile): requires = "hello/1.0" settings = "os", "arch", "compiler", "build_type" - generators = "CMakeToolchain", "CMakeDeps" + generators = "CMakeToolchain", "CMakeConfigDeps" def build(self): cmake = CMake(self) cmake.configure() @@ -198,7 +196,7 @@ def build(self): endif() """) c.save({"conanfile.py": conanfile, "CMakeLists.txt": consumer}, clean_first=True) - c.run(f"build . -c tools.cmake.cmakedeps:new={new_value}") + c.run(f"build .") assert "Found hello header" in c.out assert "Found hello lib" in c.out @@ -242,7 +240,7 @@ def build(self): """) c.save({"conanfile.py": conanfile, "CMakeLists.txt": consumer}, clean_first=True) - c.run(f"build . -c tools.cmake.cmakedeps:new={new_value}") + c.run(f"build .") assert "MYOWNCMAKE FROM hello!" in c.out def test_include_modules_both_build_host(self): @@ -282,7 +280,7 @@ def build(self): """) c.save({"conanfile.py": conanfile, "CMakeLists.txt": consumer}, clean_first=True) - c.run(f"build . -c tools.cmake.cmakedeps:new={new_value}") + c.run(f"build .") assert "conanfile.py: There is already a 'hello/0.1' package " \ "contributing to CMAKE_MODULE_PATH" in c.out assert "MYOWNCMAKE FROM hello!" in c.out diff --git a/test/functional/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps_sources.py b/test/functional/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps_sources.py index 7e22f6f814c..40eb384df8c 100644 --- a/test/functional/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps_sources.py +++ b/test/functional/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps_sources.py @@ -38,8 +38,7 @@ def package_info(self): # Check that the hello library builds in test_package c.run(f"create . -c tools.cmake.cmakedeps:new={new_value}") # Check content of the generated files - c.run(f"install --requires=hello/1.0 -g=CMakeConfigDeps " - f"-c tools.cmake.cmakedeps:new={new_value}") + c.run(f"install --requires=hello/1.0 -g=CMakeConfigDeps") cmake = c.load("hello-Targets-release.cmake") assert "add_library(hello::hello INTERFACE IMPORTED)" in cmake assert "set_property(TARGET hello::hello APPEND PROPERTY INTERFACE_SOURCES\n"\ @@ -81,8 +80,7 @@ def package_info(self): # Check that the hello library builds in test_package c.run(f"create . -c tools.cmake.cmakedeps:new={new_value}") # Check the content of the generated files - c.run(f"install --requires=hello/1.0 -g=CMakeConfigDeps " - f"-c tools.cmake.cmakedeps:new={new_value}") + c.run(f"install --requires=hello/1.0 -g=CMakeConfigDeps") cmake = c.load("hello-Targets-release.cmake") assert "add_library(hello::hello INTERFACE IMPORTED)" in cmake assert "add_library(hello::my_comp INTERFACE IMPORTED)" in cmake diff --git a/test/integration/configuration/test_auth_remote_plugin.py b/test/integration/configuration/test_auth_remote_plugin.py index 8bc4ce07838..ffc57402daa 100644 --- a/test/integration/configuration/test_auth_remote_plugin.py +++ b/test/integration/configuration/test_auth_remote_plugin.py @@ -2,7 +2,7 @@ import pytest -from conan.test.utils.tools import TestClient +from conan.test.utils.tools import TestClient, TestServer class TestAuthRemotePlugin: @@ -57,3 +57,22 @@ def auth_remote_plugin(remote, user=None): # the input methods in this case the stdin provided by TestClient. assert ("Changed user of remote 'default' from 'None' (anonymous) to " "'admin' (authenticated)") in c.out + + def test_creds_caching_multiple_remotes(self): + """ The auth plugin can cache partial results and credentials to avoid repeated + multiple interactive requests, reusing the same inputs for all remotes + https://github.com/conan-io/conan/issues/19772 + """ + c = TestClient(servers={"server1": TestServer(users={"admin1": "passwd"}), + "server2": TestServer(users={"admin2": "passwd"})}) + auth_plugin = textwrap.dedent("""\ + count = 0 + def auth_remote_plugin(remote, user=None): + global count + count = count + 1 + return f"admin{count}", "passwd" + """) + c.save_home({"extensions/plugins/auth_remote.py": auth_plugin}) + c.run("remote auth *") # Triggers all remotes + assert "Authenticated in remote 'server1' with user 'admin1'" in c.out + assert "Authenticated in remote 'server2' with user 'admin2'" in c.out diff --git a/test/integration/graph/test_system_tools.py b/test/integration/graph/test_system_tools.py index e72b1ab88e3..5526bb9c267 100644 --- a/test/integration/graph/test_system_tools.py +++ b/test/integration/graph/test_system_tools.py @@ -27,7 +27,7 @@ def test_system_tool_require_non_matching(self): "profile": "[system_tools]\ntool/1.1"}) client.run("create tool") client.run("create . -pr=profile") - assert "WARN: Profile [system_tools] is deprecated" in client.out + assert "WARN: deprecated: Profile [system_tools] is deprecated" in client.out assert "tool/1.0#60ed6e65eae112df86da7f6d790887fd - Cache" in client.out @pytest.mark.parametrize("revision", ["", "#myrev"]) From 768531b456fcc06c384d038f9646e6aec9984339 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 23 Mar 2026 16:27:31 +0100 Subject: [PATCH 060/110] Fix NMake integrations to handle hexadecimal values in defines (#19779) * wip * wip --- conan/tools/microsoft/nmakedeps.py | 35 +++++++++++++------ conan/tools/microsoft/nmaketoolchain.py | 17 ++------- .../toolchains/test_nmake_toolchain.py | 5 ++- .../toolchains/microsoft/test_nmakedeps.py | 14 ++++---- 4 files changed, 38 insertions(+), 33 deletions(-) diff --git a/conan/tools/microsoft/nmakedeps.py b/conan/tools/microsoft/nmakedeps.py index e2af344b441..fe416885e01 100644 --- a/conan/tools/microsoft/nmakedeps.py +++ b/conan/tools/microsoft/nmakedeps.py @@ -5,6 +5,29 @@ from conan.tools.env import Environment +def format_defines(defines): + def is_hex_or_numeric(s): + try: + # Check for Hexadecimal (base 16) + int(s, 16) + return True + except ValueError: + return False + + formated_defines = [] + for define in defines: + if "=" in define: + # CL env-var can't accept '=' sign in /D option, it can be replaced by '#' sign: + # https://learn.microsoft.com/en-us/cpp/build/reference/cl-environment-variables + macro, value = define.split("=", 1) + if value and not is_hex_or_numeric(value): + # value quotes are escaped + value = f'\\"{value}\\"' + define = f"{macro}#{value}" + formated_defines.append(f'/D"{define}"') + return formated_defines + + class NMakeDeps: def __init__(self, conanfile): @@ -45,20 +68,10 @@ def format_lib(lib): ret.extend([format_lib(lib) for lib in cpp_info.system_libs or []]) link_args = " ".join(ret) - def format_define(define): - if "=" in define: - # CL env-var can't accept '=' sign in /D option, it can be replaced by '#' sign: - # https://learn.microsoft.com/en-us/cpp/build/reference/cl-environment-variables - macro, value = define.split("=", 1) - if value and not value.isnumeric(): - value = f'\"{value}\"' - define = f"{macro}#{value}" - return f"/D\"{define}\"" - cl_flags = [f'-I"{p}"' for p in cpp_info.includedirs or []] cl_flags.extend(cpp_info.cflags or []) cl_flags.extend(cpp_info.cxxflags or []) - cl_flags.extend([format_define(define) for define in cpp_info.defines or []]) + cl_flags.extend(format_defines(cpp_info.defines or [])) env = Environment() env.append("CL", " ".join(cl_flags)) diff --git a/conan/tools/microsoft/nmaketoolchain.py b/conan/tools/microsoft/nmaketoolchain.py index ff205af8411..c016291547e 100644 --- a/conan/tools/microsoft/nmaketoolchain.py +++ b/conan/tools/microsoft/nmaketoolchain.py @@ -2,6 +2,7 @@ from conan.internal import check_duplicated_generator from conan.tools.build.flags import build_type_flags, cppstd_flag, build_type_link_flags from conan.tools.env import Environment +from conan.tools.microsoft.nmakedeps import format_defines from conan.tools.microsoft.visual import msvc_runtime_flag, VCVars @@ -29,20 +30,6 @@ def __init__(self, conanfile): def _format_options(options): return [f"{opt[0].replace('-', '/')}{opt[1:]}" for opt in options if len(opt) > 1] - @staticmethod - def _format_defines(defines): - formated_defines = [] - for define in defines: - if "=" in define: - # CL env-var can't accept '=' sign in /D option, it can be replaced by '#' sign: - # https://learn.microsoft.com/en-us/cpp/build/reference/cl-environment-variables - macro, value = define.split("=", 1) - if value and not value.isnumeric(): - value = f'\\"{value}\\"' - define = f"{macro}#{value}" - formated_defines.append(f"/D\"{define}\"") - return formated_defines - @property def _cl(self): bt_flags = build_type_flags(self._conanfile) @@ -71,7 +58,7 @@ def _cl(self): defines.extend(self.extra_defines) return (["/nologo"] + self._format_options(bt_flags + rt_flags + cflags + cxxflags) + - self._format_defines(defines)) + format_defines(defines)) @property def _link(self): diff --git a/test/functional/toolchains/test_nmake_toolchain.py b/test/functional/toolchains/test_nmake_toolchain.py index 3d4e32acadc..96dc5fdf50a 100644 --- a/test/functional/toolchains/test_nmake_toolchain.py +++ b/test/functional/toolchains/test_nmake_toolchain.py @@ -15,7 +15,7 @@ ("msvc", "191", "dynamic", "14", "Release", [], [], [], [], []), ("msvc", "191", "dynamic", "14", "Release", ["TEST_DEFINITION1", "TEST_DEFINITION2=0", "TEST_DEFINITION3=", "TEST_DEFINITION4=TestPpdValue4", - "TEST_DEFINITION5=__declspec(dllexport)", "TEST_DEFINITION6=foo bar"], + "TEST_DEFINITION5=__declspec(dllexport)", "TEST_DEFINITION6=foo bar", "TEST_WINVER=0x0601"], ["/GL"], ["/GL"], ["/LTCG"], ["/LTCG"]), ("msvc", "191", "static", "17", "Debug", [], [], [], [], []), ], @@ -86,6 +86,9 @@ def build(self): client.run(f"build . {settings} {conf}") client.run_command("simple.exe") assert "dep/1.0" in client.out + # It is printed as an integer number by default + if "TEST_WINVER" in conf_preprocessors: + conf_preprocessors["TEST_WINVER"] = 1537 check_exe_run(client.out, "main", "msvc", version, build_type, "x86_64", cppstd, conf_preprocessors) diff --git a/test/integration/toolchains/microsoft/test_nmakedeps.py b/test/integration/toolchains/microsoft/test_nmakedeps.py index 4eee33c80b2..abed9daf82e 100644 --- a/test/integration/toolchains/microsoft/test_nmakedeps.py +++ b/test/integration/toolchains/microsoft/test_nmakedeps.py @@ -31,7 +31,8 @@ def package_info(self): self.cpp_info.components["pkg-4"].defines = ["TEST_DEFINITION4=foo", "TEST_DEFINITION5=__declspec(dllexport)", "TEST_DEFINITION6=foo bar", - "TEST_DEFINITION7=7"] + "TEST_DEFINITION7=7", + "TEST_WINVER=0x0601"] """) client.save({"conanfile.py": conanfile}) client.run("create . -s arch=x86_64") @@ -42,12 +43,13 @@ def package_info(self): # Checking that defines are added to CL for flag in ( r'/D"TEST_DEFINITION1"', '/D"TEST_DEFINITION2#0"', - r'/D"TEST_DEFINITION3#"', '/D"TEST_DEFINITION4#"foo""', - r'/D"TEST_DEFINITION5#"__declspec\(dllexport\)""', - r'/D"TEST_DEFINITION6#"foo bar""', - r'/D"TEST_DEFINITION7#7"' + r'/D"TEST_DEFINITION3#"', r'/D"TEST_DEFINITION4#\"foo\""', + r'/D"TEST_DEFINITION5#\"__declspec(dllexport)\""', + r'/D"TEST_DEFINITION6#\"foo bar\""', + r'/D"TEST_DEFINITION7#7"', + r'/D"TEST_WINVER#0x0601"' ): - assert re.search(fr'set "CL=%CL%.*\s{flag}(?:\s|")', bat_file) + assert flag in bat_file # Checking that libs and system libs are added to _LINK_ for flag in (r"pkg-1\.lib", r"pkg-2\.lib", r"pkg-3\.lib", r"pkg-4\.lib", r"ws2_32\.lib"): assert re.search(fr'set "_LINK_=%_LINK_%.*\s{flag}(?:\s|")', bat_file) From 4e0bf67c1361a7ad55d4cd0cf907de8bfb3d1ced Mon Sep 17 00:00:00 2001 From: James Date: Mon, 23 Mar 2026 16:31:10 +0100 Subject: [PATCH 061/110] fix DB connection timeout msg (#19781) --- conan/internal/cache/db/table.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conan/internal/cache/db/table.py b/conan/internal/cache/db/table.py index 0ff12443d08..2b5d6b0b0b2 100644 --- a/conan/internal/cache/db/table.py +++ b/conan/internal/cache/db/table.py @@ -28,7 +28,7 @@ def __init__(self, filename): @contextmanager def db_connection(self): if not self._lock.acquire(timeout=20): - m = traceback.format_exc() + "\n" + m = "".join(traceback.format_stack()) + "\n" ConanOutput().error("Error while acquiring lock for DB: " + m) raise ConanException("Conan failed to acquire database lock in 20s. Maybe the system is " "under very heavy load. Please report it to Github tickets") From a5d453a5198dd3f9f28015525450840c8524a4e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= <5364255+AbrilRBS@users.noreply.github.com> Date: Tue, 24 Mar 2026 11:13:28 +0100 Subject: [PATCH 062/110] Fix missing libraries in legacy `_LIBRARIES` variable definition (#19724) * Missing libraries in legacy variable * SHow all libraries in legacy libraries version * Dont remove old test * Typo * Styling * Fix --- conan/tools/cmake/cmakeconfigdeps/config.py | 13 ++++- .../cmakeconfigdeps/test_cmakeconfigdeps.py | 47 +++++++++++++++---- 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/conan/tools/cmake/cmakeconfigdeps/config.py b/conan/tools/cmake/cmakeconfigdeps/config.py index fd53c71e30c..b0cc4928749 100644 --- a/conan/tools/cmake/cmakeconfigdeps/config.py +++ b/conan/tools/cmake/cmakeconfigdeps/config.py @@ -99,9 +99,18 @@ def _get_legacy_vars(self): for i in incdirs] include_dirs = ";".join(incdirs) definitions = ";".join("-D" + cmake_escape_value(d) for d in aggregated_cppinfo.defines) - root_target_name = self._cmakedeps.get_property("cmake_target_name", self._conanfile) - libraries = root_target_name or f"{pkg_name}::{pkg_name}" + libraries = [] + if self._full_cpp_info.has_components: + for component in self._full_cpp_info.components.keys(): + root_target_name = self._cmakedeps.get_property("cmake_target_name", + self._conanfile, + comp_name=component) + libraries.append(root_target_name or f"{pkg_name}::{component}") + else: + root_target_name = self._cmakedeps.get_property("cmake_target_name", self._conanfile) + libraries.append(root_target_name or f"{pkg_name}::{pkg_name}") + libraries = " ".join(libraries) if libraries else "" return {"additional_variables_prefixes": prefixes, "version": self._conanfile.ref.version, "include_dirs": include_dirs, diff --git a/test/integration/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps.py b/test/integration/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps.py index 9e6d913c2f6..fa2afa42f40 100644 --- a/test/integration/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps.py +++ b/test/integration/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps.py @@ -790,16 +790,43 @@ def package_info(self): assert "# Requirement dep::dep -> pkg::compA (Full link: True)\n# Link feature: MYFET" in targets -def test_legacy_defines(): - # We used not to populate this. - # We do for backward compatibility with old check_symbol_exists and similar CMake code - tc = TestClient() - tc.save({"conanfile.py": GenConanfile("mypkg", "1.0") - .with_package_info({"defines": ["MY_DEFINE", "MYVAR=1"]})}) - tc.run("create") - tc.run("install --requires=mypkg/1.0 -g CMakeConfigDeps") - mypkg_config = tc.load("mypkg-config.cmake") - assert 'set(mypkg_DEFINITIONS "-DMY_DEFINE;-DMYVAR=1" )' in mypkg_config +class TestLegacyVariables: + def test_legacy_defines(self): + # We used not to populate this. + # We do for backward compatibility with old check_symbol_exists and similar CMake code + tc = TestClient() + tc.save({"conanfile.py": GenConanfile("mypkg", "1.0") + .with_package_info({"defines": ["MY_DEFINE", "MYVAR=1"]})}) + tc.run("create") + tc.run("install --requires=mypkg/1.0 -g CMakeConfigDeps") + mypkg_config = tc.load("mypkg-config.cmake") + assert 'set(mypkg_DEFINITIONS "-DMY_DEFINE;-DMYVAR=1" )' in mypkg_config + + def test_legacy_defines_multiple_components(self): + tc = TestClient() + tc.save({"conanfile.py": GenConanfile("mypkg", "1.0") + .with_package_info({"components": {"mypkg": {"defines": ["MY_DEFINE", "MYVAR=1"]}, + "lib2": {"defines": ["MY_DEFINE2", "MYVAR2=1"]}}}) + }) + tc.run("create") + tc.run("install --requires=mypkg/1.0 -g CMakeConfigDeps") + mypkg_config = tc.load("mypkg-config.cmake") + assert 'set(mypkg_DEFINITIONS "-DMY_DEFINE2;-DMYVAR2=1;-DMY_DEFINE;-DMYVAR=1" )' in mypkg_config + + def test_legacy_libraries(self): + tc = TestClient() + tc.save({"conanfile.py": GenConanfile("mypkg", "1.0") + .with_package_file("lib/mylib1.a", "library") + .with_package_file("lib/mylib2.a", "library") + .with_package_info({"components": {"mypkg": {"libs": ["mylib1"]}, + "lib2": {"libs": ["mylib2"]}}}) + }) + tc.run("create") + tc.run("install --requires=mypkg/1.0 -g CMakeConfigDeps") + mypkg_config = tc.load("mypkg-config.cmake") + # If there's no interface global target + # mypkg::lib2 is not added to the list of libraries + assert "set(mypkg_LIBRARIES mypkg::mypkg mypkg::lib2 )" in mypkg_config class TestPropertiesBuildContext: From d820ef12773bb28fb4139f57c703cca6b4ed3c9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Ram=C3=ADrez?= Date: Tue, 24 Mar 2026 16:39:16 +0100 Subject: [PATCH 063/110] [GH] Update clang major version (#19785) Update clang major version --- .../toolchains/cmake/test_cmake_toolchain_win_clang.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/functional/toolchains/cmake/test_cmake_toolchain_win_clang.py b/test/functional/toolchains/cmake/test_cmake_toolchain_win_clang.py index 638301e3350..00ef0d1feac 100644 --- a/test/functional/toolchains/cmake/test_cmake_toolchain_win_clang.py +++ b/test/functional/toolchains/cmake/test_cmake_toolchain_win_clang.py @@ -215,7 +215,7 @@ def test_msys2_clang(self, client): # clang compilations in Windows will use MinGW Makefiles by default assert 'cmake -G "MinGW Makefiles"' in client.out # TODO: Version is still not controlled - assert "main __clang_major__21" in client.out + assert "main __clang_major__22" in client.out # Not using libstdc++ assert "_GLIBCXX_USE_CXX11_ABI" not in client.out assert "main __cplusplus2014" in client.out @@ -274,7 +274,7 @@ def test_clang_pure_c(self, client): client.run(f"create . --name=pkg --version=0.1 -pr=clang") # clang compilations in Windows will use MinGW Makefiles by default assert 'cmake -G "MinGW Makefiles"' in client.out - assert "main __clang_major__21" in client.out + assert "main __clang_major__22" in client.out assert "GLIBCXX" not in client.out assert "cplusplus" not in client.out assert "main __GNUC__" in client.out From 49810d32a68f21d7aec4bd31674ef79d46d86c49 Mon Sep 17 00:00:00 2001 From: James Date: Wed, 25 Mar 2026 10:57:09 +0100 Subject: [PATCH 064/110] allowing tool-requires negated or (#19780) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * allowing tool-requires negated or * fix * new proposal * review * format --------- Co-authored-by: Francisco Ramírez --- conan/internal/graph/graph_builder.py | 11 +++++- .../profile_build_requires_test.py | 39 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/conan/internal/graph/graph_builder.py b/conan/internal/graph/graph_builder.py index 2e2c416771f..9be4e23901a 100644 --- a/conan/internal/graph/graph_builder.py +++ b/conan/internal/graph/graph_builder.py @@ -191,8 +191,17 @@ def _prepare_node(node, profile_host, profile_build, down_options, define_consum # Apply build_tools_requires from profile, overriding the declared ones profile = profile_host if node.context == CONTEXT_HOST else profile_build + for pattern, tool_requires in profile.tool_requires.items(): - if ref_matches(ref, pattern, is_consumer=conanfile._conan_is_consumer): # noqa + is_consumer = conanfile._conan_is_consumer # noqa + if pattern[0] in "!~" and pattern[1] == "(": # This is a negated OR operation + assert pattern[-1] == ")", (f"Malformed profile OR expression without" + f" closing parenthesis: {pattern}") + parts = pattern[2:-1].split("|") + matched = not any(ref_matches(ref, p, is_consumer=is_consumer) for p in parts) + else: + matched = ref_matches(ref, pattern, is_consumer=is_consumer) + if matched: for tool_require in tool_requires: # Do the override # Check if it is a self-loop of build-requires in build context and avoid it if ref and tool_require.name == ref.name and tool_require.user == ref.user and \ diff --git a/test/integration/build_requires/profile_build_requires_test.py b/test/integration/build_requires/profile_build_requires_test.py index f293f7b4e1d..955aaef90b3 100644 --- a/test/integration/build_requires/profile_build_requires_test.py +++ b/test/integration/build_requires/profile_build_requires_test.py @@ -1,3 +1,4 @@ +import json import os import platform import textwrap @@ -222,3 +223,41 @@ def test_tool_requires_version_range_loop(): c.run("create tool") c.run("install app -pr:b=build_profile") assert "tool/1.1" in c.out # It is skipped + + +def test_profile_tool_requires_negated_or_patterns(): + """Negated [tool_requires] patterns may use | so the rule applies + if the ref matches none of the branches.""" + c = TestClient(light=True) + profile_build = textwrap.dedent("""\ + [settings] + os=Linux + + [tool_requires] + mold/1.0 + !(zlib*|mold*):cmake/1.0 + """) + profile = textwrap.dedent("""\ + [tool_requires] + mold/1.0 + cmake/1.0 + gcc/1.0 + """) + c.save({"mold/conanfile.py": GenConanfile("mold", "1.0"), + "zlib/conanfile.py": GenConanfile("zlib", "1.0"), + "cmake/conanfile.py": GenConanfile("cmake", "1.0").with_tool_requires("zlib/1.0"), + "gcc/conanfile.py": GenConanfile("gcc", "1.0").with_tool_requires("zlib/1.0"), + "app/conanfile.py": GenConanfile("app", "1.0"), + "profile_build": profile_build, + "profile": profile}) + c.run("create mold") + c.run("create zlib") + c.run("create cmake") + c.run("create gcc") + c.run("graph info app -pr=profile -pr:b=profile_build --format=json") + graph = json.loads(c.stdout) + assert len(graph["graph"]["nodes"]) == 14 + c.assert_listed_require({"cmake/1.0": "Cache", + "gcc/1.0": "Cache", + "mold/1.0": "Cache", + "zlib/1.0": "Cache"}, build=True) From 3004d93c16600068c8e16adb52e54e886a5a5ae6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= <5364255+AbrilRBS@users.noreply.github.com> Date: Wed, 25 Mar 2026 13:02:38 +0100 Subject: [PATCH 065/110] Add cve version info in `conan audit` results (#19774) * Add cve version info * v4 has priority for sorting * Review * Scores for text * Testing * Testing * Update txt output, improve tests --- conan/cli/commands/audit.py | 4 +- conan/cli/formatters/audit/vulnerabilities.py | 48 ++++++++--- conan/internal/api/audit/providers.py | 6 ++ test/integration/command/test_audit.py | 86 ++++++++++++++++++- 4 files changed, 128 insertions(+), 16 deletions(-) diff --git a/conan/cli/commands/audit.py b/conan/cli/commands/audit.py index c5f08802d99..32654e1dd66 100644 --- a/conan/cli/commands/audit.py +++ b/conan/cli/commands/audit.py @@ -14,7 +14,6 @@ from conan.cli.printers import print_profiles from conan.cli.printers.graph import print_graph_basic from conan.errors import ConanException -from conan.internal.util.files import load def _add_provider_arg(subparser): @@ -134,7 +133,8 @@ def audit_list(conan_api: ConanAPI, parser, subparser, *args): ConanOutput().warning("Nothing to list, package list does not contain recipe revisions") elif args.sbom: sbom_file = make_abs_path(args.sbom) - sbom = json.loads(load(sbom_file)) + with open(sbom_file, 'r') as f: + sbom = json.load(f) if sbom.get("bomFormat") != "CycloneDX": raise ConanException(f"Unsupported SBOM format, only CycloneDX is supported.") purls = [component["purl"] for component in sbom["components"]] diff --git a/conan/cli/formatters/audit/vulnerabilities.py b/conan/cli/formatters/audit/vulnerabilities.py index 468f5d8f21d..1dbd6b5b818 100644 --- a/conan/cli/formatters/audit/vulnerabilities.py +++ b/conan/cli/formatters/audit/vulnerabilities.py @@ -72,8 +72,9 @@ def wrap_and_indent(txt, limit=80, indent=2): name = node["name"] sev = node.get("severity", "Medium") sev_color = severity_colors.get(sev, Color.BRIGHT_YELLOW) - score = node.get("cvss", {}).get("preferredBaseScore") - score_txt = f", CVSS: {score}" if score else "" + cvss = node.get("cvss", {}) + preferred_score = cvss.get("preferredBaseScore") + score_txt = f" - {preferred_score}" if preferred_score else "" desc = node.get("description", "") desc = (desc[:240] + "...") if len(desc) > 240 else desc desc_wrapped = wrap_and_indent(desc) @@ -127,6 +128,17 @@ def wrap_and_indent(txt, limit=80, indent=2): if fixVersions: cli_out_write(f" fixed in version(s): ", endline="", fg=Color.BRIGHT_BLUE) cli_out_write(', '.join(fixVersions)) + + if "v3" in cvss and cvss["v3"].get("baseScore", 0) > 0: + score_v3 = cvss["v3"].get("baseScore", 0) + if score_v3: + cli_out_write(f" CVSS v3: ", endline="", fg=Color.BRIGHT_BLUE) + cli_out_write(score_v3) + if "v4" in cvss and cvss["v4"].get("baseScore", 0) > 0: + score_v4 = cvss["v4"].get("baseScore", 0) + if score_v4: + cli_out_write(f" CVSS v4: ", endline="", fg=Color.BRIGHT_BLUE) + cli_out_write(score_v4) cli_out_write("") color_for_total = Color.BRIGHT_RED if total_vulns else Color.BRIGHT_GREEN @@ -237,18 +249,20 @@ def _render_vulns(vulns, template): {{ vuln.package }}
    PackageIDSeverityScoreInfo Description
    {{ vuln.package }}{{ vuln.vuln_id }} - {% if severity_id == 'N/A' %}-1{% else %}{{ severity_id }}{% endif %} + {{ vuln.package }} + + {{ vuln.score }} + {% if vuln.withdrawn %} + [WITHDRAWN]
    + {% endif %} + {{ vuln.vuln_id }} +
    {% if vuln.severity not in ['N/A', ''] %} {{ severity_label }} {% else %} {{ vuln.severity }} {% endif %} + {{ vuln.score }}
    {{ vuln.score }} + {% for research in vuln.advisories %} + {% if research.shortDescription %} +
    + Summary provided by JFrog Research ({{ research.name }}) +
    + Short description: {{ research.shortDescription }}
    + {% if research.severity %} + Impact severity: {{ research.severity }}
    + {% if research.impactReasons %} + Impact reasons: +
      + {% for reason in research.impactReasons %} +
    • {{ reason.name }}
    • + {% endfor %} +
    + {% endif %} + {% endif %} + {% if vuln.provider_url %} + {% set expected_url = vuln.provider_url.rstrip('/') + '/ui/catalog/vulnerabilities/details/' + research.name %} + More info available in: {{ expected_url }}
    + {% endif %} +
    +
    + {% endif %} + {% endfor %} + Description: +
    {{ vuln.description }} + {% if vuln.publishedAt %} +
    +
    + Published at: {{ vuln.publishedAt }} + {% endif %} + {% if vuln.fixVersions %} +
    +
    + Fixed in version(s): +
    + {% for version in vuln.fixVersions %} + {{ version }} + {% endfor %} +
    + {% endif %} {% if vuln.references %} -

    References: +
    References:
      {% for ref in vuln.references %}
    • {{ ref }}
    • @@ -212,7 +303,7 @@ def _render_vulns(vulns, template):
    {% endif %} {% if vuln.aliases %} -

    Aliases: {{ ', '.join(vuln.aliases) }} +
    Aliases: {{ ', '.join(vuln.aliases) }} {% endif %}
    - {{ vuln.score }} - {% if vuln.withdrawn %} - [WITHDRAWN]
    - {% endif %} - {{ vuln.vuln_id }} -
    + {{ vuln.preferred_score }} {% if vuln.severity not in ['N/A', ''] %} - {{ severity_label }} + {{ severity_label }} {% if vuln.preferred_score %}({{ vuln.preferred_score }}){% endif %} {% else %} {{ vuln.severity }} {% endif %} - {{ vuln.score }} +
    +
    + {% if vuln.withdrawn %} + [WITHDRAWN]
    + {% endif %} + {{ vuln.vuln_id }} + {% if vuln.score_v3 %}
    CVSS v3: {{ vuln.score_v3 }}{% endif %} + {% if vuln.score_v4 %}
    CVSS v4: {{ vuln.score_v4 }}{% endif %}
    {% for research in vuln.advisories %} @@ -349,8 +363,14 @@ def html_vuln_formatter(result): name = node.get("name") sev = node.get("severity", "Medium") sev = f"{severity_order.get(sev, 2)} - {sev}" - score = node.get("cvss", {}).get("preferredBaseScore") - score_txt = f"CVSS: {score}" if score else "-" + cvss = node.get("cvss", {}) + preferred_score = cvss.get("preferredBaseScore") + score_v3 = 0 + score_v4 = 0 + if "v3" in cvss and cvss["v3"].get("baseScore", 0) > 0: + score_v3 = cvss["v3"].get("baseScore", 0) + if "v4" in cvss and cvss["v4"].get("baseScore", 0) > 0: + score_v4 = cvss["v4"].get("baseScore", 0) aliases = node.get("aliases", []) references = node.get("references", []) desc = node.get("description", "") @@ -366,7 +386,9 @@ def html_vuln_formatter(result): "vuln_id": name, "aliases": aliases, "severity": sev, - "score": score_txt, + "preferred_score": preferred_score, + "score_v3": score_v3, + "score_v4": score_v4, "description": desc, "references": references, "withdrawn": withdrawn, diff --git a/conan/internal/api/audit/providers.py b/conan/internal/api/audit/providers.py index ebf5017718e..1bc1bafe464 100644 --- a/conan/internal/api/audit/providers.py +++ b/conan/internal/api/audit/providers.py @@ -148,6 +148,12 @@ def _build_query(ref): severity cvss {{ preferredBaseScore + v3 {{ + baseScore + }} + v4 {{ + baseScore + }} }} aliases withdrawn diff --git a/test/integration/command/test_audit.py b/test/integration/command/test_audit.py index eb2e0980594..554cd5cc53a 100644 --- a/test/integration/command/test_audit.py +++ b/test/integration/command/test_audit.py @@ -479,7 +479,7 @@ def test_audit_scan_threshold_error(severity_level, threshold, should_fail): severity_param = "" if threshold is None else f"-sl {threshold}" tc.run(f"audit scan --requires=foobar/0.1.0 {severity_param}", assert_error=should_fail) assert "foobar/0.1.0 1 vulnerability found" in tc.out - assert f"CVSS: {severity_level}" in tc.out + assert f"Critical - {severity_level}" in tc.out if should_fail: if threshold is None: threshold = "9.0" @@ -530,6 +530,90 @@ def test_audit_scan_context_filter(package_context, filter_context): assert "Requesting vulnerability info for: zlib/1.2.11" not in tc.out +@pytest.mark.parametrize("score", [ + { + "preferredBaseScore": 8.9, + }, + { + "preferredBaseScore": 8.9, + "v3": { + "baseScore": 8.9 + } + }, + { + "preferredBaseScore": 8.9, + "v3": { + "baseScore": 8.9 + }, + "v4": { + "baseScore": 5.6 + } + }, +]) +@pytest.mark.parametrize("out", ["html", "text"]) +def test_audit_cvss_versions(score, out): + """In case the severity level is equal or higher than the found for a CVE, + the command should output the information as usual, and exit with non-success code error. + """ + successful_response = { + "data": { + "query": { + "vulnerabilities": { + "totalCount": 1, + "edges": [ + { + "node": { + "name": "CVE-2023-45853", + "description": "Zip vulnerability", + "severity": "Critical", + "cvss": score, + "aliases": [ + "CVE-2023-45853", + "JFSA-2023-000272529" + ], + "advisories": [ + { + "name": "CVE-2023-45853" + }, + { + "name": "JFSA-2023-000272529" + } + ], + "references": [ + "https://pypi.org/project/pyminizip/#history", + ] + } + } + ] + } + } + } + } + + tc = TestClient(light=True) + + tc.save({"conanfile.py": GenConanfile("foobar", "0.1.0")}) + tc.run("export .") + tc.run("audit provider auth conancenter --token=valid_token") + + with proxy_response(200, successful_response): + tc.run(f"audit scan --requires=foobar/0.1.0 -f={out}") + if out == "text": + assert "foobar/0.1.0 1 vulnerability found" in tc.out + assert f"Critical - {score['preferredBaseScore']}" in tc.out + if "v4" in score: + assert f'CVSS v4: {score["v4"]["baseScore"]}' in tc.out + if "v3" in score: + assert f'CVSS v3: {score["v3"]["baseScore"]}' in tc.out + else: + assert "CVE-2023-45853" in tc.out + assert f"Critical ({score['preferredBaseScore']})" in tc.out + if "v4" in score: + assert f'CVSS v4: {score["v4"]["baseScore"]}' in tc.out + if "v3" in score: + assert f'CVSS v3: {score["v3"]["baseScore"]}' in tc.out + + class TestAuditApiBranchouts: def test_audit_load_provider_default(self): tc = TestClient(light=True) From 3500931911bf61674c0c1912813e0bcd098a6014 Mon Sep 17 00:00:00 2001 From: Carlos Zoido Date: Wed, 25 Mar 2026 13:04:25 +0100 Subject: [PATCH 066/110] Conan 2.27.0 --- conan/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conan/__init__.py b/conan/__init__.py index 415d5f586e3..f0b3b7346ed 100644 --- a/conan/__init__.py +++ b/conan/__init__.py @@ -2,5 +2,5 @@ from conan.internal.model.workspace import Workspace from conan.internal.model.version import Version -__version__ = '2.27.0-dev' +__version__ = '2.27.0' conan_version = Version(__version__) From a4a7f77f3ea79f39e1a085b925c26ff72b5a7ae2 Mon Sep 17 00:00:00 2001 From: Carlos Zoido Date: Wed, 25 Mar 2026 15:52:35 +0100 Subject: [PATCH 067/110] Conan 2.28.0 dev --- conan/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conan/__init__.py b/conan/__init__.py index f0b3b7346ed..9933544736f 100644 --- a/conan/__init__.py +++ b/conan/__init__.py @@ -2,5 +2,5 @@ from conan.internal.model.workspace import Workspace from conan.internal.model.version import Version -__version__ = '2.27.0' +__version__ = '2.28.0-dev' conan_version = Version(__version__) From 80210c043416b411af5da087f27ac3f2520e5caf Mon Sep 17 00:00:00 2001 From: Linus van Elswijk Date: Thu, 26 Mar 2026 09:55:27 +0100 Subject: [PATCH 068/110] Feature/cachyos package manager (#19788) Add support for CachyOS in package_manager - Register pacman as the default package_manager for CachyOS - Cover this in `package_manager_test.py` --- conan/tools/system/package_manager.py | 2 +- test/integration/tools/system/package_manager_test.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/conan/tools/system/package_manager.py b/conan/tools/system/package_manager.py index eeec1856a08..f1fcfae57f5 100644 --- a/conan/tools/system/package_manager.py +++ b/conan/tools/system/package_manager.py @@ -49,7 +49,7 @@ def get_default_tool(self): "dnf": ["fedora", "rhel", "centos", "mageia", "nobara", "almalinux", "rocky", "oracle"], "brew": ["Darwin"], - "pacman": ["arch", "manjaro", "msys2", "endeavouros"], + "pacman": ["arch", "manjaro", "msys2", "endeavouros", "cachyos"], "choco": ["Windows"], "zypper": ["opensuse", "sles"], "pkg": ["freebsd"], diff --git a/test/integration/tools/system/package_manager_test.py b/test/integration/tools/system/package_manager_test.py index 643f05727ea..6632c1ee683 100644 --- a/test/integration/tools/system/package_manager_test.py +++ b/test/integration/tools/system/package_manager_test.py @@ -58,6 +58,7 @@ def test_msys2(): ("fedora", "dnf"), ("nobara", "dnf"), ("arch", "pacman"), + ("cachyos", "pacman"), ("opensuse", "zypper"), ("sles", "zypper"), ("opensuse", "zypper"), From 13a89a245f2a869898f802ffa86407d4e8eb224e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= <5364255+AbrilRBS@users.noreply.github.com> Date: Thu, 26 Mar 2026 11:32:32 +0100 Subject: [PATCH 069/110] Add missing `CMAKE_CXX_COMPILER_WORKS` and family (#19708) * Add some shortcuts for cmake * Only where necessary * Improvements --- test/functional/command/test_new.py | 2 ++ .../cmake/test_cmake_extra_variables.py | 2 ++ .../toolchains/cmake/test_cmake_find_none.py | 4 ++++ .../toolchains/cmake/test_cmake_multi.py | 2 ++ test/functional/toolchains/cmake/test_cps.py | 18 ++++++++++++++++++ test/functional/toolchains/ios/_utils.py | 2 ++ .../toolchains/meson/test_subproject.py | 2 ++ .../functional/util/test_cmd_args_to_string.py | 2 ++ 8 files changed, 34 insertions(+) diff --git a/test/functional/command/test_new.py b/test/functional/command/test_new.py index d9cbf58781b..dee307e3622 100644 --- a/test/functional/command/test_new.py +++ b/test/functional/command/test_new.py @@ -23,6 +23,8 @@ def test_conan_new_empty(): c = TestClient() c.run("new") cmakelists = textwrap.dedent(""" + set(CMAKE_CXX_COMPILER_WORKS 1) + set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(PackageTest CXX) add_executable(example main.cpp) diff --git a/test/functional/toolchains/cmake/test_cmake_extra_variables.py b/test/functional/toolchains/cmake/test_cmake_extra_variables.py index 2a9de2caf03..f2d1de7b560 100644 --- a/test/functional/toolchains/cmake/test_cmake_extra_variables.py +++ b/test/functional/toolchains/cmake/test_cmake_extra_variables.py @@ -27,6 +27,8 @@ def package_info(self): client.run("create dep") cmakelists = textwrap.dedent(""" + set(CMAKE_CXX_COMPILER_WORKS 1) + set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.27) project(myproject CXX) find_package(dep CONFIG REQUIRED) diff --git a/test/functional/toolchains/cmake/test_cmake_find_none.py b/test/functional/toolchains/cmake/test_cmake_find_none.py index 08245cf5efc..f801e358169 100644 --- a/test/functional/toolchains/cmake/test_cmake_find_none.py +++ b/test/functional/toolchains/cmake/test_cmake_find_none.py @@ -115,6 +115,8 @@ def test_cmake_find_none_relocation(): conanfile = conanfile + '\n self.cpp_info.builddirs = ["pkg/cmake"]' cmake_export = textwrap.dedent(""" + set(CMAKE_CXX_COMPILER_WORKS 1) + set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(MyHello CXX) @@ -220,6 +222,8 @@ def package_info(self): """) cmake_export = textwrap.dedent(""" + set(CMAKE_CXX_COMPILER_WORKS 1) + set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(MyHello CXX) diff --git a/test/functional/toolchains/cmake/test_cmake_multi.py b/test/functional/toolchains/cmake/test_cmake_multi.py index bff9cd45f66..16cf34bd560 100644 --- a/test/functional/toolchains/cmake/test_cmake_multi.py +++ b/test/functional/toolchains/cmake/test_cmake_multi.py @@ -63,6 +63,8 @@ def package_info(self): """) cmakelist = textwrap.dedent(""" + set(CMAKE_CXX_COMPILER_WORKS 1) + set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(hello_{name} LANGUAGES CXX) diff --git a/test/functional/toolchains/cmake/test_cps.py b/test/functional/toolchains/cmake/test_cps.py index 681acaecf24..84e9ad028b0 100644 --- a/test/functional/toolchains/cmake/test_cps.py +++ b/test/functional/toolchains/cmake/test_cps.py @@ -52,6 +52,8 @@ def package_info(self): """) cmake = textwrap.dedent("""\ + set(CMAKE_CXX_COMPILER_WORKS 1) + set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 4.2) project(mypkg CXX) @@ -80,6 +82,8 @@ def package_info(self): # Lets consume directly with CPS test_cmake = textwrap.dedent("""\ + set(CMAKE_CXX_COMPILER_WORKS 1) + set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 4.2) project(PackageTest CXX) @@ -182,6 +186,8 @@ def package_info(self): """) cmake = textwrap.dedent("""\ + set(CMAKE_CXX_COMPILER_WORKS 1) + set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 4.2) project(mypkg CXX) @@ -214,6 +220,8 @@ def package_info(self): # Create test_package files for the two components test_package_cmake = textwrap.dedent("""\ + set(CMAKE_CXX_COMPILER_WORKS 1) + set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(PackageTest CXX) @@ -342,6 +350,8 @@ def package_info(self): lib_type = "PUBLIC" if "public" in kind else "PRIVATE" cmake = textwrap.dedent("""\ + set(CMAKE_CXX_COMPILER_WORKS 1) + set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 4.2) project({name} CXX) @@ -386,6 +396,8 @@ def package_info(self): # Create test_package files for the two components test_package_cmake = textwrap.dedent("""\ + set(CMAKE_CXX_COMPILER_WORKS 1) + set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(PackageTest CXX) @@ -512,6 +524,8 @@ def test_pure_cmake_shared(): c = TestClient() cmake = textwrap.dedent("""\ + set(CMAKE_CXX_COMPILER_WORKS 1) + set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 4.2) project(myproj CXX) @@ -609,6 +623,8 @@ def package_info(self): """) cmake = textwrap.dedent("""\ + set(CMAKE_CXX_COMPILER_WORKS 1) + set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 4.2) project(mypkg CXX) @@ -642,6 +658,8 @@ def package_info(self): # Lets consume directly with CPS test_cmake = textwrap.dedent("""\ + set(CMAKE_CXX_COMPILER_WORKS 1) + set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 4.2) project(PackageTest CXX) diff --git a/test/functional/toolchains/ios/_utils.py b/test/functional/toolchains/ios/_utils.py index 10caeec6333..ccc5d0f7140 100644 --- a/test/functional/toolchains/ios/_utils.py +++ b/test/functional/toolchains/ios/_utils.py @@ -23,6 +23,8 @@ class HelloLib { """) cmakelists = textwrap.dedent(""" + set(CMAKE_CXX_COMPILER_WORKS 1) + set(CMAKE_CXX_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.1) project(MyHello CXX) set(SOURCES diff --git a/test/functional/toolchains/meson/test_subproject.py b/test/functional/toolchains/meson/test_subproject.py index 7415bb0388b..668017a39b2 100644 --- a/test/functional/toolchains/meson/test_subproject.py +++ b/test/functional/toolchains/meson/test_subproject.py @@ -150,6 +150,8 @@ def test(self): """) _test_package_cmake_lists = textwrap.dedent(""" + set(CMAKE_C_COMPILER_WORKS 1) + set(CMAKE_C_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.1) project(test_package C) diff --git a/test/functional/util/test_cmd_args_to_string.py b/test/functional/util/test_cmd_args_to_string.py index 8fe570315ab..08158f59332 100644 --- a/test/functional/util/test_cmd_args_to_string.py +++ b/test/functional/util/test_cmd_args_to_string.py @@ -30,6 +30,8 @@ def application_folder(): ) cmake = textwrap.dedent(""" + set(CMAKE_C_COMPILER_WORKS 1) + set(CMAKE_C_ABI_COMPILED 1) cmake_minimum_required(VERSION 3.15) project(arg_printer C) From eef9e937bb162e1501f98456dd9fa407cadc52f6 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 26 Mar 2026 14:12:21 +0100 Subject: [PATCH 070/110] short form for --update-requires (#19791) --- conan/cli/commands/lock.py | 6 +++--- test/integration/lockfile/test_user_overrides.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/conan/cli/commands/lock.py b/conan/cli/commands/lock.py index d3cd0ff636a..db27e806810 100644 --- a/conan/cli/commands/lock.py +++ b/conan/cli/commands/lock.py @@ -180,11 +180,11 @@ def lock_upgrade(conan_api, parser, subparser, *args): given a conanfile or a reference. """ common_graph_args(subparser) - subparser.add_argument('--update-requires', action="append", + subparser.add_argument('-ur', '--update-requires', action="append", help='Update requires from lockfile') - subparser.add_argument('--update-build-requires', action="append", + subparser.add_argument('-ubr', '--update-build-requires', action="append", help='Update build-requires from lockfile') - subparser.add_argument('--update-python-requires', action="append", + subparser.add_argument('-upr', '--update-python-requires', action="append", help='Update python-requires from lockfile') subparser.add_argument('--build-require', action='store_true', default=False, help='Whether the provided reference is a build-require') diff --git a/test/integration/lockfile/test_user_overrides.py b/test/integration/lockfile/test_user_overrides.py index 5107fe9d7c0..0cb547f111d 100644 --- a/test/integration/lockfile/test_user_overrides.py +++ b/test/integration/lockfile/test_user_overrides.py @@ -419,7 +419,7 @@ def test_lock_upgrade_path(self): 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") + c.run("lock upgrade . -ur=liba/1.0 -ur=libb/[*] -ubr=libc/[*] -upr=libd/1.0") lock = c.load("conan.lock") assert "liba/1.9" in lock assert "libb/1.1" in lock From 89711b440192d7000ac93a8e56942bb442363d7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Dahlgren?= Date: Fri, 27 Mar 2026 10:48:25 +0100 Subject: [PATCH 071/110] Add support for Apple Clang version 21 (#19795) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add support for Apple Clang version 21 Xcode 26.4 shipped with new Clang 21 * Add missing sdk verisons and apple os versions * Add missing sdk verisons and apple os versions --------- Co-authored-by: Abril Rincón Blanco <5364255+AbrilRBS@users.noreply.github.com> --- conan/internal/default_settings.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/conan/internal/default_settings.py b/conan/internal/default_settings.py index a8aebe4b99c..13b57f83c3d 100644 --- a/conan/internal/default_settings.py +++ b/conan/internal/default_settings.py @@ -24,14 +24,14 @@ "16.0", "16.1", "16.2", "16.3", "16.4", "16.5", "16.6", "16.7", "17.0", "17.1", "17.2", "17.3", "17.4", "17.5", "17.6", "17.7", "17.8", "18.0", "18.1", "18.2", "18.3", "18.4", "18.5", "18.6", "18.7", - "26.0", "26.1", "26.2", "26.3"] + "26.0", "26.1", "26.2", "26.3", "26.4"] sdk: ["iphoneos", "iphonesimulator"] sdk_version: [null, "11.3", "11.4", "12.0", "12.1", "12.2", "12.4", "13.0", "13.1", "13.2", "13.3", "13.4", "13.5", "13.6", "13.7", "14.0", "14.1", "14.2", "14.3", "14.4", "14.5", "15.0", "15.2", "15.4", "15.5", "16.0", "16.1", "16.2", "16.4", "17.0", "17.1", "17.2", "17.4", "17.5", "18.0", "18.1", "18.2", "18.4", "18.5", - "26.0", "26.1", "26.2"] + "26.0", "26.1", "26.2", "26.4"] watchOS: version: ["4.0", "4.1", "4.2", "4.3", "5.0", "5.1", "5.2", "5.3", "6.0", "6.1", "6.2", "6.3", "7.0", "7.1", "7.2", "7.3", "7.4", "7.5", "7.6", @@ -39,13 +39,13 @@ "9.0","9.1", "9.2", "9.3", "9.4", "9.5", "9.6", "10.0", "10.1", "10.2", "10.3", "10.4", "10.5", "10.6", "11.0", "11.1", "11.2", "11.3", "11.4", "11.5", "11.6", - "26.0", "26.1", "26.2", "26.3"] + "26.0", "26.1", "26.2", "26.3", "26.4"] sdk: ["watchos", "watchsimulator"] sdk_version: [null, "4.3", "5.0", "5.1", "5.2", "5.3", "6.0", "6.1", "6.2", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.0.1", "8.3", "8.5", "9.0", "9.1", "9.4", "10.0", "10.1", "10.2", "10.4", "10.5", "11.0", "11.1", "11.2", "11.4", "11.5", - "26.0", "26.1", "26.2"] + "26.0", "26.1", "26.2", "26.4"] tvOS: version: ["11.0", "11.1", "11.2", "11.3", "11.4", "12.0", "12.1", "12.2", "12.3", "12.4", @@ -55,19 +55,19 @@ "16.0", "16.1", "16.2", "16.3", "16.4", "16.5", "16.6", "17.0", "17.1", "17.2", "17.3", "17.4", "17.5", "17.6", "18.0", "18.1", "18.2", "18.3", "18.4", "18.5", "18.6", - "26.0", "26.1", "26.2", "26.3"] + "26.0", "26.1", "26.2", "26.3", "26.4"] sdk: ["appletvos", "appletvsimulator"] sdk_version: [null, "11.3", "11.4", "12.0", "12.1", "12.2", "12.4", "13.0", "13.2", "13.3", "13.4", "14.0", "14.2", "14.3", "14.4", "14.5", "15.0", "15.2", "15.4", "15.5", "16.0", "16.1", "16.4", "17.0", "17.1", "17.2", "17.4", "17.5", "18.0", "18.1", "18.2", "18.4", "18.5", - "26.0", "26.1", "26.2"] + "26.0", "26.1", "26.2", "26.4"] visionOS: version: ["1.0", "1.1", "1.2", "1.3", "2.0", "2.1", "2.2", "2.3", "2.4", "2.5", "2.6", - "26.0", "26.1", "26.2", "26.3"] + "26.0", "26.1", "26.2", "26.3", "26.4"] sdk: ["xros", "xrsimulator"] sdk_version: [null, "1.0", "1.1", "1.2", "1.3", "2.0", "2.1", "2.2", "2.4", "2.5", - "26.0", "26.1", "26.2"] + "26.0", "26.1", "26.2", "26.4"] Macos: version: [null, "10.6", "10.7", "10.8", "10.9", "10.10", "10.11", "10.12", "10.13", "10.14", "10.15", "11.0", "11.1", "11.2", "11.3", "11.4", "11.5", "11.6", "11.7", @@ -75,7 +75,7 @@ "13.0", "13.1", "13.2", "13.3", "13.4", "13.5", "13.6", "13.7", "14.0", "14.1", "14.2", "14.3", "14.4", "14.5", "14.6", "14.7", "15.0", "15.1", "15.2", "15.3", "15.4", "15.5", "15.6", "15.7", - "26.0", "26.1", "26.2", "26.3"] + "26.0", "26.1", "26.2", "26.3", "26.4"] sdk_version: [null, "10.13", "10.14", "10.15", "11.0", "11.1", "11.2", "11.3", "12.0", "12.1", "12.3", "12.4", "13.0", "13.1", "13.3", "14.0", "14.2", "14.4", "14.5", "15.0", "15.1", "15.2", "15.4", "15.5", @@ -151,7 +151,7 @@ apple-clang: version: ["5.0", "5.1", "6.0", "6.1", "7.0", "7.3", "8.0", "8.1", "9.0", "9.1", "10.0", "11.0", "12.0", "13", "13.0", "13.1", "14", "14.0", "15", "15.0", - "16", "16.0", "17", "17.0"] + "16", "16.0", "17", "17.0", "21", "21.0"] libcxx: [libstdc++, libc++] cppstd: [null, 98, gnu98, 11, gnu11, 14, gnu14, 17, gnu17, 20, gnu20, 23, gnu23, 26, gnu26] cstd: [null, 99, gnu99, 11, gnu11, 17, gnu17, 23, gnu23] From 29a20a715ba02af56e570c66fc8f6c4caec00d76 Mon Sep 17 00:00:00 2001 From: James Date: Fri, 27 Mar 2026 17:34:31 +0100 Subject: [PATCH 072/110] Test improvements (#19800) * wip * wip * wip * wip --- conan/internal/model/profile.py | 2 +- test/functional/revisions_test.py | 13 +- test/functional/sbom/__init__.py | 0 test/functional/sbom/test_cyclonedx.py | 84 --------- test/integration/cache/cache2_update_test.py | 57 +++--- .../command/download/download_test.py | 13 +- .../command/install/install_cascade_test.py | 8 +- .../command/install/install_test.py | 103 +++++------ test/integration/command/list/list_test.py | 6 +- test/integration/command/list/search_test.py | 51 ++---- .../list/test_combined_pkglist_flows.py | 10 +- .../integration/command/remote/remote_test.py | 9 +- .../command/remote/test_remote_users.py | 169 +++++++++--------- test/integration/command/source_test.py | 13 +- test/integration/command/test_outdated.py | 11 +- .../integration/command/upload/upload_test.py | 6 +- .../integration/configuration/profile_test.py | 41 +++-- .../python_requires_test.py | 0 .../graph/core/test_version_ranges.py | 13 +- .../version_ranges_cached_test.py | 42 ++--- .../version_ranges_diamond_test.py | 8 +- .../metadata/test_metadata_deploy.py | 3 +- test/integration/py_requires/__init__.py | 0 .../remote/multi_remote_checks_test.py | 58 +++--- test/integration/remote/multi_remote_test.py | 95 ++++------ .../remote/selected_remotes_test.py | 6 +- test/integration/sbom/test_cyclonedx.py | 78 ++++++++ test/unittests/model/profile_test.py | 84 +++++---- 28 files changed, 444 insertions(+), 539 deletions(-) delete mode 100644 test/functional/sbom/__init__.py delete mode 100644 test/functional/sbom/test_cyclonedx.py rename test/integration/{py_requires => extensions}/python_requires_test.py (100%) delete mode 100644 test/integration/py_requires/__init__.py diff --git a/conan/internal/model/profile.py b/conan/internal/model/profile.py index c9f95b4da97..e452536eb9f 100644 --- a/conan/internal/model/profile.py +++ b/conan/internal/model/profile.py @@ -173,7 +173,7 @@ def update_settings(self, new_settings): """Mix the specified settings with the current profile. Specified settings are prioritized to profile""" - assert isinstance(new_settings, OrderedDict) + assert isinstance(new_settings, (OrderedDict, dict)) # apply the current profile res = copy.copy(self.settings) diff --git a/test/functional/revisions_test.py b/test/functional/revisions_test.py index 6c8bc4d9adb..d069e380de7 100644 --- a/test/functional/revisions_test.py +++ b/test/functional/revisions_test.py @@ -1,6 +1,5 @@ import copy import time -from collections import OrderedDict import pytest from unittest.mock import patch @@ -35,8 +34,7 @@ class TestInstallingPackagesWithRevisions: def setup(self): self.server = TestServer() self.server2 = TestServer() - self.servers = OrderedDict([("default", self.server), - ("remote2", self.server2)]) + self.servers = {"default": self.server, "remote2": self.server2} self.c_v2 = TestClient(servers=self.servers, inputs=2*["admin", "password"]) self.ref = RecipeReference.loads("lib/1.0@conan/testing") @@ -274,7 +272,8 @@ class TestRemoveWithRevisions: @pytest.fixture(autouse=True) def setup(self): self.server = TestServer() - self.c_v2 = TestClient(servers={"default": self.server}, inputs=["admin", "password"]) + self.c_v2 = TestClient(light=True, + servers={"default": self.server}, inputs=["admin", "password"]) self.ref = RecipeReference.loads("lib/1.0@conan/testing") def test_remove_local_recipe(self): @@ -530,7 +529,8 @@ class TestServerRevisionsIndexes: @pytest.fixture(autouse=True) def setup(self): self.server = TestServer() - self.c_v2 = TestClient(servers={"default": self.server}, inputs=["admin", "password"]) + self.c_v2 = TestClient(light=True, + servers={"default": self.server}, inputs=["admin", "password"]) self.ref = RecipeReference.loads("lib/1.0@conan/testing") def test_rotation_deleting_recipe_revisions(self): @@ -660,8 +660,7 @@ def test_deleting_all_prevs(self): def test_touching_other_server(): # https://github.com/conan-io/conan/issues/9333 - servers = OrderedDict([("remote1", TestServer()), - ("remote2", None)]) # None server will crash if touched + servers = {"remote1": TestServer(), "remote2": None} # None server will crash if touched c = TestClient(servers=servers, inputs=["admin", "password"]) c.save({"conanfile.py": GenConanfile().with_settings("os")}) c.run("create . --name=pkg --version=0.1 --user=conan --channel=channel -s os=Windows") diff --git a/test/functional/sbom/__init__.py b/test/functional/sbom/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/test/functional/sbom/test_cyclonedx.py b/test/functional/sbom/test_cyclonedx.py deleted file mode 100644 index 2b357ce8c6f..00000000000 --- a/test/functional/sbom/test_cyclonedx.py +++ /dev/null @@ -1,84 +0,0 @@ -import json -import os - -import pytest - -from conan.internal.util.files import save -from conan.test.assets.genconanfile import GenConanfile -from conan.test.utils.tools import TestClient - -# Using the sbom tool with "conan create" -sbom_hook_post_package = """ -import json -import os -from conan.errors import ConanException -from conan.api.output import ConanOutput -from conan.tools.sbom import cyclonedx_1_4, cyclonedx_1_6 - -def post_package(conanfile): - sbom_cyclonedx_1_4 = cyclonedx_1_4(conanfile, add_build=True, add_tests=True) - sbom_cyclonedx_1_6 = cyclonedx_1_6(conanfile, add_build=True, add_tests=True) - with open(os.path.join(conanfile.package_metadata_folder, "sbom14.cdx.json"), 'w') as f: - json.dump(sbom_cyclonedx_1_4, f, indent=4) - with open(os.path.join(conanfile.package_metadata_folder, "sbom16.cdx.json"), 'w') as f: - json.dump(sbom_cyclonedx_1_6, f, indent=4) -""" - - -class TestCyclonedx: - - @pytest.fixture() - def hook_setup_post_package_tl(self, transitive_libraries): - tc = transitive_libraries - hook_path = os.path.join(tc.paths.hooks_path, "hook_sbom.py") - save(hook_path, sbom_hook_post_package) - return tc - - @pytest.mark.tool("cmake") - def test_sbom_generation_create(self, hook_setup_post_package_tl): - # TODO This doesn't need to be a functional test, check why - tc = hook_setup_post_package_tl - tc.run("new cmake_lib -d name=bar -d version=1.0 -d requires=engine/1.0 -f") - # bar -> engine/1.0 -> matrix/1.0 - tc.run("create . -tf=") - bar_layout = tc.created_layout() - assert os.path.exists(os.path.join(bar_layout.metadata(), "sbom14.cdx.json")) - assert os.path.exists(os.path.join(bar_layout.metadata(), "sbom16.cdx.json")) - - @pytest.mark.tool("cmake") - @pytest.mark.parametrize("user, channel, user_dep, channel_dep", - [("user", None, "user_dep", None), - ("user", "channel", "user_dep", "channel_dep")]) - def test_sbom_user_path(self, user, channel, user_dep, channel_dep): - tc = TestClient(light=True) - hook_path = os.path.join(tc.paths.hooks_path, "hook_sbom.py") - save(hook_path, sbom_hook_post_package) - channel_ref = f"/{channel_dep}" if channel_dep else "" - tc.save({"dep/conanfile.py": GenConanfile("dep", "1.0"), - "conanfile.py": GenConanfile("main", "1.0").with_requires( - f"dep/1.0@{user_dep}{channel_ref}")}) - command = "create dep" - if user: - command += f" --user={user_dep}" - if channel: - command += f" --channel={channel_dep}" - - tc.run(command) - - command = "create ." - if user: - command += f" --user={user}" - if channel: - command += f" --channel={channel}" - tc.run(command) - - for version in ("14", "16"): - create_layout = tc.created_layout() - cyclone_path = os.path.join(create_layout.metadata(), f"sbom{version}.cdx.json") - content = tc.load(cyclone_path) - content_json = json.loads(content) - - assert content_json["components"][0]["bom-ref"].split("&user=")[ - 1] == f"{user}&channel={channel}" if channel else user - assert content_json["dependencies"][0]["dependsOn"][0].split("&user=")[ - 1] == f"{user_dep}&channel={channel_dep}" if channel_dep else user_dep diff --git a/test/integration/cache/cache2_update_test.py b/test/integration/cache/cache2_update_test.py index 4021f07889e..9acb947dd84 100644 --- a/test/integration/cache/cache2_update_test.py +++ b/test/integration/cache/cache2_update_test.py @@ -1,7 +1,6 @@ import copy import json import textwrap -from collections import OrderedDict import pytest from unittest.mock import patch @@ -17,7 +16,7 @@ class TestUpdateFlows: def _setup(self): self.liba = RecipeReference.loads("liba/1.0.0") - servers = OrderedDict() + servers = {} for index in range(3): servers[f"server{index}"] = TestServer([("*/*@*/*", "*")], [("*/*@*/*", "*")], users={"user": "password"}) @@ -442,33 +441,33 @@ def test_version_ranges(self): "from remote 'server2' " in self.client.out -@pytest.mark.parametrize("update,result", [ - # Not a real pattern, works to support legacy syntax - ["*", {"liba/1.1": "Downloaded (default)", - "libb/1.1": "Downloaded (default)"}], - ["libc", {"liba/1.0": "Cache", - "libb/1.0": "Cache"}], - ["liba", {"liba/1.1": "Downloaded (default)", - "libb/1.0": "Cache"}], - ["libb", {"liba/1.0": "Cache", - "libb/1.1": "Downloaded (default)"}], - ["libb/1.0", {"liba/1.0": "Cache", - "libb/1.0": "Cache"}], - ["libb/1.0#7e88fd43dc3c8171b6f38f8d1b139641", - {"liba/1.0": "Cache", - "libb/1.0": "Cache"}], - ["", {"liba/1.0": "Cache", - "libb/1.0": "Cache"}], - # Patterns not supported, only full name match - ["lib*", {"liba/1.0": "Cache", - "libb/1.0": "Cache"}], - ["liba/*", {"liba/1.0": "Cache", - "libb/1.0": "Cache"}], - # None only passes legacy --update without args, - # to ensure it works, it should be the same as passing * - [None, {"liba/1.1": "Downloaded (default)", - "libb/1.1": "Downloaded (default)"}] - ]) +@pytest.mark.parametrize("update,result", + [ + # Not a real pattern, works to support legacy syntax + ["*", {"liba/1.1": "Downloaded (default)", + "libb/1.1": "Downloaded (default)"}], + ["libc", {"liba/1.0": "Cache", + "libb/1.0": "Cache"}], + ["liba", {"liba/1.1": "Downloaded (default)", + "libb/1.0": "Cache"}], + ["libb", {"liba/1.0": "Cache", + "libb/1.1": "Downloaded (default)"}], + ["libb/1.0", {"liba/1.0": "Cache", + "libb/1.0": "Cache"}], + ["libb/1.0#7e88fd43dc3c8171b6f38f8d1b139641", {"liba/1.0": "Cache", + "libb/1.0": "Cache"}], + ["", {"liba/1.0": "Cache", + "libb/1.0": "Cache"}], + # Patterns not supported, only full name match + ["lib*", {"liba/1.0": "Cache", + "libb/1.0": "Cache"}], + ["liba/*", {"liba/1.0": "Cache", + "libb/1.0": "Cache"}], + # None only passes legacy --update without args, + # to ensure it works, it should be the same as passing * + [None, {"liba/1.1": "Downloaded (default)", + "libb/1.1": "Downloaded (default)"}] + ]) def test_muliref_update_pattern(update, result): tc = TestClient(light=True, default_server_user=True) tc.save({"liba/conanfile.py": GenConanfile("liba"), diff --git a/test/integration/command/download/download_test.py b/test/integration/command/download/download_test.py index fb1daa139f4..7b0bb92512a 100644 --- a/test/integration/command/download/download_test.py +++ b/test/integration/command/download/download_test.py @@ -1,6 +1,6 @@ import os import textwrap -from collections import OrderedDict + from unittest import mock from conan.api.model import RecipeReference @@ -35,12 +35,14 @@ def test_no_user_channel(): client.run("remove * -c") client.run("download pkg/1.0:{} -r default".format(NO_SETTINGS_PACKAGE_ID)) - assert f"Downloading package 'pkg/1.0#4d670581ccb765839f2239cc8dff8fbd:{NO_SETTINGS_PACKAGE_ID}" in client.out + assert (f"Downloading package " + f"'pkg/1.0#4d670581ccb765839f2239cc8dff8fbd:{NO_SETTINGS_PACKAGE_ID}") in client.out # All client.run("remove * -c") client.run("download pkg/1.0#*:* -r default") - assert f"Downloading package 'pkg/1.0#4d670581ccb765839f2239cc8dff8fbd:{NO_SETTINGS_PACKAGE_ID}" in client.out + assert (f"Downloading package " + f"'pkg/1.0#4d670581ccb765839f2239cc8dff8fbd:{NO_SETTINGS_PACKAGE_ID}") in client.out def test_download_with_python_requires(): @@ -52,8 +54,7 @@ def test_download_with_python_requires(): really need to load conanfile, so it doesn't fail because of this. """ # https://github.com/conan-io/conan/issues/9548 - servers = OrderedDict([("tools", TestServer()), - ("pkgs", TestServer())]) + servers = {"tools": TestServer(), "pkgs": TestServer()} c = TestClient(servers=servers, inputs=["admin", "password", "admin", "password"]) c.save({"tool/conanfile.py": GenConanfile("tool", "0.1"), @@ -90,7 +91,7 @@ def source(self): did_verify = {} - def custom_download(this, url, filepath, *args, **kwargs): + def custom_download(this, url, filepath, *args, **kwargs): # noqa did_verify[url] = args[2] with mock.patch("conan.internal.rest.file_downloader.FileDownloader.download", diff --git a/test/integration/command/install/install_cascade_test.py b/test/integration/command/install/install_cascade_test.py index f4f6445a900..f2e0b2a3e76 100644 --- a/test/integration/command/install/install_cascade_test.py +++ b/test/integration/command/install/install_cascade_test.py @@ -1,6 +1,4 @@ -from collections import OrderedDict - -from conan.test.utils.tools import TestServer, GenConanfile, TestClient +from conan.test.utils.tools import GenConanfile, TestClient def test_cascade(): @@ -8,9 +6,7 @@ def test_cascade(): app -> E -> D -> B -> A \\-> F -> C -------/ """ - server = TestServer() - servers = OrderedDict([("default", server)]) - c = TestClient(servers=servers) + c = TestClient(default_server_user=True) c.save({"a/conanfile.py": GenConanfile("liba", "1.0"), "b/conanfile.py": GenConanfile("libb", "1.0").with_requires("liba/1.0"), "c/conanfile.py": GenConanfile("libc", "1.0").with_requires("liba/1.0"), diff --git a/test/integration/command/install/install_test.py b/test/integration/command/install/install_test.py index e0c26dd5d75..da3c42b16f9 100644 --- a/test/integration/command/install/install_test.py +++ b/test/integration/command/install/install_test.py @@ -2,7 +2,6 @@ import os import re import textwrap -from collections import OrderedDict import pytest @@ -28,8 +27,10 @@ def test_install_reference_txt(client): def test_install_reference_error(client): # Test to check the "conan install " command argument - client.run("install --requires=pkg/0.1@myuser/testing --user=user --channel=testing", assert_error=True) - assert "ERROR: Can't use --name, --version, --user or --channel arguments with --requires" in client.out + client.run("install --requires=pkg/0.1@myuser/testing --user=user --channel=testing", + assert_error=True) + assert ("ERROR: Can't use --name, --version, " + "--user or --channel arguments with --requires") in client.out client.save({"conanfile.py": GenConanfile("pkg", "1.0")}) client.run("install . --channel=testing", assert_error=True) assert "Can't specify channel 'testing' without user" in client.out @@ -77,67 +78,67 @@ class Pkg(ConanFile): def package_info(self): self.output.info("PKG OPTION: %s" % self.options.shared) """)}) - client.run("create . --name=pkg --version=0.1 --user=user --channel=testing -o shared=True") - assert "pkg/0.1@user/testing: PKG OPTION: True" in client.out + client.run("create . --name=pkg --version=0.1 -o shared=True") + assert "pkg/0.1: PKG OPTION: True" in client.out client.save({"conanfile.py": textwrap.dedent(""" from conan import ConanFile class Pkg(ConanFile): - requires = "pkg/0.1@user/testing" + requires = "pkg/0.1" options = {"shared": [True, False, "header"]} default_options = {"shared": False} def package_info(self): self.output.info("PKG2 OPTION: %s" % self.options.shared) """)}) - client.run("create . --name=pkg2 --version=0.1 --user=user --channel=testing -o *:shared=True") - assert "pkg/0.1@user/testing: PKG OPTION: True" in client.out - assert "pkg2/0.1@user/testing: PKG2 OPTION: True" in client.out - client.run(" install --requires=pkg2/0.1@user/testing -o *:shared=True") - assert "pkg/0.1@user/testing: PKG OPTION: True" in client.out - assert "pkg2/0.1@user/testing: PKG2 OPTION: True" in client.out + client.run("create . --name=pkg2 --version=0.1 -o *:shared=True") + assert "pkg/0.1: PKG OPTION: True" in client.out + assert "pkg2/0.1: PKG2 OPTION: True" in client.out + client.run(" install --requires=pkg2/0.1 -o *:shared=True") + assert "pkg/0.1: PKG OPTION: True" in client.out + assert "pkg2/0.1: PKG2 OPTION: True" in client.out # Priority of non-scoped options - client.run("create . --name=pkg2 --version=0.1 --user=user --channel=testing -o shared=header -o *:shared=True") - assert "pkg/0.1@user/testing: PKG OPTION: True" in client.out - assert "pkg2/0.1@user/testing: PKG2 OPTION: header" in client.out - client.run(" install --requires=pkg2/0.1@user/testing -o shared=header -o *:shared=True") - assert "pkg/0.1@user/testing: PKG OPTION: True" in client.out - assert "pkg2/0.1@user/testing: PKG2 OPTION: header" in client.out + client.run("create . --name=pkg2 --version=0.1 -o shared=header -o *:shared=True") + assert "pkg/0.1: PKG OPTION: True" in client.out + assert "pkg2/0.1: PKG2 OPTION: header" in client.out + client.run(" install --requires=pkg2/0.1 -o shared=header -o *:shared=True") + assert "pkg/0.1: PKG OPTION: True" in client.out + assert "pkg2/0.1: PKG2 OPTION: header" in client.out # Prevalence of exact named option - client.run("create . --name=pkg2 --version=0.1 --user=user --channel=testing -o *:shared=True -o pkg2*:shared=header") - assert "pkg/0.1@user/testing: PKG OPTION: True" in client.out - assert "pkg2/0.1@user/testing: PKG2 OPTION: header" in client.out - client.run(" install --requires=pkg2/0.1@user/testing -o *:shared=True -o pkg2*:shared=header") - assert "pkg/0.1@user/testing: PKG OPTION: True" in client.out - assert "pkg2/0.1@user/testing: PKG2 OPTION: header" in client.out + client.run("create . --name=pkg2 --version=0.1 -o *:shared=True -o pkg2*:shared=header") + assert "pkg/0.1: PKG OPTION: True" in client.out + assert "pkg2/0.1: PKG2 OPTION: header" in client.out + client.run(" install --requires=pkg2/0.1 -o *:shared=True -o pkg2*:shared=header") + assert "pkg/0.1: PKG OPTION: True" in client.out + assert "pkg2/0.1: PKG2 OPTION: header" in client.out # Prevalence of exact named option reverse - client.run("create . --name=pkg2 --version=0.1 --user=user --channel=testing -o *:shared=True -o pkg/*:shared=header " + client.run("create . --name=pkg2 --version=0.1 -o *:shared=True -o pkg/*:shared=header " "--build=missing") - assert "pkg/0.1@user/testing: PKG OPTION: header" in client.out - assert "pkg2/0.1@user/testing: PKG2 OPTION: True" in client.out - client.run(" install --requires=pkg2/0.1@user/testing -o *:shared=True -o pkg/*:shared=header") - assert "pkg/0.1@user/testing: PKG OPTION: header" in client.out - assert "pkg2/0.1@user/testing: PKG2 OPTION: True" in client.out + assert "pkg/0.1: PKG OPTION: header" in client.out + assert "pkg2/0.1: PKG2 OPTION: True" in client.out + client.run(" install --requires=pkg2/0.1 -o *:shared=True -o pkg/*:shared=header") + assert "pkg/0.1: PKG OPTION: header" in client.out + assert "pkg2/0.1: PKG2 OPTION: True" in client.out # Prevalence of alphabetical pattern - client.run("create . --name=pkg2 --version=0.1 --user=user --channel=testing -o *:shared=True -o pkg2*:shared=header") - assert "pkg/0.1@user/testing: PKG OPTION: True" in client.out - assert "pkg2/0.1@user/testing: PKG2 OPTION: header" in client.out - client.run(" install --requires=pkg2/0.1@user/testing -o *:shared=True -o pkg2*:shared=header") - assert "pkg/0.1@user/testing: PKG OPTION: True" in client.out - assert "pkg2/0.1@user/testing: PKG2 OPTION: header" in client.out + client.run("create . --name=pkg2 --version=0.1 -o *:shared=True -o pkg2*:shared=header") + assert "pkg/0.1: PKG OPTION: True" in client.out + assert "pkg2/0.1: PKG2 OPTION: header" in client.out + client.run(" install --requires=pkg2/0.1 -o *:shared=True -o pkg2*:shared=header") + assert "pkg/0.1: PKG OPTION: True" in client.out + assert "pkg2/0.1: PKG2 OPTION: header" in client.out # Prevalence of last match, even first pattern match - client.run("create . --name=pkg2 --version=0.1 --user=user --channel=testing -o pkg2*:shared=header -o *:shared=True") - assert "pkg/0.1@user/testing: PKG OPTION: True" in client.out - assert "pkg2/0.1@user/testing: PKG2 OPTION: True" in client.out - client.run(" install --requires=pkg2/0.1@user/testing -o pkg2*:shared=header -o *:shared=True") - assert "pkg/0.1@user/testing: PKG OPTION: True" in client.out - assert "pkg2/0.1@user/testing: PKG2 OPTION: True" in client.out + client.run("create . --name=pkg2 --version=0.1 -o pkg2*:shared=header -o *:shared=True") + assert "pkg/0.1: PKG OPTION: True" in client.out + assert "pkg2/0.1: PKG2 OPTION: True" in client.out + client.run(" install --requires=pkg2/0.1 -o pkg2*:shared=header -o *:shared=True") + assert "pkg/0.1: PKG OPTION: True" in client.out + assert "pkg2/0.1: PKG2 OPTION: True" in client.out # Prevalence and override of alphabetical pattern - client.run("create . --name=pkg2 --version=0.1 --user=user --channel=testing -o *:shared=True -o pkg*:shared=header") - assert "pkg/0.1@user/testing: PKG OPTION: header" in client.out - assert "pkg2/0.1@user/testing: PKG2 OPTION: header" in client.out - client.run(" install --requires=pkg2/0.1@user/testing -o *:shared=True -o pkg*:shared=header") - assert "pkg/0.1@user/testing: PKG OPTION: header" in client.out - assert "pkg2/0.1@user/testing: PKG2 OPTION: header" in client.out + client.run("create . --name=pkg2 --version=0.1 -o *:shared=True -o pkg*:shared=header") + assert "pkg/0.1: PKG OPTION: header" in client.out + assert "pkg2/0.1: PKG2 OPTION: header" in client.out + client.run(" install --requires=pkg2/0.1 -o *:shared=True -o pkg*:shared=header") + assert "pkg/0.1: PKG OPTION: header" in client.out + assert "pkg2/0.1: PKG2 OPTION: header" in client.out def test_install_package_folder(client): @@ -297,9 +298,9 @@ def test_install_no_remotes(client): def test_install_skip_disabled_remote(): - client = TestClient(servers=OrderedDict({"default": TestServer(), - "server2": TestServer(), - "server3": TestServer()}), + client = TestClient(servers={"default": TestServer(), + "server2": TestServer(), + "server3": TestServer()}, inputs=2*["admin", "password"]) client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=pkg --version=0.1 --user=lasote --channel=testing") diff --git a/test/integration/command/list/list_test.py b/test/integration/command/list/list_test.py index 202408be4d1..579485f4287 100644 --- a/test/integration/command/list/list_test.py +++ b/test/integration/command/list/list_test.py @@ -3,7 +3,6 @@ import re import textwrap import time -from collections import OrderedDict from unittest.mock import patch, Mock import pytest @@ -85,9 +84,8 @@ def test_graph_file_error(self): @pytest.fixture(scope="module") def client(): - servers = OrderedDict([("default", TestServer()), - ("other", TestServer())]) - c = TestClient(servers=servers, inputs=2*["admin", "password"]) + c = TestClient(servers={"default": TestServer(), "other": TestServer()}, + inputs=2*["admin", "password"]) c.save({ "zlib.py": GenConanfile("zlib"), "zlib_ng.py": GenConanfile("zlib_ng", "1.0.0"), diff --git a/test/integration/command/list/search_test.py b/test/integration/command/list/search_test.py index a6ceffacbc5..dc299c2dbf9 100644 --- a/test/integration/command/list/search_test.py +++ b/test/integration/command/list/search_test.py @@ -1,5 +1,5 @@ import textwrap -from collections import OrderedDict + from unittest.mock import patch, Mock import pytest @@ -15,18 +15,14 @@ class TestSearch: @pytest.fixture def remotes(self): - self.servers = OrderedDict() - self.servers["remote1"] = TestServer(server_capabilities=[]) - self.servers["remote2"] = TestServer(server_capabilities=[]) - - self.client = TestClient(servers=self.servers) + self.servers = {"remote1": TestServer(server_capabilities=[]), + "remote2": TestServer(server_capabilities=[])} + self.client = TestClient(light=True, servers=self.servers) def test_search_no_params(self): - self.servers = OrderedDict() - self.client = TestClient(servers=self.servers) - - self.client.run("search", assert_error=True) - assert "error: the following arguments are required: reference" in self.client.out + client = TestClient(light=True, servers={}) + client.run("search", assert_error=True) + assert "error: the following arguments are required: reference" in client.out def test_search_no_matching_recipes(self, remotes): expected_output = ("Connecting to remote 'remote1' anonymously\n" @@ -40,11 +36,9 @@ def test_search_no_matching_recipes(self, remotes): assert expected_output == self.client.out def test_search_no_configured_remotes(self): - self.servers = OrderedDict() - self.client = TestClient(servers=self.servers) - - self.client.run("search whatever", assert_error=True) - assert "There are no remotes to search from" in self.client.out + client = TestClient(light=True) + client.run("search whatever", assert_error=True) + assert "There are no remotes to search from" in client.out def test_search_disabled_remote(self, remotes): self.client.run("remote disable remote1") @@ -57,25 +51,20 @@ class TestRemotes: @pytest.fixture(autouse=True) def _setup(self): - self.servers = OrderedDict() self.users = {} - self.client = TestClient() + self.servers = {} + self.client = TestClient(light=True) def _add_remote(self, remote_name): self.servers[remote_name] = TestServer(users={"user": "passwd"}) self.users[remote_name] = [("user", "passwd")] - self.client = TestClient(servers=self.servers, inputs=["user", "passwd"]) + self.client = TestClient(light=True, servers=self.servers, inputs=["user", "passwd"]) def _add_recipe(self, remote, reference): - conanfile = textwrap.dedent(""" - from conan import ConanFile - class MyLib(ConanFile): - pass - """) - - self.client.save({'conanfile.py': conanfile}) + self.client.save({'conanfile.py': GenConanfile()}) reference = RecipeReference.loads(str(reference)) - self.client.run(f"export . --name={reference.name} --version={reference.version} --user={reference.user} --channel={reference.channel}") + self.client.run(f"export . --name={reference.name} --version={reference.version} " + f"--user={reference.user} --channel={reference.channel}") self.client.run("upload --force -r {} {}".format(remote, reference)) @pytest.mark.parametrize("exc,output", [ @@ -96,11 +85,6 @@ def test_search_remote_errors_but_no_raising_exceptions(self, exc, output): """) assert expected_output == self.client.out - def test_no_remotes(self): - self.client.run("search something", assert_error=True) - expected_output = "There are no remotes to search from" - assert expected_output in self.client.out - def test_search_by_name(self): remote_name = "remote1" recipe_name = "test_recipe/1.0.0@user/channel" @@ -176,7 +160,6 @@ def test_search_in_one_remote(self): assert expected_output in self.client.out def test_search_package_found_in_one_remote(self): - remote1 = "remote1" remote2 = "remote2" @@ -249,7 +232,7 @@ def test_search_wildcard(self): def test_no_user_channel_error(): # https://github.com/conan-io/conan/issues/13170 - c = TestClient(default_server_user=True) + c = TestClient(light=True, default_server_user=True) c.save({"conanfile.py": GenConanfile("pkg")}) c.run("export . --version=1.0") c.run("export . --version=1.0 --user=user --channel=channel") diff --git a/test/integration/command/list/test_combined_pkglist_flows.py b/test/integration/command/list/test_combined_pkglist_flows.py index 906a3d42be9..eac7f51f007 100644 --- a/test/integration/command/list/test_combined_pkglist_flows.py +++ b/test/integration/command/list/test_combined_pkglist_flows.py @@ -1,5 +1,4 @@ import json -from collections import OrderedDict import pytest @@ -179,7 +178,7 @@ class TestPkgListFindRemote: """ we can recover a list of remotes for an already installed graph, for metadata download """ def test_graph_2_pkg_list_remotes(self): - servers = OrderedDict([("default", TestServer()), ("remote2", TestServer())]) + servers = {"default": TestServer(), "remote2": TestServer()} c = TestClient(servers=servers, inputs=2 * ["admin", "password"], light=True) c.save({"zlib/conanfile.py": GenConanfile("zlib", "1.0"), "app/conanfile.py": GenConanfile("app", "1.0").with_requires("zlib/1.0")}) @@ -329,7 +328,7 @@ class TestPkgListMerge: """ deep merge lists """ def test_graph_2_pkg_list_remotes(self): - servers = OrderedDict([("default", TestServer()), ("remote2", TestServer())]) + servers = {"default": TestServer(), "remote2": TestServer()} c = TestClient(servers=servers, inputs=2 * ["admin", "password"]) c.save({"zlib/conanfile.py": GenConanfile("zlib", "1.0").with_settings("build_type"), "bzip2/conanfile.py": GenConanfile("bzip2", "1.0").with_settings("build_type"), @@ -603,9 +602,8 @@ def test_context_only_binary_mismatch(self, context): tc = TestClient(light=True) tc.save({ "protobuf/conanfile.py": GenConanfile("protobuf", "1.0"), - "onnx/conanfile.py": GenConanfile("onnx", "1.0") - .with_requires("protobuf/1.0") - .with_tool_requires("protobuf/1.0")}) + "onnx/conanfile.py": GenConanfile("onnx", "1.0").with_requires("protobuf/1.0") + .with_tool_requires("protobuf/1.0")}) tc.run("create protobuf") tc.run("create onnx") diff --git a/test/integration/command/remote/remote_test.py b/test/integration/command/remote/remote_test.py index 45e57366103..a8173575d1c 100644 --- a/test/integration/command/remote/remote_test.py +++ b/test/integration/command/remote/remote_test.py @@ -1,7 +1,6 @@ import json import os import pytest -from collections import OrderedDict from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient, TestServer @@ -12,12 +11,8 @@ class TestRemoteServer: @pytest.fixture(autouse=True) def setup(self): - self.servers = OrderedDict() - for i in range(3): - test_server = TestServer() - self.servers["remote%d" % i] = test_server - - self.client = TestClient(servers=self.servers, inputs=3 * ["admin", "password"], light=True) + self.client = TestClient(servers={f"remote{i}": TestServer() for i in range(3)}, + inputs=3 * ["admin", "password"], light=True) def test_list_json(self): self.client.run("remote list --format=json") diff --git a/test/integration/command/remote/test_remote_users.py b/test/integration/command/remote/test_remote_users.py index d492d4b4296..3c67a679151 100644 --- a/test/integration/command/remote/test_remote_users.py +++ b/test/integration/command/remote/test_remote_users.py @@ -1,22 +1,22 @@ import json import textwrap import time -from collections import OrderedDict from datetime import timedelta from unittest.mock import patch from conan.internal.api.remotes.localdb import LocalDB +from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient, TestServer from conan.test.utils.env import environment_update -class TestUser: +class TestRemoteLogin: def test_command_user_no_remotes(self): """ Test that proper error is reported when no remotes are defined and conan user is executed """ - client = TestClient() + client = TestClient(light=True) client.run("remote list-users", assert_error=True) assert "ERROR: No remotes defined" in client.out @@ -26,10 +26,8 @@ def test_command_user_no_remotes(self): def test_command_user_list(self): """ Test list of user is reported for all remotes or queried remote """ - servers = OrderedDict() - servers["default"] = TestServer() - servers["test_remote_1"] = TestServer() - client = TestClient(servers=servers) + client = TestClient(light=True, servers={"default": TestServer(), + "test_remote_1": TestServer()}) # Test with wrong remote right error is reported client.run("remote login Test_Wrong_Remote foo", assert_error=True) @@ -45,7 +43,7 @@ def test_command_user_list(self): def test_with_remote_no_connect(self): test_server = TestServer() - client = TestClient(servers={"default": test_server}) + client = TestClient(light=True, servers={"default": test_server}) client.run('remote list-users') assert textwrap.dedent(""" default: @@ -61,7 +59,8 @@ def test_with_remote_no_connect(self): assert ('will', None, None) == localdb.get_login(test_server.fake_url) client.run('remote logout default') - assert "Changed user of remote 'default' from 'will' (anonymous) to 'None' (anonymous)" in client.out + assert ("Changed user of remote 'default' from 'will' (anonymous) to " + "'None' (anonymous)") in client.out assert (None, None, None) == localdb.get_login(test_server.fake_url) def test_command_user_with_password(self): @@ -71,14 +70,15 @@ def test_command_user_with_password(self): """ test_server = TestServer() servers = {"default": test_server} - client = TestClient(servers=servers, inputs=["admin", "password"]) + client = TestClient(light=True, servers=servers, inputs=["admin", "password"]) client.run('remote login default dummy -p ping_pong2', assert_error=True) assert "ERROR: Wrong user or password" in client.out client.run('remote login default admin -p password') assert "ERROR: Wrong user or password" not in client.out assert "Changed user of remote 'default' from 'None' (anonymous) to 'admin'" in client.out client.run('remote logout default') - assert "Changed user of remote 'default' from 'admin' (authenticated) to 'None' (anonymous)" in client.out + assert ("Changed user of remote 'default' from 'admin' (authenticated) to " + "'None' (anonymous)") in client.out localdb = LocalDB(client.cache_folder) assert (None, None, None) == localdb.get_login(test_server.fake_url) client.run('remote list-users') @@ -91,7 +91,7 @@ def test_command_user_with_password_spaces(self): """ test_server = TestServer(users={"lasote": 'my "password'}) servers = {"default": test_server} - client = TestClient(servers=servers, inputs=["lasote", "mypass"]) + client = TestClient(light=True, servers=servers, inputs=["lasote", "mypass"]) client.run(r'remote login default lasote -p="my \"password"') assert "Connecting to remote" not in client.out assert "Changed user of remote 'default' from 'None' (anonymous) to 'lasote'" in client.out @@ -103,22 +103,15 @@ def test_command_user_with_password_spaces(self): def test_clean(self): test_server = TestServer() servers = {"default": test_server} - client = TestClient(servers=servers, inputs=2*["admin", "password"]) - base = ''' -from conan import ConanFile - -class ConanLib(ConanFile): - name = "lib" - version = "0.1" -''' - files = {"conanfile.py": base} - client.save(files) + client = TestClient(light=True, servers=servers, inputs=2*["admin", "password"]) + client.save({"conanfile.py": GenConanfile("lib", "0.1")}) client.run("export . --user=lasote --channel=stable") client.run("upload lib/0.1@lasote/stable -r default --only-recipe") client.run("remote list-users") assert 'default:\n Username: admin\n authenticated: True' in client.out client.run("remote logout default") - assert "Changed user of remote 'default' from 'admin' (authenticated) to 'None' (anonymous)" in client.out + assert ("Changed user of remote 'default' from 'admin' (authenticated) to " + "'None' (anonymous)") in client.out client.run("remote list-users") assert 'default:\n No user' in client.out # --force will force re-authentication, otherwise not necessary to auth @@ -129,16 +122,17 @@ class ConanLib(ConanFile): def test_command_interactive_only(self): test_server = TestServer() servers = {"default": test_server} - client = TestClient(servers=servers, inputs=["password"]) + client = TestClient(light=True, servers=servers, inputs=["password"]) client.run('remote login default admin -p') - assert "Changed user of remote 'default' from 'None' (anonymous) to 'admin' (authenticated)" in client.out + assert ("Changed user of remote 'default' from 'None' (anonymous) to 'admin' " + "(authenticated)") in client.out def test_command_user_with_interactive_password_login_prompt_disabled(self): """ Interactive password should not work. """ test_server = TestServer() servers = {"default": test_server} - client = TestClient(servers=servers, inputs=[]) + client = TestClient(light=True, servers=servers, inputs=[]) conan_conf = "core:non_interactive=True" client.save_home({"global.conf": conan_conf}) client.run('remote login default admin -p', assert_error=True) @@ -149,12 +143,12 @@ def test_command_user_with_interactive_password_login_prompt_disabled(self): def test_authenticated(self): test_server = TestServer(users={"lasote": "mypass", "danimtb": "passpass"}) - servers = OrderedDict() - servers["default"] = test_server - servers["other_server"] = TestServer() - client = TestClient(servers=servers, inputs=["lasote", "mypass", "mypass", "mypass"]) + servers = {"default": test_server, "other_server": TestServer()} + client = TestClient(light=True, servers=servers, + inputs=["lasote", "mypass", "mypass", "mypass"]) client.run("remote logout default") - assert "Changed user of remote 'default' from 'None' (anonymous) to 'None' (anonymous)" in client.out + assert ("Changed user of remote 'default' from 'None' (anonymous) to " + "'None' (anonymous)") in client.out assert "[authenticated]" not in client.out client.run('remote set-user default bad_user') client.run("remote list-users") @@ -167,17 +161,17 @@ def test_authenticated(self): assert 'default:\n Username: lasote\n authenticated: True' in client.out client.run("remote login default danimtb -p passpass") - assert "Changed user of remote 'default' from 'lasote' (authenticated) to 'danimtb' (authenticated)" in client.out + assert ("Changed user of remote 'default' from 'lasote' (authenticated) to " + "'danimtb' (authenticated)") in client.out client.run("remote list-users") assert 'default:\n Username: danimtb\n authenticated: True' in client.out def test_json(self): default_server = TestServer(users={"lasote": "mypass", "danimtb": "passpass"}) other_server = TestServer() - servers = OrderedDict() - servers["default"] = default_server - servers["other_server"] = other_server - client = TestClient(servers=servers, inputs=["lasote", "mypass", "danimtb", "passpass"]) + servers = {"default": default_server, "other_server": other_server} + client = TestClient(light=True, servers=servers, + inputs=["lasote", "mypass", "danimtb", "passpass"]) client.run("remote list-users -f json") info = json.loads(client.stdout) assert info == [ @@ -294,19 +288,20 @@ def test_json(self): def test_skip_auth(self): default_server = TestServer(users={"lasote": "mypass", "danimtb": "passpass"}) - servers = OrderedDict() - servers["default"] = default_server - client = TestClient(servers=servers) + servers = {"default": default_server} + client = TestClient(light=True, servers=servers) # Regular auth client.run("remote login default lasote -p mypass") # Now skip the auth but keeping the same user client.run("remote set-user default lasote") - assert "Changed user of remote 'default' from 'lasote' (authenticated) to 'lasote' (authenticated)" in client.out + assert ("Changed user of remote 'default' from 'lasote' (authenticated) to " + "'lasote' (authenticated)") in client.out # If we change the user the credentials are removed client.run("remote set-user default flanders") - assert "Changed user of remote 'default' from 'lasote' (authenticated) to 'flanders' (anonymous)" in client.out + assert ("Changed user of remote 'default' from 'lasote' (authenticated) to " + "'flanders' (anonymous)") in client.out client.run("remote login default lasote -p BAD_PASS", assert_error=True) assert "Wrong user or password" in client.out @@ -315,10 +310,11 @@ def test_skip_auth(self): client.run("remote login default lasote -p mypass") def test_login_multiremote(self): - servers = OrderedDict() - servers["default"] = TestServer(users={"admin": "password"}) - servers["other"] = TestServer(users={"admin": "password"}) - c = TestClient(servers=servers, inputs=["admin", "password", "wrong", "wrong"]) + servers = { + "default": TestServer(users={"admin": "password"}), + "other": TestServer(users={"admin": "password"}), + } + c = TestClient(light=True, servers=servers, inputs=["admin", "password", "wrong", "wrong"]) # This must fail, not autthenticate in the next remote c.run("remote login *", assert_error=True) assert "ERROR: Wrong user or password. [Remote: other]" in c.out @@ -327,7 +323,7 @@ def test_login_multiremote(self): def test_user_removed_remote_removed(): # Make sure that removing a remote clears the credentials # https://github.com/conan-io/conan/issues/5562 - c = TestClient(default_server_user=True) + c = TestClient(light=True, default_server_user=True) server_url = c.servers["default"].fake_url c.run("remote login default admin -p password") localdb = LocalDB(c.cache_folder) @@ -340,11 +336,12 @@ def test_user_removed_remote_removed(): class TestRemoteAuth: def test_remote_auth(self): - servers = OrderedDict() - servers["default"] = TestServer(users={"lasote": "mypass", "danimtb": "passpass"}) - servers["other_server"] = TestServer(users={"lasote": "mypass"}) - c = TestClient(servers=servers, inputs=["lasote", "mypass", "danimtb", "passpass", - "lasote", "mypass"]) + servers = { + "default": TestServer(users={"lasote": "mypass", "danimtb": "passpass"}), + "other_server": TestServer(users={"lasote": "mypass"}), + } + c = TestClient(light=True, servers=servers, + inputs=["lasote", "mypass", "danimtb", "passpass", "lasote", "mypass"]) c.run("remote auth *") text = textwrap.dedent("""\ default: @@ -358,32 +355,34 @@ def test_remote_auth(self): assert result == {'default': {'user': 'lasote'}, 'other_server': {'user': 'lasote'}} def test_remote_auth_force(self): - servers = OrderedDict() - servers["default"] = TestServer(users={"lasote": "mypass", "danimtb": "passpass"}) - servers["other_server"] = TestServer(users={"lasote": "mypass"}) - c = TestClient(servers=servers, inputs=["lasote", "mypass", "danimtb", "passpass", - "lasote", "mypass"]) - - with patch("conan.internal.rest.rest_client_v2.RestV2Methods.check_credentials") as check_credentials_mock: + servers = { + "default": TestServer(users={"lasote": "mypass", "danimtb": "passpass"}), + "other_server": TestServer(users={"lasote": "mypass"}), + } + c = TestClient(light=True, servers=servers, + inputs=["lasote", "mypass", "danimtb", "passpass", "lasote", "mypass"]) + + with (patch("conan.internal.rest.rest_client_v2.RestV2Methods.check_credentials") + as check_credentials_mock): c.run("remote auth --force *") check_credentials_mock.assert_called_with(True) def test_remote_auth_force_false(self): - servers = OrderedDict() - servers["default"] = TestServer(users={"lasote": "mypass", "danimtb": "passpass"}) - servers["other_server"] = TestServer(users={"lasote": "mypass"}) - c = TestClient(servers=servers, inputs=["lasote", "mypass", "danimtb", "passpass", - "lasote", "mypass"]) - - with patch("conan.internal.rest.rest_client_v2.RestV2Methods.check_credentials") as check_credentials_mock: + servers = { + "default": TestServer(users={"lasote": "mypass", "danimtb": "passpass"}), + "other_server": TestServer(users={"lasote": "mypass"}), + } + c = TestClient(light=True, servers=servers, + inputs=["lasote", "mypass", "danimtb", "passpass", "lasote", "mypass"]) + + with (patch("conan.internal.rest.rest_client_v2.RestV2Methods.check_credentials") + as check_credentials_mock): c.run("remote auth *") check_credentials_mock.assert_called_with(False) def test_remote_auth_with_user(self): - servers = OrderedDict() - servers["default"] = TestServer(users={"lasote": "mypass"}) - servers["other_server"] = TestServer() - c = TestClient(servers=servers, inputs=["lasote", "mypass"]) + servers = {"default": TestServer(users={"lasote": "mypass"}), "other_server": TestServer()} + c = TestClient(light=True, servers=servers, inputs=["lasote", "mypass"]) c.run("remote set-user default lasote") c.run("remote auth * --with-user") text = textwrap.dedent("""\ @@ -394,10 +393,8 @@ def test_remote_auth_with_user(self): assert text in c.out def test_remote_auth_with_user_env_var(self): - servers = OrderedDict() - servers["default"] = TestServer(users={"lasote": "mypass"}) - servers["other_server"] = TestServer() - c = TestClient(servers=servers) + servers = {"default": TestServer(users={"lasote": "mypass"}), "other_server": TestServer()} + c = TestClient(light=True, servers=servers) with environment_update({"CONAN_LOGIN_USERNAME_DEFAULT": "lasote", "CONAN_PASSWORD_DEFAULT": "mypass"}): c.run("remote auth * --with-user") @@ -409,18 +406,19 @@ def test_remote_auth_with_user_env_var(self): assert text in c.out def test_remote_auth_error(self): - servers = OrderedDict() - servers["default"] = TestServer(users={"user": "password"}) - c = TestClient(servers=servers, inputs=["user1", "pass", "user2", "pass", "user3", "pass"]) + servers = {"default": TestServer(users={"user": "password"})} + c = TestClient(light=True, servers=servers, + inputs=["user1", "pass", "user2", "pass", "user3", "pass"]) c.run("remote auth *") assert "error: Too many failed login attempts, bye!" in c.out def test_remote_auth_server_expire_token_secret(self): server = TestServer(users={"myuser": "password", "myotheruser": "otherpass"}) - c = TestClient(servers={"default": server}, inputs=["myuser", "password", - "myotheruser", "otherpass", - "user", "pass", "user", "pass", - "user", "pass"]) + c = TestClient(light=True, servers={"default": server}, + inputs=["myuser", "password", + "myotheruser", "otherpass", + "user", "pass", "user", "pass", + "user", "pass"]) c.run("remote auth *") assert "Remote 'default' needs authentication, obtaining credentials" in c.out assert "user: myuser" in c.out @@ -439,10 +437,11 @@ def test_remote_auth_server_expire_token_secret(self): def test_remote_auth_server_expire_token(self): server = TestServer(users={"myuser": "password", "myotheruser": "otherpass"}) server.test_server.ra.api_v2.credentials_manager.expire_time = timedelta(seconds=2) - c = TestClient(servers={"default": server}, inputs=["myuser", "password", - "myotheruser", "otherpass", - "user", "pass", "user", "pass", - "user", "pass"]) + c = TestClient(light=True, servers={"default": server}, + inputs=["myuser", "password", + "myotheruser", "otherpass", + "user", "pass", "user", "pass", + "user", "pass"]) c.run("remote auth *") assert "user: myuser" in c.out # token not expired yet, should work @@ -459,7 +458,7 @@ def test_remote_auth_server_expire_token(self): def test_auth_after_logout(self): server = TestServer(users={"myuser": "password"}) - c = TestClient(servers={"default": server}, inputs=["myuser", "password"]*2) + c = TestClient(light=True, servers={"default": server}, inputs=["myuser", "password"]*2) c.run("remote auth *") assert "Remote 'default' needs authentication, obtaining credentials" in c.out assert "user: myuser" in c.out diff --git a/test/integration/command/source_test.py b/test/integration/command/source_test.py index f818ec3133a..8c65e139824 100644 --- a/test/integration/command/source_test.py +++ b/test/integration/command/source_test.py @@ -1,7 +1,6 @@ import os import re import textwrap -from collections import OrderedDict import pytest @@ -72,12 +71,8 @@ def source(self): def test_source_warning_os_build(self): # https://github.com/conan-io/conan/issues/2368 - conanfile = '''from conan import ConanFile -class ConanLib(ConanFile): - pass -''' client = TestClient(light=True) - client.save({CONANFILE: conanfile}) + client.save({CONANFILE: GenConanfile()}) client.run("source .") assert "This package defines both 'os' and 'os_build'" not in client.out @@ -87,8 +82,8 @@ def test_source_with_path_errors(self): # Path with conanfile.txt client.run("source conanfile.txt", assert_error=True) - assert "A conanfile.py is needed, %s is not acceptable" \ - % os.path.join(client.current_folder, "conanfile.txt") in client.out + assert ("A conanfile.py is needed, %s is not acceptable" + % os.path.join(client.current_folder, "conanfile.txt") in client.out) def test_source_local_cwd(self): conanfile = ''' @@ -175,7 +170,7 @@ def test_retrieve_exports_sources(self): # For Conan 2.0 if we install a package from a remote and we want to upload to other # remote we need to download the sources, as we consider revisions immutable, let's # iterate through the remotes to get the sources from the first match - servers = OrderedDict() + servers = {} for index in range(2): servers[f"server{index}"] = TestServer([("*/*@*/*", "*")], [("*/*@*/*", "*")], users={"user": "password"}) diff --git a/test/integration/command/test_outdated.py b/test/integration/command/test_outdated.py index 4c94e38bc7a..57b32cd8c60 100644 --- a/test/integration/command/test_outdated.py +++ b/test/integration/command/test_outdated.py @@ -1,5 +1,4 @@ import json -from collections import OrderedDict import pytest @@ -106,11 +105,7 @@ def test_two_remotes(): # remote1: zlib/1.0, libcurl/2.0, foo/1.0 # remote2: zlib/[1.0, 2.0], libcurl/1.0, foo/1.0 # local cache: zlib/1.0, libcurl/1.0, foo/1.0 - servers = OrderedDict() - for i in [1, 2]: - test_server = TestServer() - servers["remote%d" % i] = test_server - + servers = {f"remote{i}": TestServer() for i in (1, 2)} tc = TestClient(servers=servers, inputs=2 * ["admin", "password"], light=True) tc.save({"conanfile.py": GenConanfile()}) @@ -150,7 +145,7 @@ def test_two_remotes(): def test_duplicated_tool_requires(): - tc = TestClient(default_server_user=True) + tc = TestClient(default_server_user=True, light=True) # Create libraries needed to generate the dependency graph tc.save({"conanfile.py": GenConanfile()}) tc.run("create . --name=cmake --version=1.0") @@ -176,7 +171,7 @@ def test_duplicated_tool_requires(): def test_no_outdated_dependencies(): - tc = TestClient(default_server_user=True) + tc = TestClient(default_server_user=True, light=True) tc.save({"conanfile.py": GenConanfile()}) tc.run("create . --name=foo --version=1.0") diff --git a/test/integration/command/upload/upload_test.py b/test/integration/command/upload/upload_test.py index 30de88a034d..94fca4d45bf 100644 --- a/test/integration/command/upload/upload_test.py +++ b/test/integration/command/upload/upload_test.py @@ -3,7 +3,6 @@ import platform import stat import textwrap -from collections import OrderedDict import pytest from unittest.mock import patch @@ -374,10 +373,7 @@ def test_upload_key_error(self): files = {"conanfile.py": GenConanfile("hello0", "1.2.1")} server1 = TestServer([("*/*@*/*", "*")], [("*/*@*/*", "*")], users={"lasote": "mypass"}) server2 = TestServer([("*/*@*/*", "*")], [("*/*@*/*", "*")], users={"lasote": "mypass"}) - servers = OrderedDict() - servers["server1"] = server1 - servers["server2"] = server2 - client = TestClient(servers=servers) + client = TestClient(servers={"server1": server1, "server2": server2}, light=True) client.save(files) client.run("create . --user=user --channel=testing") client.run("remote login server1 lasote -p mypass") diff --git a/test/integration/configuration/profile_test.py b/test/integration/configuration/profile_test.py index ff59ed1ec63..09ac4951e0d 100644 --- a/test/integration/configuration/profile_test.py +++ b/test/integration/configuration/profile_test.py @@ -2,7 +2,6 @@ import os import platform import textwrap -from collections import OrderedDict from textwrap import dedent import pytest @@ -109,10 +108,12 @@ def test_install_with_missing_profile(self, path): @pytest.mark.skipif(platform.system() != "Windows", reason="Windows profiles") def test_install_profile_settings(self): # Create a profile and use it - profile_settings = OrderedDict([("compiler", "msvc"), - ("compiler.version", "191"), - ("compiler.runtime", "dynamic"), - ("arch", "x86")]) + profile_settings = { + "compiler": "msvc", + "compiler.version": "191", + "compiler.runtime": "dynamic", + "arch": "x86", + } create_profile(self.client.paths.profiles_path, "vs_12_86", settings=profile_settings, package_settings={}) @@ -134,10 +135,11 @@ def test_install_profile_settings(self): assert "compiler.version=191" in info # Use package settings in profile - tmp_settings = OrderedDict() - tmp_settings["compiler"] = "gcc" - tmp_settings["compiler.libcxx"] = "libstdc++11" - tmp_settings["compiler.version"] = "4.8" + tmp_settings = { + "compiler": "gcc", + "compiler.libcxx": "libstdc++11", + "compiler.version": "4.8", + } package_settings = {"hello0/*": tmp_settings} create_profile(self.client.paths.profiles_path, "vs_12_86_hello0_gcc", settings=profile_settings, @@ -182,17 +184,20 @@ def configure(self): self.client.save({"conanfile.py": conanfile}) # Create a profile and use it - profile_settings = OrderedDict([("os", "Windows"), - ("compiler", "msvc"), - ("compiler.version", "191"), - ("compiler.runtime", "dynamic"), - ("arch", "x86")]) + profile_settings = { + "os": "Windows", + "compiler": "msvc", + "compiler.version": "191", + "compiler.runtime": "dynamic", + "arch": "x86", + } # Use package settings in profile - tmp_settings = OrderedDict() - tmp_settings["compiler"] = "gcc" - tmp_settings["compiler.libcxx"] = "libstdc++11" - tmp_settings["compiler.version"] = "4.8" + tmp_settings = { + "compiler": "gcc", + "compiler.libcxx": "libstdc++11", + "compiler.version": "4.8", + } package_settings = {"*@lasote/*": tmp_settings} _create_profile(self.client.paths.profiles_path, "myprofile", settings=profile_settings, diff --git a/test/integration/py_requires/python_requires_test.py b/test/integration/extensions/python_requires_test.py similarity index 100% rename from test/integration/py_requires/python_requires_test.py rename to test/integration/extensions/python_requires_test.py diff --git a/test/integration/graph/core/test_version_ranges.py b/test/integration/graph/core/test_version_ranges.py index 4f9a19d65d6..66834ec18e1 100644 --- a/test/integration/graph/core/test_version_ranges.py +++ b/test/integration/graph/core/test_version_ranges.py @@ -1,4 +1,3 @@ -from collections import OrderedDict import pytest from conan.api.model import Remote @@ -278,10 +277,7 @@ def test_transitive_fixed_conflict(self): deps_graph = self.build_consumer(consumer, install=False) assert type(deps_graph.error) is GraphConflictError - assert 2 == len(deps_graph.nodes) - app = deps_graph.root - libb = app.edges[0].dst def test_transitive_fixed_conflict_forced(self): # app ---> libb/0.1 -----------> liba/1.2 @@ -391,18 +387,15 @@ def test_remote_version_ranges(): def test_different_user_channel_resolved_correctly(): - server1 = TestServer() - server2 = TestServer() - servers = OrderedDict([("server1", server1), ("server2", server2)]) - - client = TestClient(servers=servers, inputs=2*["admin", "password"], light=True) + client = TestClient(servers={"server1": TestServer(), "server2": TestServer()}, + inputs=2*["admin", "password"], light=True) client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=lib --version=1.0 --user=conan --channel=stable") client.run("create . --name=lib --version=1.0 --user=conan --channel=testing") client.run("upload lib/1.0@conan/stable -r=server1") client.run("upload lib/1.0@conan/testing -r=server2") - client2 = TestClient(servers=servers, light=True) + client2 = TestClient(servers=client.servers, light=True) client2.run("install --requires=lib/[>=1.0]@conan/testing") assert f"lib/1.0@conan/testing: Retrieving package {NO_SETTINGS_PACKAGE_ID} " \ f"from remote 'server2' " in client2.out diff --git a/test/integration/graph/version_ranges/version_ranges_cached_test.py b/test/integration/graph/version_ranges/version_ranges_cached_test.py index 7aeaa764ae9..2748999de66 100644 --- a/test/integration/graph/version_ranges/version_ranges_cached_test.py +++ b/test/integration/graph/version_ranges/version_ranges_cached_test.py @@ -1,6 +1,3 @@ -from collections import OrderedDict - -import pytest from unittest.mock import patch from conan.internal.rest.remote_manager import RemoteManager @@ -11,27 +8,14 @@ class TestVersionRangesCache: - @pytest.fixture(autouse=True) - def _setup(self): - self.counters = {"server0": 0, "server1": 0} - - def _mocked_search_recipes(self, remote, pattern, ignorecase=True): - packages = { - "server0": [RecipeReference.loads("liba/1.0.0"), - RecipeReference.loads("liba/1.1.0")], - "server1": [RecipeReference.loads("liba/2.0.0"), - RecipeReference.loads("liba/2.1.0")] - } - self.counters[remote.name] = self.counters[remote.name] + 1 - return packages[remote.name] - def test_version_ranges_cached(self): - servers = OrderedDict() + servers = {} for index in range(2): servers[f"server{index}"] = TestServer([("*/*@*/*", "*")], [("*/*@*/*", "*")], users={"user": "password"}) - client = TestClient(light=True, servers=servers, inputs=["user", "password", "user", "password"]) + client = TestClient(light=True, servers=servers, + inputs=["user", "password", "user", "password"]) # server0 does not satisfy range # server1 does @@ -58,12 +42,22 @@ def test_version_ranges_cached(self): .with_requires("libb/1.0", "libc/1.0")}) # should call only once to server0 - self.counters["server0"] = 0 - self.counters["server1"] = 0 - with patch.object(RemoteManager, "search_recipes", new=self._mocked_search_recipes): + counters = {"server0": 0, "server1": 0} + + def _mocked_search_recipes(_, remote, pattern, ignorecase=True): # noqa + packages = { + "server0": [RecipeReference.loads("liba/1.0.0"), + RecipeReference.loads("liba/1.1.0")], + "server1": [RecipeReference.loads("liba/2.0.0"), + RecipeReference.loads("liba/2.1.0")] + } + counters[remote.name] = counters[remote.name] + 1 + return packages[remote.name] + + with patch.object(RemoteManager, "search_recipes", new=_mocked_search_recipes): client.run("create . --update") - assert self.counters["server0"] == 1 - assert self.counters["server1"] == 1 + assert counters["server0"] == 1 + assert counters["server1"] == 1 class TestVersionRangesDiamond: diff --git a/test/integration/graph/version_ranges/version_ranges_diamond_test.py b/test/integration/graph/version_ranges/version_ranges_diamond_test.py index 748ff899196..9c21dc6a4f0 100644 --- a/test/integration/graph/version_ranges/version_ranges_diamond_test.py +++ b/test/integration/graph/version_ranges/version_ranges_diamond_test.py @@ -1,5 +1,3 @@ -from collections import OrderedDict - from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient, TestServer @@ -70,10 +68,8 @@ def test_update(self): class TestVersionRangesMultiRemote: def test_multi_remote(self): - servers = OrderedDict() - servers["default"] = TestServer() - servers["other"] = TestServer() - client = TestClient(light=True, servers=servers, inputs=2*["admin", "password"]) + client = TestClient(light=True, servers={"default": TestServer(), "other": TestServer()}, + inputs=2 * ["admin", "password"]) client.save({"hello0/conanfile.py": GenConanfile("hello0"), "hello1/conanfile.py": GenConanfile("hello1").with_requires("hello0/[*]")}) client.run("export hello0 --version=0.1") diff --git a/test/integration/metadata/test_metadata_deploy.py b/test/integration/metadata/test_metadata_deploy.py index 1737c61aaaf..8fe545e477f 100644 --- a/test/integration/metadata/test_metadata_deploy.py +++ b/test/integration/metadata/test_metadata_deploy.py @@ -1,6 +1,5 @@ import os.path import textwrap -from collections import OrderedDict import pytest @@ -43,7 +42,7 @@ def deploy(graph, output_folder, **kwargs): d.ref.name)) """) - servers = OrderedDict([("default", TestServer()), ("remote2", TestServer())]) + servers = {"default": TestServer(), "remote2": TestServer()} c = TestClient(servers=servers, inputs=2 * ["admin", "password"], light=True) c.save({"conanfile.py": conanfile, "deploy.py": deploy}) diff --git a/test/integration/py_requires/__init__.py b/test/integration/py_requires/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/test/integration/remote/multi_remote_checks_test.py b/test/integration/remote/multi_remote_checks_test.py index c4c9d2783cc..6ae4e0d74ba 100644 --- a/test/integration/remote/multi_remote_checks_test.py +++ b/test/integration/remote/multi_remote_checks_test.py @@ -1,5 +1,3 @@ -from collections import OrderedDict - from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import NO_SETTINGS_PACKAGE_ID, TestClient, TestServer @@ -7,14 +5,9 @@ class TestRemoteChecks: def test_binary_defines_remote(self): - servers = OrderedDict([("server1", TestServer()), - ("server2", TestServer()), - ("server3", TestServer())]) + servers = {"server1": TestServer(), "server2": TestServer(), "server3": TestServer()} client = TestClient(servers=servers, inputs=3*["admin", "password"]) - conanfile = """from conan import ConanFile -class Pkg(ConanFile): - pass""" - client.save({"conanfile.py": conanfile}) + client.save({"conanfile.py": GenConanfile()}) client.run("create . --name=pkg --version=0.1 --user=lasote --channel=testing") client.run("upload pkg* -r=server1 --confirm") client.run("upload pkg* -r=server2 --confirm") @@ -28,7 +21,7 @@ class Pkg(ConanFile): client.assert_listed_binary( {"pkg/0.1@lasote/testing": (NO_SETTINGS_PACKAGE_ID, "Download (server1)")}) assert "pkg/0.1@lasote/testing: Retrieving package " \ - "%s from remote 'server1'" % NO_SETTINGS_PACKAGE_ID in client.out + "%s from remote 'server1'" % NO_SETTINGS_PACKAGE_ID in client.out # Explicit remote also defines the remote client.run("remove * -c") @@ -37,7 +30,7 @@ class Pkg(ConanFile): client.assert_listed_binary( {"pkg/0.1@lasote/testing": (NO_SETTINGS_PACKAGE_ID, "Download (server2)")}) assert "pkg/0.1@lasote/testing: Retrieving package " \ - "%s from remote 'server2'" % NO_SETTINGS_PACKAGE_ID in client.out + "%s from remote 'server2'" % NO_SETTINGS_PACKAGE_ID in client.out # Ordered search of binary works client.run("remove * -c") @@ -47,7 +40,7 @@ class Pkg(ConanFile): client.assert_listed_binary( {"pkg/0.1@lasote/testing": (NO_SETTINGS_PACKAGE_ID, "Download (server2)")}) assert "pkg/0.1@lasote/testing: Retrieving package " \ - "%s from remote 'server2'" % NO_SETTINGS_PACKAGE_ID in client.out + "%s from remote 'server2'" % NO_SETTINGS_PACKAGE_ID in client.out # Download recipe and binary from the remote2 by iterating client.run("remove * -c") @@ -56,42 +49,35 @@ class Pkg(ConanFile): client.assert_listed_binary( {"pkg/0.1@lasote/testing": (NO_SETTINGS_PACKAGE_ID, "Download (server2)")}) assert "pkg/0.1@lasote/testing: Retrieving package " \ - "%s from remote 'server2'" % NO_SETTINGS_PACKAGE_ID in client.out + "%s from remote 'server2'" % NO_SETTINGS_PACKAGE_ID in client.out def test_binaries_from_different_remotes(self): - servers = OrderedDict() - servers["server1"] = TestServer() - servers["server2"] = TestServer() + servers = {"server1": TestServer(), "server2": TestServer()} client = TestClient(servers=servers, inputs=2*["admin", "password"]) - conanfile = """from conan import ConanFile -class Pkg(ConanFile): - options = {"opt": [1, 2, 3]} -""" - client.save({"conanfile.py": conanfile}) - client.run("create . --name=pkg --version=0.1 --user=lasote --channel=testing -o pkg/*:opt=1") + client.save({"conanfile.py": GenConanfile().with_option("opt", [1, 2, 3])}) + client.run("create . --name=pkg --version=0.1 -o pkg/*:opt=1") client.run("upload pkg* -r=server1 --confirm") client.run("remove *:* -c") - client.run("create . --name=pkg --version=0.1 --user=lasote --channel=testing -o pkg/*:opt=2") - package_id2 = client.created_package_id("pkg/0.1@lasote/testing") + client.run("create . --name=pkg --version=0.1 -o pkg/*:opt=2") + package_id2 = client.created_package_id("pkg/0.1") client.run("upload pkg* -r=server2 --confirm") client.run("remove *:* -c") # recipe is cached, takes binary from server2 - client.run("install --requires=pkg/0.1@lasote/testing -o pkg/*:opt=2 -r=server2") - client.assert_listed_binary({"pkg/0.1@lasote/testing": (package_id2, "Download (server2)")}) - assert f"pkg/0.1@lasote/testing: Retrieving package {package_id2} " \ - "from remote 'server2'" in client.out + client.run("install --requires=pkg/0.1 -o pkg/*:opt=2 -r=server2") + client.assert_listed_binary({"pkg/0.1": (package_id2, "Download (server2)")}) + assert f"pkg/0.1: Retrieving package {package_id2} from remote 'server2'" in client.out # Nothing to update - client.run("install --requires=pkg/0.1@lasote/testing -o pkg/*:opt=2 -r=server2 -u") - client.assert_listed_binary({"pkg/0.1@lasote/testing": (package_id2, "Cache")}) + client.run("install --requires=pkg/0.1 -o pkg/*:opt=2 -r=server2 -u") + client.assert_listed_binary({"pkg/0.1": (package_id2, "Cache")}) # Build missing - client.run("install --requires=pkg/0.1@lasote/testing -o pkg/*:opt=3 -r=server2", assert_error=True) - assert "ERROR: Missing prebuilt package for 'pkg/0.1@lasote/testing'" in client.out + client.run("install --requires=pkg/0.1 -o pkg/*:opt=3 -r=server2", assert_error=True) + assert "ERROR: Missing prebuilt package for 'pkg/0.1'" in client.out - client.run("install --requires=pkg/0.1@lasote/testing -o pkg/*:opt=3", assert_error=True) - assert "ERROR: Missing prebuilt package for 'pkg/0.1@lasote/testing'" in client.out + client.run("install --requires=pkg/0.1 -o pkg/*:opt=3", assert_error=True) + assert "ERROR: Missing prebuilt package for 'pkg/0.1'" in client.out def test_version_range_multi_remote(): @@ -100,9 +86,7 @@ def test_version_range_multi_remote(): valid is found in the first server, it is return - Using --update forces to really update to the latest version available, anywhere """ - servers = OrderedDict([("server1", TestServer()), - ("server2", TestServer()), - ("server3", TestServer())]) + servers = {"server1": TestServer(), "server2": TestServer(), "server3": TestServer()} client = TestClient(servers=servers, inputs=3*["admin", "password"]) client.save({"conanfile.py": GenConanfile()}) for i in (1, 2, 3): diff --git a/test/integration/remote/multi_remote_test.py b/test/integration/remote/multi_remote_test.py index 012266f5c95..d426ed2161d 100644 --- a/test/integration/remote/multi_remote_test.py +++ b/test/integration/remote/multi_remote_test.py @@ -1,5 +1,3 @@ -import pytest -from collections import OrderedDict from time import sleep from conan.api.model import RecipeReference @@ -11,47 +9,38 @@ class TestExportsSourcesMissing: def test_exports_sources_missing(self): - client = TestClient(default_server_user=True) + client = TestClient(light=True, default_server_user=True) client.save({"conanfile.py": GenConanfile().with_exports_sources("*"), "source.txt": "somesource"}) client.run("create . --name=pkg --version=0.1 --user=user --channel=testing") client.run("upload pkg/0.1@user/testing -r default") # Failure because remote is removed - servers = OrderedDict(client.servers) + servers = dict(client.servers) servers["new_server"] = TestServer(users={"user": "password"}) - client2 = TestClient(servers=servers, inputs=["user", "password"]) + client2 = TestClient(light=True, servers=servers, inputs=["user", "password"]) client2.run("install --requires=pkg/0.1@user/testing") client2.run("remote remove default") client2.run("upload pkg/0.1@user/testing -r=new_server", assert_error=True) assert "The 'pkg/0.1@user/testing' package has 'exports_sources' but sources " \ - "not found in local cache." in client2.out - assert "Probably it was installed from a remote that is no longer available." \ - in client2.out + "not found in local cache." in client2.out + assert "Probably it was installed from a remote that is no longer available." in client2.out # Failure because remote removed the package - client2 = TestClient(servers=servers, inputs=2*["admin", "password"]) + client2 = TestClient(light=True, servers=servers, inputs=2 * ["admin", "password"]) client2.run("install --requires=pkg/0.1@user/testing") client2.run("remove * -r=default -c") client2.run("upload pkg/0.1@user/testing -r=new_server", assert_error=True) assert "pkg/0.1@user/testing Error while compressing: The 'pkg/0.1@user/testing' " \ - in client2.out - assert "The 'pkg/0.1@user/testing' package has 'exports_sources' but sources " \ - in client2.out - assert "Probably it was installed from a remote that is no longer available." \ - in client2.out + in client2.out + assert "The 'pkg/0.1@user/testing' package has 'exports_sources' but sources " in client2.out + assert "Probably it was installed from a remote that is no longer available." in client2.out class TestMultiRemotes: - @pytest.fixture(autouse=True) - def setup(self): - self.servers = OrderedDict() - self.servers["default"] = TestServer() - self.servers["local"] = TestServer() - @staticmethod - def _create(client, number, version, modifier=""): + def _export(client, number, version, modifier=""): files = {CONANFILE: str(GenConanfile(number, version)) + modifier} client.save(files, clean_first=True) client.run("export . --user=lasote --channel=stable") @@ -60,11 +49,12 @@ def test_conan_install_build_flag(self): """ Checks conan install --update works with different remotes """ - client_a = TestClient(servers=self.servers, inputs=2*["admin", "password"]) - client_b = TestClient(servers=self.servers, inputs=2*["admin", "password"]) + servers = {"default": TestServer(), "local": TestServer()} + client_a = TestClient(light=True, servers=servers, inputs=2*["admin", "password"]) + client_b = TestClient(light=True, servers=servers, inputs=2*["admin", "password"]) # Upload hello0 to local and default from client_a - self._create(client_a, "hello0", "0.0") + self._export(client_a, "hello0", "0.0") client_a.run("upload hello0/0.0@lasote/stable -r local --only-recipe") client_a.run("upload hello0/0.0@lasote/stable -r default --only-recipe") sleep(1) # For timestamp and updates checks @@ -73,7 +63,7 @@ def test_conan_install_build_flag(self): client_b.run("install --requires=hello0/0.0@lasote/stable -r local --build missing") # Update hello0 with client_a and reupload - self._create(client_a, "hello0", "0.0", modifier="\n") + self._export(client_a, "hello0", "0.0", modifier="\n") client_a.run("upload hello0/0.0@lasote/stable -r local --only-recipe") assert "Uploading recipe 'hello0/0.0@lasote/stable" in client_a.out @@ -84,13 +74,12 @@ def test_conan_install_build_flag(self): # Now try to update the package with install -u client_b.run("install --requires=hello0/0.0@lasote/stable -u --build='*'") - assert "hello0/0.0@lasote/stable#64fd8ae21db9eff69c6c681b0e2fc178 - Updated" \ - in client_b.out + assert "hello0/0.0@lasote/stable#64fd8ae21db9eff69c6c681b0e2fc178 - Updated" in client_b.out # Upload a new version from client A, but only to the default server (not the ref-listed) # Upload hello0 to local and default from client_a sleep(1) # For timestamp and updates checks - self._create(client_a, "hello0", "0.0", modifier="\n\n") + self._export(client_a, "hello0", "0.0", modifier="\n\n") client_a.run("upload hello0/0.0@lasote/stable#latest -r default --only-recipe") # Now client_b checks for updates without -r parameter @@ -107,8 +96,7 @@ def test_conan_install_build_flag(self): # Well, now try to update the package with -r default -u client_b.run("install --requires=hello0/0.0@lasote/stable -r default -u --build='*'") - assert "hello0/0.0@lasote/stable: Forced build from source" \ - in str(client_b.out) + assert "hello0/0.0@lasote/stable: Forced build from source" in str(client_b.out) # TODO: cache2.0 conan info not yet implemented with new cache client_b.run("graph info --requires=hello0/0.0@lasote/stable -u") assert "recipe: Cache" in client_b.out @@ -118,15 +106,16 @@ def test_conan_install_update(self): """ Checks conan install --update works only with the remote associated """ - client = TestClient(servers=self.servers, inputs=2*["admin", "password"]) + servers = {"default": TestServer(), "local": TestServer()} + client = TestClient(light=True, servers=servers, inputs=2*["admin", "password"]) - self._create(client, "hello0", "0.0") + self._export(client, "hello0", "0.0") default_remote_rev = client.exported_recipe_revision() client.run("install --requires=hello0/0.0@lasote/stable --build missing") client.run("upload hello0/0.0@lasote/stable -r default") sleep(1) # For timestamp and updates checks - self._create(client, "hello0", "0.0", modifier=" ") + self._export(client, "hello0", "0.0", modifier=" ") local_remote_rev = client.exported_recipe_revision() client.run("install --requires=hello0/0.0@lasote/stable --build missing") @@ -140,13 +129,12 @@ def test_conan_install_update(self): assert f"hello0/0.0@lasote/stable#{local_remote_rev} - Updated" in client.out client.run("install --requires=hello0/0.0@lasote/stable --update -r default") - assert f"hello0/0.0@lasote/stable#{local_remote_rev} - Newer" \ - in client.out + assert f"hello0/0.0@lasote/stable#{local_remote_rev} - Newer" in client.out sleep(1) # For timestamp and updates checks # Check that it really updates in case of newer package uploaded to the associated remote - client_b = TestClient(servers=self.servers, inputs=3*["admin", "password"]) - self._create(client_b, "hello0", "0.0", modifier=" ") + client_b = TestClient(light=True, servers=servers, inputs=3*["admin", "password"]) + self._export(client_b, "hello0", "0.0", modifier=" ") new_local_remote_rev = client_b.exported_recipe_revision() client_b.run("install --requires=hello0/0.0@lasote/stable --build missing") client_b.run("upload hello0/0.0@lasote/stable -r local") @@ -156,52 +144,39 @@ def test_conan_install_update(self): class TestMultiRemote: - @pytest.fixture(autouse=True) - def setup(self): - self.servers = OrderedDict() - self.users = {} - for i in range(3): - test_server = TestServer() - self.servers["remote%d" % i] = test_server - self.users["remote%d" % i] = [("admin", "password")] - - self.client = TestClient(servers=self.servers, inputs=3*["admin", "password"]) - def test_fail_when_not_notfound(self): """ If a remote fails with a 404 it has to keep looking in the next remote, but if it fails by any other reason it has to stop """ - servers = OrderedDict() - servers["s0"] = TestServer() - servers["s1"] = TestServer() - servers["s2"] = TestServer() + servers = {"s0": TestServer(), "s1": TestServer(), "s2": TestServer()} - client = TestClient(servers=servers) + client = TestClient(light=True, servers=servers) client.save({"conanfile.py": GenConanfile("mylib", "0.1")}) client.run("create . --user=lasote --channel=testing") client.run("remote login s1 admin -p password") client.run("upload mylib* -r s1 -c") servers["s1"].fake_url = "http://asdlhaljksdhlajkshdljakhsd.com" # Do not exist - client2 = TestClient(servers=servers) + client2 = TestClient(light=True, servers=servers) client2.run("install --requires=mylib/0.1@conan/testing --build=missing", assert_error=True) assert "mylib/0.1@conan/testing: Checking remote: s0" in client2.out assert "mylib/0.1@conan/testing: Checking remote: s1" in client2.out - assert "Unable to connect to remote s1=http://asdlhaljksdhlajkshdljakhsd.com" \ - in client2.out + assert "Unable to connect to remote s1=http://asdlhaljksdhlajkshdljakhsd.com" in client2.out # s2 is not even tried assert "mylib/0.1@conan/testing: Trying with 's2'..." not in client2.out def test_install_from_remotes(self): + servers = {"remote%d" % i: TestServer() for i in range(3)} + client = TestClient(light=True, servers=servers, inputs=3 * ["admin", "password"]) for i in range(3): ref = RecipeReference.loads("hello%d/0.1@lasote/stable" % i) - self.client.save({"conanfile.py": GenConanfile("hello%d" % i, "0.1")}) - self.client.run("export . --user=lasote --channel=stable") - self.client.run("upload %s -r=remote%d" % (str(ref), i)) + client.save({"conanfile.py": GenConanfile("hello%d" % i, "0.1")}) + client.run("export . --user=lasote --channel=stable") + client.run("upload %s -r=remote%d" % (str(ref), i)) # Now install it in other machine from remote 0 - client2 = TestClient(servers=self.servers) + client2 = TestClient(light=True, servers=servers) refs = ["hello0/0.1@lasote/stable", "hello1/0.1@lasote/stable", "hello2/0.1@lasote/stable"] client2.save({"conanfile.py": GenConanfile("helloX", "0.1").with_requires(*refs)}) diff --git a/test/integration/remote/selected_remotes_test.py b/test/integration/remote/selected_remotes_test.py index 8191d0c44b5..7b7d6a84acf 100644 --- a/test/integration/remote/selected_remotes_test.py +++ b/test/integration/remote/selected_remotes_test.py @@ -1,5 +1,3 @@ -from collections import OrderedDict - import pytest from conan.test.assets.genconanfile import GenConanfile @@ -9,11 +7,11 @@ class TestSelectedRemotesInstall: @pytest.fixture(autouse=True) def _setup(self): - servers = OrderedDict() + servers = {} for index in range(3): servers[f"server{index}"] = TestServer([("*/*@*/*", "*")], [("*/*@*/*", "*")], users={"user": "password"}) - self.client = TestClient(servers=servers, inputs=3 * ["user", "password"]) + self.client = TestClient(light=True, servers=servers, inputs=3 * ["user", "password"]) def test_selected_remotes(self): self.client.save({"conanfile.py": GenConanfile("liba", "1.0").with_build_msg("OLDREV")}) diff --git a/test/integration/sbom/test_cyclonedx.py b/test/integration/sbom/test_cyclonedx.py index 220a3cbc8aa..4964d57fe0b 100644 --- a/test/integration/sbom/test_cyclonedx.py +++ b/test/integration/sbom/test_cyclonedx.py @@ -353,3 +353,81 @@ def test_sbom_deployer(self, cyclone_version, install): }.get(cyclone_version) tc.run(f"install {install} --deployer=cyclone_{method}") assert os.path.exists(os.path.join(tc.current_folder, f"sbom-cyclonedx-{method}.json")) + + +class TestCyclonedx2: + # Using the sbom tool with "conan create" + sbom_hook_post_package = textwrap.dedent(""" + import json + import os + from conan.errors import ConanException + from conan.api.output import ConanOutput + from conan.tools.sbom import cyclonedx_1_4, cyclonedx_1_6 + + def post_package(conanfile): + sbom_cyclonedx_1_4 = cyclonedx_1_4(conanfile, add_build=True, add_tests=True) + sbom_cyclonedx_1_6 = cyclonedx_1_6(conanfile, add_build=True, add_tests=True) + with open(os.path.join(conanfile.package_metadata_folder, "sbom14.cdx.json"), 'w') as f: + json.dump(sbom_cyclonedx_1_4, f, indent=4) + with open(os.path.join(conanfile.package_metadata_folder, "sbom16.cdx.json"), 'w') as f: + json.dump(sbom_cyclonedx_1_6, f, indent=4) + """) + + @pytest.fixture() + def sbom_hook_client(self): + tc = TestClient(light=True) + hook_path = os.path.join(tc.paths.hooks_path, "hook_sbom.py") + save(hook_path, self.sbom_hook_post_package) + return tc + + def test_sbom_generation_create(self, sbom_hook_client): + tc = sbom_hook_client + # bar -> engine/1.0 -> matrix/1.0 (same graph as cmake_lib + transitive_libraries, no CMake) + tc.save({ + "matrix/conanfile.py": GenConanfile("matrix", "1.0"), + "engine/conanfile.py": GenConanfile("engine", "1.0").with_requires("matrix/1.0"), + "conanfile.py": GenConanfile("bar", "1.0").with_requires("engine/1.0"), + }) + tc.run("create matrix") + tc.run("create engine") + tc.run("create . -tf=") + bar_layout = tc.created_layout() + assert os.path.exists(os.path.join(bar_layout.metadata(), "sbom14.cdx.json")) + assert os.path.exists(os.path.join(bar_layout.metadata(), "sbom16.cdx.json")) + + @pytest.mark.parametrize("user, channel, user_dep, channel_dep", + [("user", None, "user_dep", None), + ("user", "channel", "user_dep", "channel_dep")]) + def test_sbom_user_path(self, user, channel, user_dep, channel_dep): + tc = TestClient(light=True) + hook_path = os.path.join(tc.paths.hooks_path, "hook_sbom.py") + save(hook_path, self.sbom_hook_post_package) + channel_ref = f"/{channel_dep}" if channel_dep else "" + tc.save({"dep/conanfile.py": GenConanfile("dep", "1.0"), + "conanfile.py": GenConanfile("main", "1.0").with_requires( + f"dep/1.0@{user_dep}{channel_ref}")}) + command = "create dep" + if user: + command += f" --user={user_dep}" + if channel: + command += f" --channel={channel_dep}" + + tc.run(command) + + command = "create ." + if user: + command += f" --user={user}" + if channel: + command += f" --channel={channel}" + tc.run(command) + + for version in ("14", "16"): + create_layout = tc.created_layout() + cyclone_path = os.path.join(create_layout.metadata(), f"sbom{version}.cdx.json") + content = tc.load(cyclone_path) + content_json = json.loads(content) + + assert content_json["components"][0]["bom-ref"].split("&user=")[ + 1] == f"{user}&channel={channel}" if channel else user + assert content_json["dependencies"][0]["dependsOn"][0].split("&user=")[ + 1] == f"{user_dep}&channel={channel_dep}" if channel_dep else user_dep diff --git a/test/unittests/model/profile_test.py b/test/unittests/model/profile_test.py index 9ba034cfb29..994483a05ff 100644 --- a/test/unittests/model/profile_test.py +++ b/test/unittests/model/profile_test.py @@ -1,4 +1,4 @@ -from collections import OrderedDict +import textwrap from conan.tools.env.environment import ProfileEnvironment from conan.internal.model.profile import Profile @@ -8,38 +8,48 @@ class TestProfile: def test_profile_settings_update(self): new_profile = Profile() - new_profile.update_settings(OrderedDict([("os", "Windows")])) + new_profile.update_settings({"os": "Windows"}) - new_profile.update_settings(OrderedDict([("OTHER", "2")])) - assert new_profile.settings == OrderedDict([("os", "Windows"), ("OTHER", "2")]) + new_profile.update_settings({"OTHER": "2"}) + assert new_profile.settings == {"os": "Windows", "OTHER": "2"} - new_profile.update_settings(OrderedDict([("compiler", "2"), ("compiler.version", "3")])) - assert new_profile.settings == OrderedDict([("os", "Windows"), ("OTHER", "2"), - ("compiler", "2"), ("compiler.version", "3")]) + new_profile.update_settings({"compiler": "2", "compiler.version": "3"}) + assert new_profile.settings == { + "os": "Windows", + "OTHER": "2", + "compiler": "2", + "compiler.version": "3", + } def test_profile_subsettings_update(self): new_profile = Profile() - new_profile.update_settings(OrderedDict([("os", "Windows"), - ("compiler", "Visual Studio"), - ("compiler.runtime", "MT")])) + new_profile.update_settings({ + "os": "Windows", + "compiler": "Visual Studio", + "compiler.runtime": "MT", + }) - new_profile.update_settings(OrderedDict([("compiler", "gcc")])) + new_profile.update_settings({"compiler": "gcc"}) assert dict(new_profile.settings) == {"compiler": "gcc", "os": "Windows"} new_profile = Profile() - new_profile.update_settings(OrderedDict([("os", "Windows"), - ("compiler", "Visual Studio"), - ("compiler.runtime", "MT")])) - - new_profile.update_settings(OrderedDict([("compiler", "Visual Studio"), - ("compiler.subsetting", "3"), - ("other", "value")])) + new_profile.update_settings({ + "os": "Windows", + "compiler": "Visual Studio", + "compiler.runtime": "MT", + }) + + new_profile.update_settings({ + "compiler": "Visual Studio", + "compiler.subsetting": "3", + "other": "value", + }) assert dict(new_profile.settings) == {"compiler": "Visual Studio", - "os": "Windows", - "compiler.runtime": "MT", - "compiler.subsetting": "3", - "other": "value"} + "os": "Windows", + "compiler.runtime": "MT", + "compiler.subsetting": "3", + "other": "value"} def test_package_settings_update(self): np = Profile() @@ -51,7 +61,8 @@ def test_package_settings_update(self): np._package_settings_values = None # invalidate caching np.update_package_settings({"MyPackage": [("compiler", "2"), ("compiler.version", "3")]}) assert np.package_settings_values == {"MyPackage": [("os", "Windows"), ("OTHER", "2"), - ("compiler", "2"), ("compiler.version", "3")]} + ("compiler", "2"), + ("compiler.version", "3")]} def test_profile_dump_order(self): # Settings @@ -61,17 +72,17 @@ def test_profile_dump_order(self): profile.settings["compiler"] = "Visual Studio" profile.settings["compiler.version"] = "12" profile.tool_requires["*"] = ["zlib/1.2.8@lasote/testing"] - profile.tool_requires["zlib/*"] = ["aaaa/1.2.3@lasote/testing", - "bb/1.2@lasote/testing"] - assert """[settings] -arch=x86_64 -compiler=Visual Studio -compiler.version=12 -zlib:compiler=gcc -[tool_requires] -*: zlib/1.2.8@lasote/testing -zlib/*: aaaa/1.2.3@lasote/testing, bb/1.2@lasote/testing -""".splitlines() == profile.dumps().splitlines() + profile.tool_requires["zlib/*"] = ["aaaa/1.2.3@lasote/testing", "bb/1.2@lasote/testing"] + assert textwrap.dedent("""\ + [settings] + arch=x86_64 + compiler=Visual Studio + compiler.version=12 + zlib:compiler=gcc + [tool_requires] + *: zlib/1.2.8@lasote/testing + zlib/*: aaaa/1.2.3@lasote/testing, bb/1.2@lasote/testing + """) == profile.dumps() def test_apply(self): # Settings @@ -80,9 +91,10 @@ def test_apply(self): profile.settings["compiler"] = "Visual Studio" profile.settings["compiler.version"] = "12" - profile.update_settings(OrderedDict([("compiler.version", "14")])) + profile.update_settings({"compiler.version": "14"}) - assert '[settings]\narch=x86_64\ncompiler=Visual Studio\ncompiler.version=14\n' == profile.dumps() + assert ('[settings]\narch=x86_64\n' + 'compiler=Visual Studio\ncompiler.version=14\n') == profile.dumps() def test_update_build_requires(): From f7853ebf3d6f5ca6ca0b6ff62c78e96e22b9edea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= <5364255+AbrilRBS@users.noreply.github.com> Date: Sat, 28 Mar 2026 00:16:29 +0100 Subject: [PATCH 073/110] Fix syntax error in Python <3.9 (#19805) --- test/integration/command/remote/test_remote_users.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/integration/command/remote/test_remote_users.py b/test/integration/command/remote/test_remote_users.py index 3c67a679151..0503097a4a1 100644 --- a/test/integration/command/remote/test_remote_users.py +++ b/test/integration/command/remote/test_remote_users.py @@ -362,8 +362,8 @@ def test_remote_auth_force(self): c = TestClient(light=True, servers=servers, inputs=["lasote", "mypass", "danimtb", "passpass", "lasote", "mypass"]) - with (patch("conan.internal.rest.rest_client_v2.RestV2Methods.check_credentials") - as check_credentials_mock): + with patch("conan.internal.rest.rest_client_v2.RestV2Methods.check_credentials") \ + as check_credentials_mock: c.run("remote auth --force *") check_credentials_mock.assert_called_with(True) @@ -375,8 +375,8 @@ def test_remote_auth_force_false(self): c = TestClient(light=True, servers=servers, inputs=["lasote", "mypass", "danimtb", "passpass", "lasote", "mypass"]) - with (patch("conan.internal.rest.rest_client_v2.RestV2Methods.check_credentials") - as check_credentials_mock): + with patch("conan.internal.rest.rest_client_v2.RestV2Methods.check_credentials") \ + as check_credentials_mock: c.run("remote auth *") check_credentials_mock.assert_called_with(False) From cb806e4858d69eba8716494c8c74789f46963818 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 30 Mar 2026 15:24:49 +0200 Subject: [PATCH 074/110] avoid casing errors while looking for version ranges (#19799) --- conan/internal/cache/cache.py | 4 ++-- test/integration/conanfile/conanfile_errors_test.py | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/conan/internal/cache/cache.py b/conan/internal/cache/cache.py index c61e6146837..0281702179e 100644 --- a/conan/internal/cache/cache.py +++ b/conan/internal/cache/cache.py @@ -201,13 +201,13 @@ def update_recipe_timestamp(self, ref: RecipeReference): assert ref.timestamp self._db.update_recipe_timestamp(ref) - def search_recipes(self, pattern=None, ignorecase=True): + def search_recipes(self, pattern=None): # Conan references in main storage if pattern: if isinstance(pattern, RecipeReference): pattern = repr(pattern) pattern = translate(pattern) - pattern = re.compile(pattern, re.IGNORECASE if ignorecase else 0) + pattern = re.compile(pattern) return self._db.list_references(pattern) diff --git a/test/integration/conanfile/conanfile_errors_test.py b/test/integration/conanfile/conanfile_errors_test.py index 3fc05e9250c..4ca09a6d52a 100644 --- a/test/integration/conanfile/conanfile_errors_test.py +++ b/test/integration/conanfile/conanfile_errors_test.py @@ -137,6 +137,16 @@ class HelloConan(ConanFile): client.run("export .", assert_error=True) assert "Duplicated requirement" in client.out + @pytest.mark.parametrize("version", ["0.1", "[*]"]) + def test_error_requirement_casing(self, version): + # https://github.com/conan-io/conan/issues/19796 + c = TestClient(light=True) + c.save({"mypkg/conanfile.py": GenConanfile("mypkg", "0.1"), + "app/conanfile.py": GenConanfile("app", "0.1").with_requires(f"myPkg/{version}")}) + c.run("create mypkg") + c.run("install app", assert_error=True) + assert f"ERROR: Package 'myPkg/{version}' not resolved" in c.out + class TestWrongMethods: # https://github.com/conan-io/conan/issues/12961 From 27cb3fe8298e79a3d22e5728090a64d07500e57d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= <5364255+AbrilRBS@users.noreply.github.com> Date: Mon, 6 Apr 2026 15:18:00 +0200 Subject: [PATCH 075/110] Undefined locale for list html (#19828) --- conan/cli/formatters/list/search_table_html.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conan/cli/formatters/list/search_table_html.py b/conan/cli/formatters/list/search_table_html.py index 0fa834621a2..50ac39cc48c 100644 --- a/conan/cli/formatters/list/search_table_html.py +++ b/conan/cli/formatters/list/search_table_html.py @@ -50,7 +50,7 @@ second: "2-digit", hour12: false }; - return new Date(timeStamp * 1000).toLocaleDateString('en', options); + return new Date(timeStamp * 1000).toLocaleDateString(undefined, options); } function getInfoFieldsBadges(info) { From dc2cefb2255d25177e3d4b1e8af2916bdf79f924 Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Mon, 6 Apr 2026 18:44:29 +0200 Subject: [PATCH 076/110] finalize() output folder should be printed only once (#19834) --- conan/internal/graph/installer.py | 2 +- .../conanfile/test_finalize_method.py | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/conan/internal/graph/installer.py b/conan/internal/graph/installer.py index 384519d4449..b5a75d38280 100644 --- a/conan/internal/graph/installer.py +++ b/conan/internal/graph/installer.py @@ -487,4 +487,4 @@ def _call_finalize_method(conanfile, finalize_folder): 'finalize'): conanfile.finalize() - conanfile.output.success(f"Finalized folder {finalize_folder}") + conanfile.output.success(f"Finalized folder {finalize_folder}") diff --git a/test/integration/conanfile/test_finalize_method.py b/test/integration/conanfile/test_finalize_method.py index 45162fd7bb3..568c9d03af7 100644 --- a/test/integration/conanfile/test_finalize_method.py +++ b/test/integration/conanfile/test_finalize_method.py @@ -319,6 +319,27 @@ def test_test_package_uses_created_tool_which_modifies_pkgfolder(self): assert "There are corrupted artifacts" not in tc.out + def test_multiple_instances_of_finalized_package(self): + tc = TestClient(light=True) + tc.save({"tool/conanfile.py": GenConanfile("tool", "1.0") + .with_package_type("application") + .with_finalize("self.output.info('RUNNING MY FINALIZE')"), + "liba/conanfile.py": GenConanfile("liba", "1.0").with_tool_requires("tool/1.0"), + "libb/conanfile.py": GenConanfile("libb", "1.0").with_tool_requires("tool/1.0"), + "consumer/conanfile.py": GenConanfile("consummer", "1.0") + .with_requires("liba/1.0") + .with_requires("libb/1.0")}) + + tc.run("export tool") + tc.run("export liba") + tc.run("export libb") + tc.run("install consumer -c:a tools.graph:skip_binaries=False -b missing") + + # The finalize() method of the tool should only be called once, even if multiple packages depend on it + assert tc.out.count("RUNNING MY FINALIZE") == 1 + # Finalize folder conan output should be printed only once + assert tc.out.count("Finalized folder ") == 1 + class TestRemoteFlows: @pytest.fixture From ecb8c39d5e0ed6b3b39d76f0140e80c5c072cd71 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 7 Apr 2026 10:41:17 +0200 Subject: [PATCH 077/110] Refactor/ordered dict (#19839) * wip * more rid * wip --- conan/cli/formatters/graph/graph_info_text.py | 3 +-- conan/internal/graph/compatibility.py | 2 +- conan/internal/model/conf.py | 23 ++++--------------- conan/internal/model/lockfile.py | 13 +++++------ conan/tools/apple/xcodedeps.py | 7 +++--- conan/tools/cmake/toolchain/blocks.py | 11 ++++----- .../lockfile/test_lock_packages.py | 7 ++++++ 7 files changed, 27 insertions(+), 39 deletions(-) diff --git a/conan/cli/formatters/graph/graph_info_text.py b/conan/cli/formatters/graph/graph_info_text.py index ac04a056e60..319fe1be509 100644 --- a/conan/cli/formatters/graph/graph_info_text.py +++ b/conan/cli/formatters/graph/graph_info_text.py @@ -1,5 +1,4 @@ import fnmatch -from collections import OrderedDict from conan.api.model import RecipeReference from conan.api.output import ConanOutput, cli_out_write @@ -29,7 +28,7 @@ def _matching(node, pattern): field_filter.append("ref") result = {} for id_, n in graph["nodes"].items(): - new_node = OrderedDict((k, v) for k, v in n.items() if k in field_filter) + new_node = {k: v for k, v in n.items() if k in field_filter} result[id_] = new_node graph["nodes"] = result return graph diff --git a/conan/internal/graph/compatibility.py b/conan/internal/graph/compatibility.py index 2de231caa46..0300f434ffa 100644 --- a/conan/internal/graph/compatibility.py +++ b/conan/internal/graph/compatibility.py @@ -135,7 +135,7 @@ def compatibles(self, conanfile): if not compat_infos: return {} - result = OrderedDict() + result = {} original_info = conanfile.info original_settings = conanfile.settings original_settings_target = conanfile.settings_target diff --git a/conan/internal/model/conf.py b/conan/internal/model/conf.py index 84a41389f4f..459e7457a98 100644 --- a/conan/internal/model/conf.py +++ b/conan/internal/model/conf.py @@ -7,8 +7,6 @@ import fnmatch import textwrap -from collections import OrderedDict - from jinja2 import Environment, FileSystemLoader from conan.errors import ConanException @@ -345,20 +343,11 @@ class Conf: def __init__(self): # It being ordered allows for Windows case-insensitive composition - self._values = OrderedDict() # {var_name: [] of values, including separators} + self._values = {} # {var_name: [] of values, including separators} def __bool__(self): return bool(self._values) - def __repr__(self): - return "Conf: " + repr(self._values) - - def __eq__(self, other): - """ - :type other: Conf - """ - return other._values == self._values - def clear(self): self._values.clear() @@ -429,13 +418,12 @@ def show(self, fnpattern, pattern=""): def copy(self): c = Conf() - c._values = OrderedDict((k, v.copy()) for k, v in self._values.items()) + c._values = {k: v.copy() for k, v in self._values.items()} return c def filter_core(self): c = Conf() - c._values = OrderedDict((k, v.copy()) for k, v in self._values.items() - if not CORE_CONF_PATTERN.match(k)) + c._values = {k: v.copy() for k, v in self._values.items() if not CORE_CONF_PATTERN.match(k)} return c def dumps(self): @@ -600,10 +588,7 @@ class ConfDefinition: ("=!", "unset"), ("*=", "update"), ("=", "define")) def __init__(self): - self._pattern_confs = OrderedDict() - - def __repr__(self): - return "ConfDefinition: " + repr(self._pattern_confs) + self._pattern_confs = {} def __bool__(self): return bool(self._pattern_confs) diff --git a/conan/internal/model/lockfile.py b/conan/internal/model/lockfile.py index f6811344af8..d93a5f7436e 100644 --- a/conan/internal/model/lockfile.py +++ b/conan/internal/model/lockfile.py @@ -1,7 +1,6 @@ import fnmatch import json import os -from collections import OrderedDict from conan.api.output import ConanOutput from conan.internal.graph.graph import RECIPE_VIRTUAL, RECIPE_CONSUMER, CONTEXT_BUILD, Overrides @@ -21,7 +20,7 @@ class _LockRequires: otherwise it could be a bare list """ def __init__(self): - self._requires = OrderedDict() # {require: package_ids} + self._requires = {} # {require: package_ids} def refs(self): return self._requires.keys() @@ -53,9 +52,9 @@ def add(self, ref, package_ids=None): old_package_ids = self._requires.pop(ref, None) # Get existing one if old_package_ids is not None: if package_ids is not None: - package_ids = old_package_ids.update(package_ids) - else: - package_ids = old_package_ids + assert isinstance(old_package_ids, dict) + old_package_ids.update(package_ids) + package_ids = old_package_ids self._requires[ref] = package_ids else: # Manual addition of something without revision existing = {r: r for r in self._requires}.get(ref) @@ -77,7 +76,7 @@ def remove(self, pattern): remove.append(k) else: remove = [k for k in self._requires if k.matches(pattern, False)] - self._requires = OrderedDict((k, v) for k, v in self._requires.items() if k not in remove) + self._requires = {k: v for k, v in self._requires.items() if k not in remove} return remove def update(self, refs, name): @@ -96,7 +95,7 @@ def update(self, refs, name): self.sort() def sort(self): - self._requires = OrderedDict(reversed(sorted(self._requires.items()))) + self._requires = dict(reversed(sorted(self._requires.items()))) def merge(self, other): """ diff --git a/conan/tools/apple/xcodedeps.py b/conan/tools/apple/xcodedeps.py index f78ef8878f6..9cdad6c3b19 100644 --- a/conan/tools/apple/xcodedeps.py +++ b/conan/tools/apple/xcodedeps.py @@ -1,7 +1,6 @@ import os import re import textwrap -from collections import OrderedDict from jinja2 import Template @@ -130,7 +129,7 @@ def _conf_xconfig_file(self, require, pkg_name, comp_name, package_folder, trans """ def _merged_vars(name): merged = [var for cpp_info in transitive_cpp_infos for var in getattr(cpp_info, name)] - return list(OrderedDict.fromkeys(merged).keys()) + return list(dict.fromkeys(merged).keys()) # TODO: Investigate if paths can be made relative to "root" folder fields = { @@ -292,8 +291,8 @@ def _transitive_components(component): _transitive_components(comp_cpp_info) # remove duplicates - transitive_internal = list(OrderedDict.fromkeys(transitive_internal).keys()) - transitive_external = list(OrderedDict.fromkeys(transitive_external).keys()) + transitive_internal = list(dict.fromkeys(transitive_internal).keys()) + transitive_external = list(dict.fromkeys(transitive_external).keys()) # In case dep is editable and package_folder=None pkg_folder = dep.package_folder or dep.recipe_folder diff --git a/conan/tools/cmake/toolchain/blocks.py b/conan/tools/cmake/toolchain/blocks.py index 00e06db1746..4b62064220f 100644 --- a/conan/tools/cmake/toolchain/blocks.py +++ b/conan/tools/cmake/toolchain/blocks.py @@ -1,7 +1,6 @@ import os import re import textwrap -from collections import OrderedDict from jinja2 import Template @@ -13,7 +12,7 @@ from conan.tools.build.flags import architecture_flag, architecture_link_flag, libcxx_flags, threads_flags from conan.tools.build.cross_building import cross_building from conan.tools.cmake.toolchain import CONAN_TOOLCHAIN_FILENAME -from conan.tools.cmake.utils import is_multi_configuration, cmake_escape_value +from conan.tools.cmake.utils import is_multi_configuration from conan.tools.intel import IntelCC from conan.tools.microsoft.visual import msvc_version_to_toolset_version, msvc_platform_from_arch from conan.internal.api.install.generators import relativize_path @@ -1390,7 +1389,7 @@ def context(self): class ToolchainBlocks: def __init__(self, conanfile, toolchain, items=None): - self._blocks = OrderedDict() + self._blocks = {} self._conanfile = conanfile self._toolchain = toolchain if items: @@ -1416,14 +1415,14 @@ def select(self, name, *args): self._conanfile.output.warning("CMakeToolchain.select is deprecated. Use blocks.enabled()" " instead", warn_tag="deprecated") to_keep = [name] + list(args) + ["variables", "preprocessor"] - self._blocks = OrderedDict((k, v) for k, v in self._blocks.items() if k in to_keep) + self._blocks = {k: v for k, v in self._blocks.items() if k in to_keep} def enabled(self, name, *args): """ keep the blocks provided as arguments, remove the others """ to_keep = [name] + list(args) - self._blocks = OrderedDict((k, v) for k, v in self._blocks.items() if k in to_keep) + self._blocks = {k: v for k, v in self._blocks.items() if k in to_keep} def __setitem__(self, name, block_type): # Create a new class inheriting Block with the elements of the provided one @@ -1438,7 +1437,7 @@ def process_blocks(self): check_type=list) if blocks is not None: try: - new_blocks = OrderedDict((b, self._blocks[b]) for b in blocks) + new_blocks = {b: self._blocks[b] for b in blocks} except KeyError as e: raise ConanException(f"Block {e} defined in tools.cmake.cmaketoolchain" f":enabled_blocks doesn't exist in {list(self._blocks.keys())}") diff --git a/test/integration/lockfile/test_lock_packages.py b/test/integration/lockfile/test_lock_packages.py index 5b1d3acbcdd..a648d78b75e 100644 --- a/test/integration/lockfile/test_lock_packages.py +++ b/test/integration/lockfile/test_lock_packages.py @@ -27,6 +27,13 @@ def test_lock_packages(requires): lock = client.load("consumer/conan.lock") assert NO_SETTINGS_PACKAGE_ID in lock + # Just repeat + client.run("lock create consumer/conanfile.txt --lockfile-packages") + assert "ERROR: The --lockfile-packages arg is private and shouldn't be used" in client.out + assert "pkg/0.1#" in client.out + lock = client.load("consumer/conan.lock") + assert NO_SETTINGS_PACKAGE_ID in lock + with environment_update({"MYVAR": "MYVALUE2"}): client.run("create pkg --name=pkg --version=0.1") prev2 = client.created_package_revision("pkg/0.1") From 336b58004029acbc2e910b8ff08555b99c415eff Mon Sep 17 00:00:00 2001 From: James Date: Tue, 7 Apr 2026 10:46:32 +0200 Subject: [PATCH 078/110] fix powershell unset (and other possible unsets too) (#19820) --- conan/internal/model/conf.py | 8 +++++--- .../toolchains/env/test_virtualenv_powershell.py | 12 ++++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/conan/internal/model/conf.py b/conan/internal/model/conf.py index 459e7457a98..956e5cb278f 100644 --- a/conan/internal/model/conf.py +++ b/conan/internal/model/conf.py @@ -376,7 +376,9 @@ def get(self, conf_name, default=None, check_type=None, choices=None): conf_value = self._values.get(conf_name) if conf_value: v = conf_value.value - if choices is not None and v not in choices and v is not None: + if v is None: # value was unset + return default + if choices is not None and v not in choices: raise ConanException(f"Unknown value '{v}' for '{conf_name}'") # Some smart conversions if check_type is bool and not isinstance(v, bool): @@ -387,9 +389,9 @@ def get(self, conf_name, default=None, check_type=None, choices=None): raise ConanException(f"[conf] {conf_name} must be a boolean-like object " f"(true/false, 1/0, on/off) and value '{v}' does not match it.") elif check_type is str and not isinstance(v, str): + # TODO: this would be converting things like lists to strings without + # proper error, is it worth trying to change it? return str(v) - elif v is None: # value was unset - return default elif (check_type is not None and not isinstance(v, check_type) or check_type is int and isinstance(v, bool)): raise ConanException(f"[conf] {conf_name} must be a " diff --git a/test/functional/toolchains/env/test_virtualenv_powershell.py b/test/functional/toolchains/env/test_virtualenv_powershell.py index 15d192a53ee..3041c3a6fa9 100644 --- a/test/functional/toolchains/env/test_virtualenv_powershell.py +++ b/test/functional/toolchains/env/test_virtualenv_powershell.py @@ -260,3 +260,15 @@ def test_verbosity_flag(): assert "/verbosity:Detailed" in tc.out assert "-verbosity:Detailed" not in tc.out + + +def test_powershell_deactivation(): + # https://github.com/conan-io/conan/issues/19819 + c = TestClient(light=True) + c.save({"conanfile.txt": ""}) + c.run('install . -c tools.env.virtualenv:powershell=!') + files = os.listdir(c.current_folder) + assert "conanrun.ps1" not in files + assert "conanbuild.ps1" not in files + assert "conanrunenv.ps1" not in files + assert "conanbuildenv.ps1" not in files From 57f643d5a8086eb69a75e844211e4d0a346275fb Mon Sep 17 00:00:00 2001 From: James Date: Tue, 7 Apr 2026 13:08:59 +0200 Subject: [PATCH 079/110] doc retry conf and change default (#19830) * doc retry conf and change default * fix tests --- conan/internal/model/conf.py | 12 ++++++------ conan/internal/rest/rest_client_v2.py | 2 +- test/integration/remote/broken_download_test.py | 6 +++--- test/integration/remote/download_retries_test.py | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/conan/internal/model/conf.py b/conan/internal/model/conf.py index 956e5cb278f..620bd4b0f67 100644 --- a/conan/internal/model/conf.py +++ b/conan/internal/model/conf.py @@ -28,12 +28,12 @@ "core:default_build_profile": "Defines the default build profile ('default' 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.upload:retry": "(int, default: 1) Number of retries in case of failure when uploading to Conan server", + "core.upload:retry_wait": "(int, default: 5s) Seconds to wait between upload attempts to Conan server", "core.upload:parallel": "Number of concurrent threads to upload packages", "core.download:parallel": "Number of concurrent threads to download packages", - "core.download:retry": "Number of retries in case of failure when downloading from Conan server", - "core.download:retry_wait": "Seconds to wait between download attempts from Conan server", + "core.download:retry": " (int, default: 2) Number of retries in case of failure when downloading from Conan server", + "core.download:retry_wait": "(int, default: 1s) Seconds to wait between download attempts from Conan server", "core.download:download_cache": "Define path to a file download cache", "core.cache:storage_path": "Absolute path where the packages and database are stored", "core:update_policy": "(Legacy). If equal 'legacy' when multiple remotes, update based on order of remotes, only the timestamp of the first occurrence of each revision counts.", @@ -102,8 +102,8 @@ "tools.cmake:configure_args": "Add extra arguments to CMake.configure() command line ", "tools.cmake:install_strip": "(Deprecated) Add --strip to cmake.install(). Use tools.build:install_strip instead", "tools.deployer:symlinks": "Set to False to disable deployers copying symlinks", - "tools.files.download:retry": "Number of retries in case of failure when downloading", - "tools.files.download:retry_wait": "Seconds to wait between download attempts", + "tools.files.download:retry": "(int, default: 2) Number of retries in case of failure when downloading", + "tools.files.download:retry_wait": "(int, default: 5s) Seconds to wait between download attempts", "tools.files.download:verify": "If set, overrides recipes on whether to perform SSL verification for their downloaded files. Only recommended to be set while testing", "tools.files.unzip:filter": "Define tar extraction filter: 'fully_trusted', 'tar', 'data'", "tools.graph:vendor": "(Experimental) If 'build', enables the computation of dependencies of vendoring packages to build them", diff --git a/conan/internal/rest/rest_client_v2.py b/conan/internal/rest/rest_client_v2.py index aabd00536d1..9033e6d9189 100644 --- a/conan/internal/rest/rest_client_v2.py +++ b/conan/internal/rest/rest_client_v2.py @@ -309,7 +309,7 @@ def _download_and_save_files(self, urls, dest_folder, files, parallel=False, sco # Take advantage of filenames ordering, so that conan_package.tgz and conan_export.tgz # can be < conanfile, conaninfo, and sent always the last, so smaller files go first retry = self._config.get("core.download:retry", check_type=int, default=2) - retry_wait = self._config.get("core.download:retry_wait", check_type=int, default=0) + retry_wait = self._config.get("core.download:retry_wait", check_type=int, default=1) downloader = ConanInternalCacheDownloader(self.requester, self._config, scope=scope) threads = [] diff --git a/test/integration/remote/broken_download_test.py b/test/integration/remote/broken_download_test.py index fff7898910e..44aac5592c0 100644 --- a/test/integration/remote/broken_download_test.py +++ b/test/integration/remote/broken_download_test.py @@ -114,13 +114,13 @@ def DownloadFilesBrokenRequesterTimesOne(*args, **kwargs): # noqa client.run("install --requires=lib/1.0@lasote/stable") assert "WARN: network: Error downloading file" in client.out assert 'Fake connection error exception' in client.out - assert 1 == str(client.out).count("Waiting 0 seconds to retry...") + assert 1 == str(client.out).count("Waiting 1 seconds to retry...") client = TestClient(servers=servers, inputs=["admin", "password"], requester_class=DownloadFilesBrokenRequesterTimesOne) - client.save_home({"global.conf": "core.download:retry_wait=1"}) + client.save_home({"global.conf": "core.download:retry_wait=2"}) client.run("install --requires=lib/1.0@lasote/stable") - assert 1 == str(client.out).count("Waiting 1 seconds to retry...") + assert 1 == str(client.out).count("Waiting 2 seconds to retry...") def DownloadFilesBrokenRequesterTimesTen(*args, **kwargs): # noqa return DownloadFilesBrokenRequester(10, *args, **kwargs) diff --git a/test/integration/remote/download_retries_test.py b/test/integration/remote/download_retries_test.py index bff70264dff..7772978376e 100644 --- a/test/integration/remote/download_retries_test.py +++ b/test/integration/remote/download_retries_test.py @@ -29,5 +29,5 @@ def get(self, *args, **kwargs): client = TestClient(servers={"default": test_server}, inputs=["admin", "password"], requester_class=BuggyRequester) client.run("install --requires=pkg/0.1@lasote/stable", assert_error=True) - assert str(client.out).count("Waiting 0 seconds to retry...") == 2 + assert str(client.out).count("Waiting 1 seconds to retry...") == 2 assert str(client.out).count("Error 200 downloading") == 3 From c6fd6918e5f9c74f2ea8c14df17d29b035fb6b87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= <5364255+AbrilRBS@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:41:27 +0200 Subject: [PATCH 080/110] Documentation improvement for items() iteration of packages list (#19829) * Improve items() * Improve items documentation * Fix docstring --- conan/api/model/list.py | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/conan/api/model/list.py b/conan/api/model/list.py index ea6495d4c28..63572209e0f 100644 --- a/conan/api/model/list.py +++ b/conan/api/model/list.py @@ -312,11 +312,32 @@ def refs(self): return result def items(self) -> Iterable[Tuple[RecipeReference, Dict[PkgReference, Dict]]]: - """ Iterate the contents of the package list. + """Iterate over the contents of the package list. - The first dictionary is the information directly belonging to the recipe-revision. - The second dictionary contains PkgReference as keys, and a dictionary with the values - belonging to that specific package reference (settings, options, etc.). + Yields tuples containing a recipe reference and a dictionary of its + associated package references content. + + Returns: + An iterable of tuples where: + + - The first element is a ``RecipeReference`` (representing the recipe revision). + - The second element is a dictionary mapping a ``PkgReference`` to a nested dictionary of + its specific attributes (e.g., settings, options). + + Warning: + **Missing Revisions Behavior:** + This method filters out results that lack revision information. + + - It will ONLY yield items if they contain at least a **recipe revision**. + - The nested package dictionary will be empty unless it contains a **package revision**. + + **When to use serialize instead:** + If you perform a general search that does not fetch revisions (e.g., running + ``conan list *``), this method will yield nothing because no artifact references + are created. In these cases, use the ``serialize()`` method to access the results. + + To successfully use ``items()``, your query must explicitly request revisions + (e.g., running ``conan list pkg/version#*:*#*``). """ for ref, ref_dict in self._data.items(): for rrev, rrev_dict in ref_dict.get("revisions", {}).items(): From 54720f385124a7a1d4de1ad231ed0fc314ff40a0 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 7 Apr 2026 17:51:00 +0200 Subject: [PATCH 081/110] Poc/editable refactor (#19835) * wip * wip * wip --- conan/api/subapi/install.py | 13 +++---------- conan/internal/api/local/editable.py | 5 ----- conan/internal/cache/conan_reference_layout.py | 5 +++++ conan/internal/graph/graph.py | 1 + conan/internal/graph/graph_builder.py | 2 ++ conan/internal/graph/installer.py | 10 +++------- conan/internal/graph/proxy.py | 8 +++++--- 7 files changed, 19 insertions(+), 25 deletions(-) diff --git a/conan/api/subapi/install.py b/conan/api/subapi/install.py index 806ec3f9bb5..f53abb27f6f 100644 --- a/conan/api/subapi/install.py +++ b/conan/api/subapi/install.py @@ -3,7 +3,6 @@ from conan.api.model import Remote from conan.internal.api.install.generators import write_generators -from conan.internal.conan_app import ConanBasicApp from conan.internal.deploy import do_deploys from conan.internal.graph.install_graph import InstallGraph @@ -39,9 +38,7 @@ def install_binaries(self, deps_graph, remotes: List[Remote] = None, return_inst :param remotes: List of remotes to fetch packages from if necessary. :param return_install_error: If ``True``, do not raise an exception, but return it """ - app = ConanBasicApp(self._conan_api) - installer = BinaryInstaller(self._conan_api, self._helpers.global_conf, app.editable_packages, - self._helpers.hook_manager) + installer = BinaryInstaller(self._conan_api, self._helpers.global_conf, self._helpers.hook_manager) install_graph = InstallGraph(deps_graph) install_graph.raise_errors() install_order = install_graph.install_order() @@ -68,9 +65,7 @@ def install_system_requires(self, graph, only_info=False): :param graph: Dependency graph to install system requirements for :param only_info: If ``True``, only reporting and checking of whether the system requirements are installed is performed. """ - app = ConanBasicApp(self._conan_api) - installer = BinaryInstaller(self._conan_api, self._helpers.global_conf, app.editable_packages, - self._helpers.hook_manager) + installer = BinaryInstaller(self._conan_api, self._helpers.global_conf, self._helpers.hook_manager) installer.install_system_requires(graph, only_info) def install_sources(self, graph, remotes: List[Remote]): @@ -89,9 +84,7 @@ def install_sources(self, graph, remotes: List[Remote]): :param remotes: List of remotes where the ``exports_sources`` of the packages might be located :param graph: Dependency graph to download sources from """ - app = ConanBasicApp(self._conan_api) - installer = BinaryInstaller(self._conan_api, self._helpers.global_conf, app.editable_packages, - self._helpers.hook_manager) + installer = BinaryInstaller(self._conan_api, self._helpers.global_conf, self._helpers.hook_manager) installer.install_sources(graph, remotes) def install_consumer(self, deps_graph, generators: List[str] = None, source_folder=None, diff --git a/conan/internal/api/local/editable.py b/conan/internal/api/local/editable.py index 1d0608af949..6a64dda4119 100644 --- a/conan/internal/api/local/editable.py +++ b/conan/internal/api/local/editable.py @@ -49,11 +49,6 @@ def get(self, ref): _tmp.revision = None return self._edited_refs.get(_tmp) - def get_path(self, ref): - editable = self.get(ref) - if editable is not None: - return editable["path"] - def add(self, ref, path, output_folder=None): assert isinstance(ref, RecipeReference) _tmp = copy.copy(ref) diff --git a/conan/internal/cache/conan_reference_layout.py b/conan/internal/cache/conan_reference_layout.py index b9522ca8975..37ffe9b9dca 100644 --- a/conan/internal/cache/conan_reference_layout.py +++ b/conan/internal/cache/conan_reference_layout.py @@ -32,6 +32,11 @@ def remove(self): class BasicLayout(LayoutBase): # For editables and platform_requires + + def __init__(self, ref, base_folder, editable_output_folder=None): + super().__init__(ref, base_folder) + self.editable_output_folder = editable_output_folder + def conanfile(self): # the full conanfile path (including other other.py names) for editables # None for platform_requires diff --git a/conan/internal/graph/graph.py b/conan/internal/graph/graph.py index 924b7e2248b..9af88d7b07a 100644 --- a/conan/internal/graph/graph.py +++ b/conan/internal/graph/graph.py @@ -71,6 +71,7 @@ def __init__(self, ref, conanfile, context, recipe=None, path=None, test=False): self.is_conf = False self.replaced_requires = {} # To track the replaced requires for self.edges[old-ref] self.skipped_build_requires = False + self.editable_output_folder = None # In case this node is editable @property def dependencies(self): diff --git a/conan/internal/graph/graph_builder.py b/conan/internal/graph/graph_builder.py index 9be4e23901a..73cf3d65a7a 100644 --- a/conan/internal/graph/graph_builder.py +++ b/conan/internal/graph/graph_builder.py @@ -418,6 +418,8 @@ def _create_new_node(self, node, require, graph, profile_host, profile_build, gr new_node = Node(new_ref, dep_conanfile, context=context, test=require.test or node.test) new_node.recipe = recipe_status new_node.remote = remote + if isinstance(layout, BasicLayout): # Store the editable_output_folder for BinaryInstaller + new_node.editable_output_folder = layout.editable_output_folder down_options = self._compute_down_options(node, require, new_ref) diff --git a/conan/internal/graph/installer.py b/conan/internal/graph/installer.py index b5a75d38280..5293ac0ddc5 100644 --- a/conan/internal/graph/installer.py +++ b/conan/internal/graph/installer.py @@ -165,9 +165,8 @@ class BinaryInstaller: locally in case they are not found in remotes """ - def __init__(self, api, global_conf, editable_packages, hook_manager): + def __init__(self, api, global_conf, hook_manager): helpers = api._api_helpers # noqa - self._editable_packages = editable_packages self._cache = helpers.cache self._remote_manager = helpers.remote_manager self._hook_manager = hook_manager @@ -351,12 +350,9 @@ def _handle_node_editable(self, install_node): # It will only run generation node = install_node.nodes[0] conanfile = node.conanfile - ref = node.ref - editable = self._editable_packages.get(ref) - conanfile_path = editable["path"] - output_folder = editable.get("output_folder") - base_path = os.path.dirname(conanfile_path) + output_folder = node.editable_output_folder + base_path = conanfile.recipe_folder conanfile.folders.set_base_folders(base_path, output_folder) diff --git a/conan/internal/graph/proxy.py b/conan/internal/graph/proxy.py index 2fa1fcb3b73..910970932b3 100644 --- a/conan/internal/graph/proxy.py +++ b/conan/internal/graph/proxy.py @@ -32,9 +32,11 @@ def get_recipe(self, ref, remotes, update, check_update): def _get_recipe(self, reference, remotes, update, check_update): output = ConanOutput(scope=str(reference)) - conanfile_path = self._editable_packages.get_path(reference) - if conanfile_path is not None: - return BasicLayout(reference, conanfile_path), RECIPE_EDITABLE, None + editable = self._editable_packages.get(reference) + if editable is not None: + path = editable["path"] + output_folder = editable.get("output_folder") + return BasicLayout(reference, path, output_folder), RECIPE_EDITABLE, None # check if it there's any revision of this recipe in the local cache try: From 683e0753697f62f9912cd2081d67598aacc4897d Mon Sep 17 00:00:00 2001 From: James Date: Wed, 8 Apr 2026 09:37:53 +0200 Subject: [PATCH 082/110] testing msvc minor update via conf (#19843) --- conan/internal/model/conan_file.py | 1 + test/unittests/tools/cmake/test_cmaketoolchain.py | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/conan/internal/model/conan_file.py b/conan/internal/model/conan_file.py index eff7c2cd842..313060cf40f 100644 --- a/conan/internal/model/conan_file.py +++ b/conan/internal/model/conan_file.py @@ -75,6 +75,7 @@ class ConanFile: buildenv_info = None runenv_info = None conf_info = None + conf = None generator_info = None conan_data = None diff --git a/test/unittests/tools/cmake/test_cmaketoolchain.py b/test/unittests/tools/cmake/test_cmaketoolchain.py index 05d7720bfcb..e1fd31ea41b 100644 --- a/test/unittests/tools/cmake/test_cmaketoolchain.py +++ b/test/unittests/tools/cmake/test_cmaketoolchain.py @@ -314,6 +314,12 @@ def test_toolset_update_version_overflow(self, conanfile_msvc): c = CMakeToolchain(conanfile_msvc) assert 'set(CMAKE_GENERATOR_TOOLSET "v143,version=14.48" CACHE STRING "" FORCE)' in c.content + def test_toolset_exact(self, conanfile_msvc): + conanfile_msvc.settings.compiler.version = "195" + conanfile_msvc.conf.define("tools.microsoft:msvc_update", "0.35717") + c = CMakeToolchain(conanfile_msvc) + assert 'set(CMAKE_GENERATOR_TOOLSET "v145,version=14.50.35717"' in c.content + def test_toolset_x64(self, conanfile_msvc): # https://github.com/conan-io/conan/issues/11144 conanfile_msvc.conf.define("tools.cmake.cmaketoolchain:toolset_arch", "x64") From 035961fc963a98e8afcb8918ed4c8368e3048b22 Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Wed, 8 Apr 2026 14:07:36 +0200 Subject: [PATCH 083/110] Avoid detecting default package manager when overrided from profile (#19847) Avoid searching for default package manager when defined in profile --- conan/tools/system/package_manager.py | 2 +- .../integration/tools/system/package_manager_test.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/conan/tools/system/package_manager.py b/conan/tools/system/package_manager.py index f1fcfae57f5..412569ea0f9 100644 --- a/conan/tools/system/package_manager.py +++ b/conan/tools/system/package_manager.py @@ -26,7 +26,7 @@ def __init__(self, conanfile): :param conanfile: The current recipe object. Always use ``self``. """ self._conanfile = conanfile - self._active_tool = self._conanfile.conf.get("tools.system.package_manager:tool", default=self.get_default_tool()) + self._active_tool = self._conanfile.conf.get("tools.system.package_manager:tool") or self.get_default_tool() self._sudo = self._conanfile.conf.get("tools.system.package_manager:sudo", default=False, check_type=bool) self._sudo_askpass = self._conanfile.conf.get("tools.system.package_manager:sudo_askpass", default=False, check_type=bool) self._mode = self._conanfile.conf.get("tools.system.package_manager:mode", default=self.mode_check) diff --git a/test/integration/tools/system/package_manager_test.py b/test/integration/tools/system/package_manager_test.py index 6632c1ee683..883b9bd3e4f 100644 --- a/test/integration/tools/system/package_manager_test.py +++ b/test/integration/tools/system/package_manager_test.py @@ -84,6 +84,18 @@ def test_package_manager_distro(distro, tool): assert tool == manager.get_default_tool() +def test_conf_tool_skips_default_detection_message_on_unknown_distro(): + """If ``tools.system.package_manager:tool`` is set, ``get_default_tool`` shall never be invoked""" + with mock.patch('conan.ConanFile.context', new_callable=PropertyMock) as context_mock: + context_mock.return_value = "host" + conanfile = ConanFileMock() + conanfile.settings = Settings() + conanfile.conf.define("tools.system.package_manager:tool", "apt-get") + with mock.patch.object(_SystemPackageManagerTool, "get_default_tool") as get_default_mock: + Apt(conanfile) + get_default_mock.assert_not_called() + + @pytest.mark.parametrize("sudo, sudo_askpass, expected_str", [ (True, True, "sudo -A "), (True, False, "sudo "), From 6f4cb53e04034eceececc64b2950af68109835e3 Mon Sep 17 00:00:00 2001 From: Carlos Zoido Date: Wed, 8 Apr 2026 19:35:32 +0200 Subject: [PATCH 084/110] Add `--strict` flag to `conan remote auth` (#19848) * add strict * third case --- conan/cli/commands/remote.py | 9 +++++++ .../command/remote/test_remote_users.py | 25 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/conan/cli/commands/remote.py b/conan/cli/commands/remote.py index 43cd7d2d813..96f450572f5 100644 --- a/conan/cli/commands/remote.py +++ b/conan/cli/commands/remote.py @@ -284,6 +284,8 @@ def remote_auth(conan_api, parser, subparser, *args): no CONAN_LOGIN* and CONAN_PASSWORD* variables available which could be used. Usually you'd use this method over conan remote login for scripting which needs to run in CI and locally. + By default, this command returns exit code 0 even if authentication fails for some remotes. + Use --strict to return exit code 1 if authentication fails for any remote. """ subparser.add_argument("remote", help="Pattern or name of the remote/s to authenticate against." " The pattern uses 'fnmatch' style wildcards.") @@ -296,6 +298,8 @@ def remote_auth(conan_api, parser, subparser, *args): "instance has anonymous access enabled and Conan would not ask " "for username and password even for non-anonymous repositories " "if not yet authenticated.") + subparser.add_argument("--strict", action="store_true", + help="Return exit code 1 if authentication fails for any remote.") args = parser.parse_args(*args) remotes = conan_api.remotes.list(pattern=args.remote) if not remotes: @@ -307,6 +311,11 @@ def remote_auth(conan_api, parser, subparser, *args): results[r.name] = {"user": conan_api.remotes.user_auth(r, args.with_user, args.force)} except Exception as e: results[r.name] = {"error": str(e)} + + if args.strict: + failed = [name for name, v in results.items() if "error" in v] + if failed: + raise ConanException("Authentication error in remotes: {}".format(", ".join(failed))) return results diff --git a/test/integration/command/remote/test_remote_users.py b/test/integration/command/remote/test_remote_users.py index 0503097a4a1..dfd42522875 100644 --- a/test/integration/command/remote/test_remote_users.py +++ b/test/integration/command/remote/test_remote_users.py @@ -456,6 +456,31 @@ def test_remote_auth_server_expire_token(self): c.run("remote auth *") assert "error: Too many failed login attempts, bye!" in c.out + def test_remote_auth_strict(self): + servers = { + "good": TestServer(users={"myuser": "mypassword"}), + "bad": TestServer(users={"myuser": "mypassword"}), + } + # All succeed -> exit 0 + c = TestClient(light=True, servers=servers, + inputs=["myuser", "mypassword", "myuser", "mypassword"]) + c.run("remote auth * --strict") + assert "user: myuser" in c.out + + # Partial failure without --strict -> exit 0 + c2 = TestClient(light=True, servers=servers, + inputs=["myuser", "mypassword", + "wrong", "wrong", "wrong", "wrong", "wrong", "wrong"]) + c2.run("remote auth *") + assert "error" in c2.out + + # Partial failure with --strict -> exit != 0 + c3 = TestClient(light=True, servers=servers, + inputs=["myuser", "mypassword", + "wrong", "wrong", "wrong", "wrong", "wrong", "wrong"]) + c3.run("remote auth * --strict", assert_error=True) + assert "Authentication error in remotes: bad" in c3.out + def test_auth_after_logout(self): server = TestServer(users={"myuser": "password"}) c = TestClient(light=True, servers={"default": server}, inputs=["myuser", "password"]*2) From fc22da00955d572011d80bf075d1e0c277353073 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= <5364255+AbrilRBS@users.noreply.github.com> Date: Wed, 8 Apr 2026 19:39:43 +0200 Subject: [PATCH 085/110] Propagate build requirement run trait to upstream shared dependency (#19751) * Only propagate shared build requires if the requires itself is run=True * Test for run=False of the shared library * Different versions * Fix tests * experimental * This arcane knowledge shall be kept secret for now --- conan/internal/model/requires.py | 2 +- .../graph/core/graph_manager_test.py | 48 +++++++++++++++++-- .../graph/test_require_same_pkg_versions.py | 23 +++++++++ 3 files changed, 67 insertions(+), 6 deletions(-) diff --git a/conan/internal/model/requires.py b/conan/internal/model/requires.py index 9ca88535232..62cbed2c1b3 100644 --- a/conan/internal/model/requires.py +++ b/conan/internal/model/requires.py @@ -289,7 +289,7 @@ def transform_downstream(self, pkg_type, require, dep_pkg_type): # visible=self.visible will further propagate it downstream if dep_pkg_type is PackageType.SHARED or require.run: downstream_require = Requirement(require.ref, headers=False, libs=False, build=True, - run=True, visible=self.visible, direct=False) + run=self.run, visible=self.visible, direct=False) return downstream_require return diff --git a/test/integration/graph/core/graph_manager_test.py b/test/integration/graph/core/graph_manager_test.py index ccca1384154..4146e41d712 100644 --- a/test/integration/graph/core/graph_manager_test.py +++ b/test/integration/graph/core/graph_manager_test.py @@ -14,11 +14,11 @@ def _check_transitive(node, transitive_deps): f"!=\n{transitive_deps}" for v1, v2 in zip(values, transitive_deps): - assert v1.node is v2[0], f"{v1.node}!={v2[0]}" - assert v1.require.headers is v2[1], f"{v1.node}!={v2[0]} headers" - assert v1.require.libs is v2[2], f"{v1.node}!={v2[0]} libs" - assert v1.require.build is v2[3], f"{v1.node}!={v2[0]} build" - assert v1.require.run is v2[4], f"{v1.node}!={v2[0]} run" + assert v1.node is v2[0], f"{v1.node}!=expected {v2[0]}" + assert v1.require.headers is v2[1], f"{v1.node}!=expected {v2[0]} ({v2[1]}) headers" + assert v1.require.libs is v2[2], f"{v1.node}!=expected {v2[0]} ({v2[2]}) libs" + assert v1.require.build is v2[3], f"{v1.node}!=expected {v2[0]} ({v2[3]}) build" + assert v1.require.run is v2[4], f"{v1.node}!=expected {v2[0]} ({v2[4]}) run" assert len(v2) <= 5 @@ -761,6 +761,44 @@ def test_negate_run(self): (libb, False, False, False, True), (liba, False, False, False, False)]) + def test_run_false_multiversion_shared(self): + self.recipe_conanfile("liba/1.0", GenConanfile().with_package_type("shared-library")) + self.recipe_conanfile("liba/2.0", GenConanfile().with_package_type("shared-library")) + self.recipe_conanfile("tool/1.0", GenConanfile().with_package_type("application") + .with_requires("liba/1.0")) + self.recipe_conanfile("tool/2.0", GenConanfile().with_package_type("application") + .with_requires("liba/2.0")) + self.recipe_conanfile("libc/0.1", + GenConanfile().with_package_type("shared-library") + .with_tool_requirement("tool/1.0", run=False) + .with_tool_requirement("tool/2.0", run=False)) + consumer = self.recipe_consumer("app/0.1", ["libc/0.1"]) + + deps_graph = self.build_consumer(consumer) + + assert 6 == len(deps_graph.nodes) + app = deps_graph.root + libc = app.edges[0].dst + tool1 = libc.edges[0].dst + tool2 = libc.edges[1].dst + liba1 = tool1.edges[0].dst + liba2 = tool2.edges[0].dst + + self._check_node(app, "app/0.1", deps=[libc]) + self._check_node(libc, "libc/0.1#123", deps=[tool1, tool2], dependents=[app]) + self._check_node(tool1, "tool/1.0#123", deps=[liba1], dependents=[libc]) + self._check_node(tool2, "tool/2.0#123", deps=[liba2], dependents=[libc]) + + # node, headers, lib, build, run + _check_transitive(libc, [ + (tool1, False, False, True, False), + (liba1, False, False, True, False), + (tool2, False, False, True, False), + (liba2, False, False, True, False) + ]) + + _check_transitive(app, [(libc, True, True, False, True)]) + def test_force_run(self): # app -> libc/0.1 -> libb0.1 -> liba0.1 # even if all are static, there is something in a static lib (files or whatever) that diff --git a/test/integration/graph/test_require_same_pkg_versions.py b/test/integration/graph/test_require_same_pkg_versions.py index c97d9a00a54..399031504cf 100644 --- a/test/integration/graph/test_require_same_pkg_versions.py +++ b/test/integration/graph/test_require_same_pkg_versions.py @@ -419,3 +419,26 @@ def package(self): assert "MYGCC=1.0!!" in c.out assert "wrapperb=1.0!!" in c.out assert "MYGCC=2.0!!" in c.out + + def test_require_different_versions_transitive_conflict_require(self): + c = TestClient(light=True) + c.save({"abseil/conanfile.py": GenConanfile("abseil") + .with_package_type("shared-library"), + "protobuf1/conanfile.py": GenConanfile("protobuf", "1.0") + .with_requirement("abseil/[~1]"), + "protobuf2/conanfile.py": GenConanfile("protobuf", "2.0") + .with_requirement("abseil/[~2]"), + "consumer/conanfile.py": GenConanfile("consumer", "1.0") + .with_tool_requirement("protobuf/1.0", run=False) + .with_tool_requirement("protobuf/2.0", run=False) + }) + + c.run("create abseil --version=1.0") + c.run("create abseil --version=2.0") + c.run("create protobuf1") + c.run("create protobuf2") + c.run("export consumer") + c.run("install consumer") + # No conflict, because both protobufs are run=False, + # so the shared-library abseil is not propagated with run=True in any of the branches + assert "Install finished successfully" in c.out From 24559231f04ef9605046047cae1a9bce18c2a464 Mon Sep 17 00:00:00 2001 From: James Date: Fri, 10 Apr 2026 07:15:14 +0200 Subject: [PATCH 086/110] Refactor/api helpers (#19797) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip * wip * wip * fix * Undefined locale for list html (#19828) * finalize() output folder should be printed only once (#19834) * Refactor/ordered dict (#19839) * wip * more rid * wip * fix powershell unset (and other possible unsets too) (#19820) * doc retry conf and change default (#19830) * doc retry conf and change default * fix tests * Documentation improvement for items() iteration of packages list (#19829) * Improve items() * Improve items documentation * Fix docstring * wip * review, removed global_editable_packages * fix --------- Co-authored-by: Abril Rincón Blanco <5364255+AbrilRBS@users.noreply.github.com> Co-authored-by: PerseoGI --- conan/api/conan_api.py | 35 ++++++++++++++- conan/api/subapi/cache.py | 5 +-- conan/api/subapi/config.py | 7 ++- conan/api/subapi/export.py | 5 +-- conan/api/subapi/graph.py | 45 ++++++++++---------- conan/api/subapi/install.py | 11 +++-- conan/api/subapi/local.py | 30 ++++++------- conan/api/subapi/report.py | 9 ++-- conan/api/subapi/upload.py | 10 ++--- conan/api/subapi/workspace.py | 14 +++--- conan/internal/api/uploader.py | 7 ++- conan/internal/conan_app.py | 1 - conan/internal/runner/docker.py | 5 +-- test/integration/conan_api/test_local_api.py | 4 +- 14 files changed, 105 insertions(+), 83 deletions(-) diff --git a/conan/api/conan_api.py b/conan/api/conan_api.py index 85d7a616856..c0f2f63458d 100644 --- a/conan/api/conan_api.py +++ b/conan/api/conan_api.py @@ -24,11 +24,17 @@ from conan.internal.api.remotes.localdb import LocalDB from conan.internal.cache.cache import PkgCache from conan.internal.cache.home_paths import HomePaths +from conan.internal.conan_app import ConanFileHelpers, CmdWrapper +from conan.internal.graph.proxy import ConanProxy +from conan.internal.graph.python_requires import PyRequireLoader +from conan.internal.graph.range_resolver import RangeResolver from conan.internal.hook_manager import HookManager from conan.internal.loader import load_python_file +from conan.internal.loader import ConanFileLoader from conan.internal.model.conf import load_global_conf, ConfDefinition, CORE_CONF_PATTERN from conan.internal.model.settings import load_settings_yml from conan.internal.paths import get_conan_user_home +from conan.internal.api.local.editable import EditablePackages from conan.internal.api.migrations import ClientMigrator from conan.internal.model.version_range import validate_conan_version from conan.internal.rest.auth_manager import ConanApiAuthManager @@ -106,7 +112,6 @@ def reinit(self): Reinitialize the Conan API. This is useful when the configuration changes. """ self._api_helpers.reinit() - self.local.reinit() def migrate(self): # Migration system @@ -123,6 +128,7 @@ def __init__(self, conan_api): self._init_global_conf() # TODO: Make uniform lazy vs non lazy collaborators self.hook_manager = HookManager(HomePaths(self._conan_api.home_folder).hooks_path) + self._editable_packages = EditablePackages(self._conan_api.home_folder) # Wraps an http_requester to inject proxies, certs, etc self._requester = ConanRequester(self.global_conf, self._conan_api.home_folder) self.cache = PkgCache(self._conan_api.home_folder, self.global_conf) @@ -175,6 +181,7 @@ def reinit(self): self._settings_yml = None self.cache = PkgCache(self._conan_api.home_folder, self.global_conf) self._remote_manager = None + self._editable_packages = EditablePackages(self._conan_api.home_folder) self._compression_plugin = None @property @@ -198,3 +205,29 @@ def remote_manager(self): @property def requester(self): return self._requester + + @property + def editable_packages(self): + # These are just the global editables, not including workspace ones + return self._editable_packages + + def get_loader(self): + ws_editables = self._conan_api.workspace.packages() + editable_packages = self._editable_packages.update_copy(ws_editables) + + legacy_update = self.global_conf.get("core:update_policy", choices=["legacy"]) + # This proxy is caching information + proxy = ConanProxy(self.cache, self.remote_manager, editable_packages, + legacy_update=legacy_update) + # This is caching too + range_resolver = RangeResolver(self.cache, self.remote_manager, self.global_conf, + editable_packages) + + cmd_wrap = CmdWrapper(HomePaths(self._conan_api.home_folder).wrapper_path) + conanfile_helpers = ConanFileHelpers(self._requester, cmd_wrap, self.global_conf, + self.cache, self._conan_api.home_folder, + self._conan_api) + pyreq_loader = PyRequireLoader(proxy, range_resolver, self.global_conf) + # This is caching too! + loader = ConanFileLoader(pyreq_loader, conanfile_helpers) + return proxy, range_resolver, loader diff --git a/conan/api/subapi/cache.py b/conan/api/subapi/cache.py index f2bf74e136c..7e43faaff17 100644 --- a/conan/api/subapi/cache.py +++ b/conan/api/subapi/cache.py @@ -19,7 +19,6 @@ from conan.api.model import PkgReference from conan.api.model import RecipeReference from conan.internal.api.uploader import PackagePreparator -from conan.internal.conan_app import ConanApp from conan.internal.rest.pkg_sign import PkgSignaturesPlugin from conan.internal.util.dates import revision_timestamp_now from conan.internal.util.files import rmdir, mkdir, remove, save @@ -194,8 +193,8 @@ def sign(self, package_list): "information on how to configure the plugin, please read the documentation at " "https://docs.conan.io/2/reference/extensions/package_signing.html.") - app = ConanApp(self._conan_api) - preparator = PackagePreparator(app, self._api_helpers.cache, + _, _, loader = self._api_helpers.get_loader() + preparator = PackagePreparator(loader, self._api_helpers.cache, self._api_helpers.remote_manager, self._api_helpers.global_conf) # Some packages can have missing sources/exports_sources diff --git a/conan/api/subapi/config.py b/conan/api/subapi/config.py index eca212d3534..d8af964b5a3 100644 --- a/conan/api/subapi/config.py +++ b/conan/api/subapi/config.py @@ -3,7 +3,6 @@ from conan.api.output import ConanOutput from conan.internal.cache.home_paths import HomePaths -from conan.internal.conan_app import ConanApp from conan.internal.graph.graph import CONTEXT_HOST, RECIPE_VIRTUAL, Node from conan.internal.graph.graph_builder import DepsGraphBuilder from conan.internal.graph.profile_node_definer import consumer_definer @@ -212,7 +211,7 @@ def fetch_packages(self, requires, lockfile=None, remotes=None, profile=None): remotes = conan_api.remotes.list() if remotes is None else remotes profile_host = profile_build = profile or conan_api.profiles.get_profile([]) - app = ConanApp(self._conan_api) + proxy, range_resolver, loader = self._helpers.get_loader() cache = self._helpers.cache ConanOutput().title("Fetching requested configuration packages") @@ -220,13 +219,13 @@ def fetch_packages(self, requires, lockfile=None, remotes=None, profile=None): for ref in requires: # Computation of a very simple graph that requires "ref" # Need to convert input requires to RecipeReference - conanfile = app.loader.load_virtual(requires=[ref]) + conanfile = loader.load_virtual(requires=[ref]) consumer_definer(conanfile, profile_host, profile_build) root_node = Node(ref=None, conanfile=conanfile, context=CONTEXT_HOST, recipe=RECIPE_VIRTUAL) root_node.is_conf = True update = ["*"] - builder = DepsGraphBuilder(app.proxy, app.loader, app.range_resolver, cache, remotes, + builder = DepsGraphBuilder(proxy, loader, range_resolver, cache, remotes, update, update, self._helpers.global_conf) deps_graph = builder.load_graph(root_node, profile_host, profile_build, lockfile) diff --git a/conan/api/subapi/export.py b/conan/api/subapi/export.py index 3901c3c0b8c..ca8a74cbf93 100644 --- a/conan/api/subapi/export.py +++ b/conan/api/subapi/export.py @@ -5,7 +5,6 @@ from conan.api.output import ConanOutput from conan.cli.printers.graph import print_graph_basic from conan.internal.cache.cache import PkgCache -from conan.internal.conan_app import ConanApp from conan.internal.api.export import cmd_export from conan.internal.methods import run_package_method from conan.internal.graph.graph import BINARY_BUILD, RECIPE_INCACHE @@ -44,9 +43,9 @@ def export(self, path, name: str = None, version: str = None, user: str = None, :return: A tuple of the exported RecipeReference and a ConanFile object """ ConanOutput().title("Exporting recipe to the cache") - app = ConanApp(self._conan_api) + _, _, loader = self._helpers.get_loader() hook_manager = self._helpers.hook_manager - return cmd_export(app.loader,self._helpers.cache, hook_manager, self._helpers.global_conf, path, + return cmd_export(loader,self._helpers.cache, hook_manager, self._helpers.global_conf, path, name, version, user, channel, graph_lock=lockfile, remotes=remotes) def export_pkg_graph(self, path, ref: RecipeReference, profile_host, profile_build, diff --git a/conan/api/subapi/graph.py b/conan/api/subapi/graph.py index 24200b455ba..cfa182b0641 100644 --- a/conan/api/subapi/graph.py +++ b/conan/api/subapi/graph.py @@ -1,5 +1,4 @@ from conan.api.output import ConanOutput -from conan.internal.conan_app import ConanApp from conan.internal.model.recipe_ref import ref_matches from conan.internal.graph.graph import Node, RECIPE_CONSUMER, CONTEXT_HOST, RECIPE_VIRTUAL, \ CONTEXT_BUILD, BINARY_MISSING, DepsGraph @@ -21,17 +20,17 @@ def _load_root_consumer_conanfile(self, path, profile_host, profile_build, name=None, version=None, user=None, channel=None, update=None, remotes=None, lockfile=None, is_build_require=False): - app = ConanApp(self._conan_api) + _, _, loader = self._helpers.get_loader() if path.endswith(".py"): - conanfile = app.loader.load_consumer(path, - name=name, - version=version, - user=user, - channel=channel, - graph_lock=lockfile, - remotes=remotes, - update=update) + conanfile = loader.load_consumer(path, + name=name, + version=version, + user=user, + channel=channel, + graph_lock=lockfile, + remotes=remotes, + update=update) ref = RecipeReference(conanfile.name, conanfile.version, conanfile.user, conanfile.channel) context = CONTEXT_BUILD if is_build_require else CONTEXT_HOST @@ -43,7 +42,7 @@ def _load_root_consumer_conanfile(self, path, profile_host, profile_build, root_node = Node(ref, conanfile, context=context, recipe=RECIPE_CONSUMER, path=path) root_node.should_build = True # It is a consumer, this is something we are building else: - conanfile = app.loader.load_conanfile_txt(path) + conanfile = loader.load_conanfile_txt(path) consumer_definer(conanfile, profile_host, profile_build) root_node = Node(None, conanfile, context=CONTEXT_HOST, recipe=RECIPE_CONSUMER, path=path) @@ -65,10 +64,9 @@ def load_root_test_conanfile(self, path, tested_reference, profile_host, profile :return: a graph Node, recipe=RECIPE_CONSUMER """ - app = ConanApp(self._conan_api) # necessary for correct resolution and update of remote python_requires - loader = app.loader + _, _, loader = self._helpers.get_loader() profile_host.options.scope(tested_reference) # do not try apply lock_python_requires for test_package/conanfile.py consumer @@ -92,12 +90,12 @@ def _load_root_virtual_conanfile(self, profile_host, profile_build, requires, to python_requires=None): if not python_requires and not requires and not tool_requires: raise ConanException("Provide requires or tool_requires") - app = ConanApp(self._conan_api) - conanfile = app.loader.load_virtual(requires=requires, - tool_requires=tool_requires, - python_requires=python_requires, - graph_lock=lockfile, remotes=remotes, - update=update, check_updates=check_updates) + _, _, loader = self._helpers.get_loader() + conanfile = loader.load_virtual(requires=requires, + tool_requires=tool_requires, + python_requires=python_requires, + graph_lock=lockfile, remotes=remotes, + update=update, check_updates=check_updates) consumer_definer(conanfile, profile_host, profile_build) root_node = Node(ref=None, conanfile=conanfile, context=CONTEXT_HOST, recipe=RECIPE_VIRTUAL) @@ -182,15 +180,16 @@ def load_graph(self, root_node, profile_host, profile_build, lockfile=None, remo :param check_update: For "graph info" command, check if there are recipe updates """ ConanOutput().title("Computing dependency graph") - app = ConanApp(self._conan_api) assert profile_host is not None assert profile_build is not None + proxy, range_resolver, loader = self._helpers.get_loader() + remotes = remotes or [] - cache = self._conan_api._api_helpers.cache # noqa - builder = DepsGraphBuilder(app.proxy, app.loader, app.range_resolver, cache, remotes, - update, check_update, self._conan_api._api_helpers.global_conf) + cache = self._helpers.cache + builder = DepsGraphBuilder(proxy, loader, range_resolver, cache, remotes, + update, check_update, self._helpers.global_conf) deps_graph = builder.load_graph(root_node, profile_host, profile_build, lockfile) return deps_graph diff --git a/conan/api/subapi/install.py b/conan/api/subapi/install.py index f53abb27f6f..3c9271b7846 100644 --- a/conan/api/subapi/install.py +++ b/conan/api/subapi/install.py @@ -38,7 +38,8 @@ def install_binaries(self, deps_graph, remotes: List[Remote] = None, return_inst :param remotes: List of remotes to fetch packages from if necessary. :param return_install_error: If ``True``, do not raise an exception, but return it """ - installer = BinaryInstaller(self._conan_api, self._helpers.global_conf, self._helpers.hook_manager) + installer = BinaryInstaller(self._conan_api, self._helpers.global_conf, + self._helpers.hook_manager) install_graph = InstallGraph(deps_graph) install_graph.raise_errors() install_order = install_graph.install_order() @@ -65,7 +66,8 @@ def install_system_requires(self, graph, only_info=False): :param graph: Dependency graph to install system requirements for :param only_info: If ``True``, only reporting and checking of whether the system requirements are installed is performed. """ - installer = BinaryInstaller(self._conan_api, self._helpers.global_conf, self._helpers.hook_manager) + installer = BinaryInstaller(self._conan_api, self._helpers.global_conf, + self._helpers.hook_manager) installer.install_system_requires(graph, only_info) def install_sources(self, graph, remotes: List[Remote]): @@ -84,7 +86,8 @@ def install_sources(self, graph, remotes: List[Remote]): :param remotes: List of remotes where the ``exports_sources`` of the packages might be located :param graph: Dependency graph to download sources from """ - installer = BinaryInstaller(self._conan_api, self._helpers.global_conf, self._helpers.hook_manager) + installer = BinaryInstaller(self._conan_api, self._helpers.global_conf, + self._helpers.hook_manager) installer.install_sources(graph, remotes) def install_consumer(self, deps_graph, generators: List[str] = None, source_folder=None, @@ -140,7 +143,7 @@ def install_consumer(self, deps_graph, generators: List[str] = None, source_fold write_generators(conanfile, hook_manager, self._conan_api.home_folder, envs_generation=envs_generation) - def deploy(self, graph, deployer: List[str], deploy_package: List[str]=None, + def deploy(self, graph, deployer: List[str], deploy_package: List[str] = None, deploy_folder=None) -> None: """ Run the given deployer in the dependency graph. diff --git a/conan/api/subapi/local.py b/conan/api/subapi/local.py index 848c33be54c..01c572954ee 100644 --- a/conan/api/subapi/local.py +++ b/conan/api/subapi/local.py @@ -2,8 +2,6 @@ from typing import List from conan.cli import make_abs_path -from conan.internal.conan_app import ConanApp -from conan.internal.api.local.editable import EditablePackages from conan.internal.methods import run_build_method, run_source_method from conan.internal.graph.graph import CONTEXT_HOST from conan.internal.graph.profile_node_definer import initialize_conanfile_profile @@ -21,7 +19,6 @@ class LocalAPI: def __init__(self, conan_api, helpers): self._conan_api = conan_api self._helpers = helpers - self.editable_packages = EditablePackages(conan_api.home_folder) @staticmethod def get_conanfile_path(path, cwd, py): @@ -73,8 +70,8 @@ def editable_add(self, path, name=None, version=None, user=None, channel=None, c :return: RecipeReference of the added package """ path = self.get_conanfile_path(path, cwd, py=True) - app = ConanApp(self._conan_api) - conanfile = app.loader.load_named(path, name, version, user, channel, remotes=remotes) + _, _, loader = self._helpers.get_loader() + conanfile = loader.load_named(path, name, version, user, channel, remotes=remotes) if conanfile.name is None or conanfile.version is None: raise ConanException("Editable package recipe should declare its name and version") ref = RecipeReference(conanfile.name, conanfile.version, conanfile.user, conanfile.channel) @@ -82,7 +79,7 @@ def editable_add(self, path, name=None, version=None, user=None, channel=None, c target_path = self.get_conanfile_path(path=path, cwd=cwd, py=True) output_folder = make_abs_path(output_folder) if output_folder else None # Check the conanfile is there, and name/version matches - self.editable_packages.add(ref, target_path, output_folder=output_folder) + self._helpers.editable_packages.add(ref, target_path, output_folder=output_folder) return ref def editable_remove(self, path=None, requires=None, cwd=None): @@ -99,10 +96,10 @@ def editable_remove(self, path=None, requires=None, cwd=None): if path: path = make_abs_path(path, cwd) path = os.path.join(path, "conanfile.py") - return self.editable_packages.remove(path, requires) + return self._helpers.editable_packages.remove(path, requires) def editable_list(self): - return self.editable_packages.edited_refs + return self._helpers.editable_packages.edited_refs def source(self, path, name=None, version=None, user=None, channel=None, remotes: List[Remote] = None): @@ -118,10 +115,10 @@ def source(self, path, name=None, version=None, user=None, channel=None, :param channel: The channel of the package. If not defined, it is taken from conanfile :param remotes: The remotes to resolve possible ``python-requires`` for this recipe if needed. """ - app = ConanApp(self._conan_api) - conanfile = app.loader.load_consumer(path, name=name, version=version, - user=user, channel=channel, graph_lock=None, - remotes=remotes) + _, _, loader = self._helpers.get_loader() + conanfile = loader.load_consumer(path, name=name, version=version, + user=user, channel=channel, graph_lock=None, + remotes=remotes) # This profile is empty, but with the conf from global.conf profile = self._conan_api.profiles.get_profile([]) initialize_conanfile_profile(conanfile, profile, profile, CONTEXT_HOST, False) @@ -184,10 +181,7 @@ def test(conanfile) -> None: def inspect(self, conanfile_path, remotes, lockfile, name=None, version=None, user=None, channel=None): - app = ConanApp(self._conan_api) - conanfile = app.loader.load_named(conanfile_path, name=name, version=version, user=user, - channel=channel, remotes=remotes, graph_lock=lockfile) + _, _, loader = self._helpers.get_loader() + conanfile = loader.load_named(conanfile_path, name=name, version=version, user=user, + channel=channel, remotes=remotes, graph_lock=lockfile) return conanfile - - def reinit(self): - self.editable_packages = EditablePackages(self._conan_api.home_folder) diff --git a/conan/api/subapi/report.py b/conan/api/subapi/report.py index 7fe903411f2..21dcd960fae 100644 --- a/conan/api/subapi/report.py +++ b/conan/api/subapi/report.py @@ -5,7 +5,6 @@ from conan.api.output import ConanOutput from conan.errors import ConanException from conan.api.model import RecipeReference -from conan.internal.conan_app import ConanApp from conan.internal.errors import conanfile_exception_formatter from conan.internal.graph.graph import CONTEXT_HOST from conan.internal.graph.profile_node_definer import initialize_conanfile_profile @@ -86,10 +85,10 @@ def _source(path_to_conanfile, reference): def _configure_source(conan_api, hook_manager, conanfile_path, ref, remotes): - app = ConanApp(conan_api) - conanfile = app.loader.load_consumer(conanfile_path, name=ref.name, version=str(ref.version), - user=ref.user, channel=ref.channel, graph_lock=None, - remotes=remotes) + _, _, loader = conan_api._api_helpers.get_loader() # noqa + conanfile = loader.load_consumer(conanfile_path, name=ref.name, version=str(ref.version), + user=ref.user, channel=ref.channel, graph_lock=None, + remotes=remotes) # This profile is empty, but with the conf from global.conf profile = conan_api.profiles.get_profile([]) initialize_conanfile_profile(conanfile, profile, profile, CONTEXT_HOST, False) diff --git a/conan/api/subapi/upload.py b/conan/api/subapi/upload.py index 1fc18bcd593..c8243790b45 100644 --- a/conan/api/subapi/upload.py +++ b/conan/api/subapi/upload.py @@ -6,7 +6,6 @@ from conan.api.model import PackagesList, Remote from conan.api.output import ConanOutput from conan.internal.api.upload import add_urls -from conan.internal.conan_app import ConanApp from conan.internal.api.uploader import PackagePreparator, UploadExecutor, UploadUpstreamChecker from conan.internal.rest.pkg_sign import PkgSignaturesPlugin from conan.internal.rest.file_uploader import FileUploader @@ -36,11 +35,11 @@ def check_upstream(self, package_list: PackagesList, remote: Remote, :parameter force: If ``True``, it will skip the check and mark that all items need to be uploaded. A ``force_upload`` key will be added to the entries that will be uploaded. """ - app = ConanApp(self._conan_api) + _, _, loader = self._api_helpers.get_loader() for ref, _ in package_list.items(): layout = self._api_helpers.cache.recipe_layout(ref) conanfile_path = layout.conanfile() - conanfile = app.loader.load_basic(conanfile_path, remotes=enabled_remotes) + conanfile = loader.load_basic(conanfile_path, remotes=enabled_remotes) if conanfile.upload_policy == "skip": ConanOutput().info(f"{ref}: Skipping upload of binaries, " "because upload_policy='skip'") @@ -63,8 +62,9 @@ def prepare(self, package_list: PackagesList, enabled_remotes: List[Remote], it means that no metadata files should be uploaded.""" if metadata and metadata != [''] and '' in metadata: raise ConanException("Empty string and patterns can not be mixed for metadata.") - app = ConanApp(self._conan_api) - preparator = PackagePreparator(app, self._api_helpers.cache, + + _, _, loader = self._api_helpers.get_loader() + preparator = PackagePreparator(loader, self._api_helpers.cache, self._api_helpers.remote_manager, self._api_helpers.global_conf) preparator.prepare(package_list, enabled_remotes, metadata) diff --git a/conan/api/subapi/workspace.py b/conan/api/subapi/workspace.py index 0394f1829cd..00d7adeb912 100644 --- a/conan/api/subapi/workspace.py +++ b/conan/api/subapi/workspace.py @@ -10,7 +10,6 @@ from conan.cli import make_abs_path from conan.cli.printers.graph import print_graph_basic, print_graph_packages from conan.errors import ConanException -from conan.internal.conan_app import ConanApp from conan.internal.errors import conanfile_exception_formatter from conan.internal.graph.install_graph import ProfileArgs from conan.internal.methods import auto_language, auto_shared_fpic_config_options, \ @@ -142,16 +141,16 @@ def packages(self): def open(self, ref, remotes, cwd=None): cwd = cwd or os.getcwd() - app = ConanApp(self._conan_api) + proxy, _, loader = self._conan_api._api_helpers.get_loader() # noqa ref = RecipeReference.loads(ref) if isinstance(ref, str) else ref - recipe = app.proxy.get_recipe(ref, remotes, update=False, check_update=False) + recipe = proxy.get_recipe(ref, remotes, update=False, check_update=False) layout, recipe_status, remote = recipe if recipe_status == RECIPE_EDITABLE: raise ConanException(f"Can't open a dependency that is already an editable: {ref}") ref = layout.reference conanfile_path = layout.conanfile() - conanfile, module = app.loader.load_basic_module(conanfile_path, remotes=remotes) + conanfile, module = loader.load_basic_module(conanfile_path, remotes=remotes) scm = conanfile.conan_data.get("scm") if conanfile.conan_data else None dst_path = os.path.join(cwd, ref.name) @@ -194,8 +193,8 @@ def add(self, path, name=None, version=None, user=None, channel=None, cwd=None, """ self._check_ws() full_path = self._conan_api.local.get_conanfile_path(path, cwd, py=True) - app = ConanApp(self._conan_api) - conanfile = app.loader.load_named(full_path, name, version, user, channel, remotes=remotes) + _, _, loader = self._conan_api._api_helpers.get_loader() # noqa + conanfile = loader.load_named(full_path, name, version, user, channel, remotes=remotes) if conanfile.name is None or conanfile.version is None: raise ConanException("Editable package recipe should declare its name and version") ref = RecipeReference(conanfile.name, conanfile.version, conanfile.user, conanfile.channel) @@ -330,7 +329,8 @@ def find_folder(ref): if root_class is not None: conanfile = root_class(f"{WORKSPACE_PY} base project Conanfile") # To inject things like cmd_wrapper to the consumer conanfile, so self.run() works - helpers = ConanApp(self._conan_api).loader._conanfile_helpers # noqa + _, _, loader = self._conan_api._api_helpers.get_loader() # noqa + helpers = loader._conanfile_helpers # noqa conanfile._conan_helpers = helpers conanfile._conan_is_consumer = True initialize_conanfile_profile(conanfile, profile_build, profile_host, CONTEXT_HOST, diff --git a/conan/internal/api/uploader.py b/conan/internal/api/uploader.py index a5b70603224..9de51e9232a 100644 --- a/conan/internal/api/uploader.py +++ b/conan/internal/api/uploader.py @@ -6,7 +6,6 @@ import tarfile import time -from conan.internal.conan_app import ConanApp from conan.api.output import ConanOutput from conan.internal.source import retrieve_exports_sources from conan.internal.errors import NotFoundException @@ -104,8 +103,8 @@ def get_compress_level(compressformat, global_conf): class PackagePreparator: - def __init__(self, app: ConanApp, cache, remote_manager, global_conf): - self._app = app + def __init__(self, loader, cache, remote_manager, global_conf): + self._loader = loader self._remote_manager = remote_manager self._cache = cache self._global_conf = global_conf @@ -122,7 +121,7 @@ def prepare(self, pkg_list, enabled_remotes, metadata, force=False): for ref, packages in pkg_list.items(): recipe_layout = self._cache.recipe_layout(ref) conanfile_path = recipe_layout.conanfile() - conanfile = self._app.loader.load_basic(conanfile_path) + conanfile = self._loader.load_basic(conanfile_path) url = conanfile.conan_data.get("scm", {}).get("url") if conanfile.conan_data else None if local_url != "allow" and url is not None: if not any(url.startswith(v) for v in ("ssh", "git", "http", "file")): diff --git a/conan/internal/conan_app.py b/conan/internal/conan_app.py index 3dba2e69b31..6abdde4cf47 100644 --- a/conan/internal/conan_app.py +++ b/conan/internal/conan_app.py @@ -2,7 +2,6 @@ from conan.internal.api.local.editable import EditablePackages from conan.internal.cache.cache import PkgCache -from conan.internal.cache.home_paths import HomePaths from conan.internal.model.conf import ConfDefinition from conan.internal.graph.proxy import ConanProxy from conan.internal.graph.python_requires import PyRequireLoader diff --git a/conan/internal/runner/docker.py b/conan/internal/runner/docker.py index 4a62993c81d..01fab2cc5b9 100644 --- a/conan/internal/runner/docker.py +++ b/conan/internal/runner/docker.py @@ -16,7 +16,6 @@ from conan.internal.model.profile import Profile from conan.internal.model.version import Version from conan.internal.runner.output import RunnerOutput -from conan.internal.conan_app import ConanApp class _ContainerConfig(NamedTuple): @@ -245,9 +244,9 @@ def _run_command(self, command: str, workdir: Optional[str] = None, verbose: boo return stdout_log, stderr_log def _get_volumes_and_docker_path(self) -> tuple[dict, str]: - app = ConanApp(self.conan_api) + _, _, loader = self.conan_api._api_helpers.get_loader() # noqa remotes = self.conan_api.remotes.list(self.args.remote) if not self.args.no_remote else [] - conanfile = app.loader.load_consumer(self.abs_host_path / "conanfile.py", remotes=remotes) + conanfile = loader.load_consumer(self.abs_host_path / "conanfile.py", remotes=remotes) abs_docker_base_path = Path('/') / self.docker_user_name / 'conanrunner' # Check if recipe has defined a root folder # In this case, mount the root folder as the base path and update the abs_docker_path to the diff --git a/test/integration/conan_api/test_local_api.py b/test/integration/conan_api/test_local_api.py index ce728fd58c8..078899cb418 100644 --- a/test/integration/conan_api/test_local_api.py +++ b/test/integration/conan_api/test_local_api.py @@ -13,6 +13,6 @@ def test_local_api(): cache_folder = temp_folder() save(os.path.join(current_folder, "conanfile.py"), str(GenConanfile("foo", "1.0"))) api = ConanAPI(cache_folder) - assert api.local.editable_packages.edited_refs == {} + assert api._api_helpers.editable_packages.edited_refs == {} api.local.editable_add(".", cwd=current_folder) - assert list(api.local.editable_packages.edited_refs) == [RecipeReference.loads("foo/1.0")] + assert list(api._api_helpers.editable_packages.edited_refs) == [RecipeReference.loads("foo/1.0")] From 102fa4175ef45badfa069f3536bb5395956efcec Mon Sep 17 00:00:00 2001 From: James Date: Fri, 10 Apr 2026 10:11:20 +0200 Subject: [PATCH 087/110] Internal improvements (#19852) * wip * wip * wip --- conan/internal/api/migrations.py | 84 +++++++++++++++++- conan/internal/rest/download_cache.py | 2 +- conans/migrations.py | 86 ------------------- conans/server/migrations.py | 2 +- .../test_cmakeconfigdeps_aliases.py | 37 ++++---- .../test_cmakeconfigdeps_new.py | 86 +++++++++---------- test/integration/cache/backup_sources_test.py | 50 +++++++---- .../command/cache/test_cache_sign.py | 1 + .../command/info/test_graph_info_graphical.py | 3 +- .../conanfile/test_finalize_method.py | 2 +- test/integration/remote/retry_test.py | 2 +- .../cmakeconfigdeps/test_cmakeconfigdeps.py | 79 ++++++++--------- .../test_cmakeconfigdeps_frameworks.py | 9 +- .../microsoft/test_nmaketoolchain.py | 1 - .../tools/system/package_manager_test.py | 18 ++-- .../client/migrations/test_migrator.py | 2 +- 16 files changed, 230 insertions(+), 234 deletions(-) delete mode 100644 conans/migrations.py diff --git a/conan/internal/api/migrations.py b/conan/internal/api/migrations.py index e1ff0b7abec..cd59605fb29 100644 --- a/conan/internal/api/migrations.py +++ b/conan/internal/api/migrations.py @@ -1,9 +1,12 @@ import os import textwrap +from conan import conan_version from conan.api.output import ConanOutput +from conan.errors import ConanException, ConanMigrationError from conan.internal.default_settings import migrate_settings_file -from conans.migrations import Migrator +from conan.internal.loader import load_python_file +from conan.internal.model.version import Version from conan.internal.util.files import load, save CONAN_GENERATED_COMMENT = "This file was generated by Conan" @@ -32,6 +35,85 @@ def update_file(file_path, new_content): out.success(f"Migration: Successfully updated {file_name}") +CONAN_VERSION = "version.txt" + + +class Migrator: + + def __init__(self, conf_path, current_version): + self.conf_path = conf_path + + self.current_version = current_version + self.file_version_path = os.path.join(self.conf_path, CONAN_VERSION) + + def migrate(self): + try: + old_version = self._load_old_version() + if old_version is None or old_version < self.current_version: + self._apply_migrations(old_version) + self._update_version_file() + elif self.current_version < old_version: # backwards migrations + ConanOutput().warning(f"Downgrading cache from Conan {old_version} to " + f"{self.current_version}") + self._apply_back_migrations() + self._update_version_file() + except Exception as e: + ConanOutput().error(str(e), error_type="exception") + raise ConanMigrationError(e) + + def _update_version_file(self): + try: + save(self.file_version_path, str(self.current_version)) + except Exception as error: + raise ConanException("Can't write version file in '{}': {}" + .format(self.file_version_path, str(error))) + + def _load_old_version(self): + try: + tmp = load(self.file_version_path) + old_version = Version(tmp) + except Exception: + old_version = None + return old_version + + def _apply_migrations(self, old_version): + """ + Apply any migration script. + + :param old_version: ``str`` previous Conan version. + """ + pass + + def _apply_back_migrations(self): + migrations = os.path.join(self.conf_path, "migrations") + if not os.path.exists(migrations): + return + + # Order by versions, and filter only newer than the current version + migration_files = [] + for f in os.listdir(migrations): + if not f.endswith(".py"): + continue + version, remain = f.split("_", 1) + version = Version(version) + if version > conan_version: + migration_files.append((version, remain)) + migration_files = [f"{v}_{r}" for (v, r) in reversed(sorted(migration_files))] + + for migration in migration_files: + ConanOutput().warning(f"Applying downgrade migration {migration}") + migration = os.path.join(migrations, migration) + try: + migrate_module, _ = load_python_file(migration) + migrate_method = migrate_module.migrate + migrate_method(self.conf_path) + except Exception as e: + ConanOutput().error(f"There was an error running downgrade migration: {e}. " + f"Recommended to remove the cache and start from scratch", + error_type="exception") + os.remove(migration) + + class ClientMigrator(Migrator): def __init__(self, cache_folder, current_version): diff --git a/conan/internal/rest/download_cache.py b/conan/internal/rest/download_cache.py index 5e366084470..5aeddb5b72d 100644 --- a/conan/internal/rest/download_cache.py +++ b/conan/internal/rest/download_cache.py @@ -139,7 +139,7 @@ def update_backup_sources_json(cached_path, conanfile, urls): urls = [urls] existing_urls = summary["references"].setdefault(summary_key, []) existing_urls.extend(url for url in urls if url not in existing_urls) - conanfile.output.verbose(f"Updating ${summary_path} summary file") + conanfile.output.verbose(f"Updating {summary_path} summary file") summary_dump = json.dumps(summary) conanfile.output.debug(f"New summary: ${summary_dump}") save(summary_path, json.dumps(summary)) diff --git a/conans/migrations.py b/conans/migrations.py deleted file mode 100644 index 3efa116536c..00000000000 --- a/conans/migrations.py +++ /dev/null @@ -1,86 +0,0 @@ -import os - -from conan import conan_version -from conan.api.output import ConanOutput -from conan.internal.loader import load_python_file -from conan.errors import ConanException, ConanMigrationError -from conan.internal.model.version import Version -from conan.internal.util.files import load, save - -CONAN_VERSION = "version.txt" - - -class Migrator: - - def __init__(self, conf_path, current_version): - self.conf_path = conf_path - - self.current_version = current_version - self.file_version_path = os.path.join(self.conf_path, CONAN_VERSION) - - def migrate(self): - try: - old_version = self._load_old_version() - if old_version is None or old_version < self.current_version: - self._apply_migrations(old_version) - self._update_version_file() - elif self.current_version < old_version: # backwards migrations - ConanOutput().warning(f"Downgrading cache from Conan {old_version} to " - f"{self.current_version}") - self._apply_back_migrations() - self._update_version_file() - except Exception as e: - ConanOutput().error(str(e), error_type="exception") - raise ConanMigrationError(e) - - def _update_version_file(self): - try: - save(self.file_version_path, str(self.current_version)) - except Exception as error: - raise ConanException("Can't write version file in '{}': {}" - .format(self.file_version_path, str(error))) - - def _load_old_version(self): - try: - tmp = load(self.file_version_path) - old_version = Version(tmp) - except Exception: - old_version = None - return old_version - - def _apply_migrations(self, old_version): - """ - Apply any migration script. - - :param old_version: ``str`` previous Conan version. - """ - pass - - def _apply_back_migrations(self): - migrations = os.path.join(self.conf_path, "migrations") - if not os.path.exists(migrations): - return - - # Order by versions, and filter only newer than the current version - migration_files = [] - for f in os.listdir(migrations): - if not f.endswith(".py"): - continue - version, remain = f.split("_", 1) - version = Version(version) - if version > conan_version: - migration_files.append((version, remain)) - migration_files = [f"{v}_{r}" for (v, r) in reversed(sorted(migration_files))] - - for migration in migration_files: - ConanOutput().warning(f"Applying downgrade migration {migration}") - migration = os.path.join(migrations, migration) - try: - migrate_module, _ = load_python_file(migration) - migrate_method = migrate_module.migrate - migrate_method(self.conf_path) - except Exception as e: - ConanOutput().error(f"There was an error running downgrade migration: {e}. " - f"Recommended to remove the cache and start from scratch", - error_type="exception") - os.remove(migration) diff --git a/conans/server/migrations.py b/conans/server/migrations.py index ff0eef353c4..0af576824e2 100644 --- a/conans/server/migrations.py +++ b/conans/server/migrations.py @@ -1,4 +1,4 @@ -from conans.migrations import Migrator +from conan.internal.api.migrations import Migrator class ServerMigrator(Migrator): diff --git a/test/functional/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps_aliases.py b/test/functional/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps_aliases.py index b09dd22071a..1a4b4c8c968 100644 --- a/test/functional/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps_aliases.py +++ b/test/functional/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps_aliases.py @@ -3,7 +3,6 @@ from conan.test.utils.tools import TestClient -new_value = "will_break_next" consumer = textwrap.dedent(""" from conan import ConanFile @@ -13,7 +12,7 @@ class Consumer(ConanFile): name = "consumer" version = "1.0" settings = "os", "compiler", "build_type", "arch" - generators = "CMakeDeps", "CMakeToolchain" + generators = "CMakeConfigDeps", "CMakeToolchain" exports_sources = ["CMakeLists.txt"] requires = "hello/1.0" @@ -50,10 +49,10 @@ def package_info(self): client = TestClient() client.save({"conanfile.py": conanfile}) - client.run(f"create . -c tools.cmake.cmakedeps:new={new_value}") + client.run(f"create .") client.save({"conanfile.py": consumer, "CMakeLists.txt": cmakelists}) - client.run(f"create . -c tools.cmake.cmakedeps:new={new_value}") + client.run(f"create .") assert "hello aliased target: hello::hello" in client.out @@ -84,10 +83,10 @@ def package_info(self): client = TestClient() client.save({"conanfile.py": conanfile}) - client.run(f"create . -c tools.cmake.cmakedeps:new={new_value}") + client.run(f"create .") client.save({"conanfile.py": consumer, "CMakeLists.txt": cmakelists}) - client.run(f"create . -c tools.cmake.cmakedeps:new={new_value}") + client.run(f"create .") assert "hola::adios aliased target: hello::buy" in client.out @@ -119,10 +118,10 @@ def package_info(self): client = TestClient() client.save({"conanfile.py": conanfile}) - client.run(f"create . -c tools.cmake.cmakedeps:new={new_value}") + client.run(f"create .") client.save({"conanfile.py": consumer, "CMakeLists.txt": cmakelists}) - client.run(f"create . -c tools.cmake.cmakedeps:new={new_value}") + client.run(f"create .") assert "hola::hola aliased target: hello::hello" in client.out @@ -153,10 +152,10 @@ def package_info(self): client = TestClient() client.save({"conanfile.py": conanfile}) - client.run(f"create . -c tools.cmake.cmakedeps:new={new_value}") + client.run(f"create .") client.save({"conanfile.py": consumer, "CMakeLists.txt": cmakelists}) - client.run(f"create . -c tools.cmake.cmakedeps:new={new_value}") + client.run(f"create .") assert "hello aliased target: ola::comprar" in client.out @@ -184,10 +183,10 @@ def package_info(self): client = TestClient() client.save({"conanfile.py": conanfile}) - client.run(f"create . -c tools.cmake.cmakedeps:new={new_value}") + client.run(f"create .") client.save({"conanfile.py": consumer, "CMakeLists.txt": cmakelists}) - client.run(f"create . -c tools.cmake.cmakedeps:new={new_value}", assert_error=True) + client.run(f"create .", assert_error=True) assert "Alias 'hello::buy' already defined as a target in hello/1.0" in client.out @@ -216,10 +215,10 @@ def package_info(self): client = TestClient() client.save({"conanfile.py": conanfile}) - client.run(f"create . -c tools.cmake.cmakedeps:new={new_value}") + client.run(f"create .") client.save({"conanfile.py": consumer, "CMakeLists.txt": cmakelists}) - client.run(f"create . -c tools.cmake.cmakedeps:new={new_value}", assert_error=True) + client.run(f"create .", assert_error=True) assert "Alias 'hello::foo' already defined in hello/1.0" in client.out @@ -227,7 +226,8 @@ def package_info(self): @pytest.mark.tool("cmake", "3.27") @pytest.mark.parametrize("root_target", ["hello::custom", None]) def test_skip_global_if_aliased(root_target): - target_line = f'self.cpp_info.set_property("cmake_target_name", "{root_target}")' if root_target else "" + target_line = f'self.cpp_info.set_property("cmake_target_name", "{root_target}")' \ + if root_target else "" target_name = root_target or "hello::hello" conanfile = textwrap.dedent(f""" from conan import ConanFile @@ -253,9 +253,10 @@ def package_info(self): client = TestClient() client.save({"conanfile.py": conanfile}) - client.run(f"create . -c tools.cmake.cmakedeps:new={new_value}") + client.run(f"create .") client.save({"conanfile.py": consumer, "CMakeLists.txt": cmakelists}) - client.run(f"create . -c tools.cmake.cmakedeps:new={new_value}", assert_error=True) + client.run(f"create .", assert_error=True) - assert f"Can't define an alias '{target_name}' for the root target '{target_name}' in hello/1.0" in client.out + assert (f"Can't define an alias '{target_name}' for the root " + f"target '{target_name}' in hello/1.0") in client.out diff --git a/test/functional/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps_new.py b/test/functional/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps_new.py index faaf91787fc..03e8c119942 100644 --- a/test/functional/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps_new.py +++ b/test/functional/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps_new.py @@ -71,13 +71,13 @@ def package_info(self): for requires in ("tool_requires", "requires"): consumer = textwrap.dedent(f""" from conan import ConanFile - from conan.tools.cmake import CMakeDeps, CMakeToolchain, CMake, cmake_layout + from conan.tools.cmake import CMakeConfigDeps, CMakeToolchain, CMake, cmake_layout class Consumer(ConanFile): settings = "os", "compiler", "arch", "build_type" {requires} = "mytool/0.1" def generate(self): - deps = CMakeDeps(self) + deps = CMakeConfigDeps(self) deps.generate() tc = CMakeToolchain(self) tc.generate() @@ -101,7 +101,7 @@ def build(self): """) c.save({f"consumer_{requires}/conanfile.py": consumer, f"consumer_{requires}/CMakeLists.txt": cmake}) - c.run(f"build consumer_{requires} -c tools.cmake.cmakedeps:new={new_value}") + c.run(f"build consumer_{requires}") assert "find_package(mytool)" in c.out assert "target_link_libraries(..." not in c.out assert "Conan: Target declared imported executable 'MyTool::myexe'" in c.out @@ -169,13 +169,13 @@ def package_info(self): consumer = textwrap.dedent(""" from conan import ConanFile - from conan.tools.cmake import CMakeDeps, CMakeToolchain, CMake, cmake_layout + from conan.tools.cmake import CMakeConfigDeps, CMakeToolchain, CMake, cmake_layout class Consumer(ConanFile): settings = "os", "compiler", "arch", "build_type" tool_requires = "mytool/0.1" def generate(self): - deps = CMakeDeps(self) + deps = CMakeConfigDeps(self) deps.generate() tc = CMakeToolchain(self) tc.generate() @@ -199,7 +199,7 @@ def build(self): """) c.save({"conanfile.py": consumer, "CMakeLists.txt": cmake}, clean_first=True) - c.run(f"build . -c tools.cmake.cmakedeps:new={new_value}") + c.run(f"build .") assert "Conan: Target declared imported executable 'MyTool::my1exe'" in c.out assert "Mytool1 generating out1.c!!!!!" in c.out assert "Conan: Target declared imported executable 'MyTool::my2exe'" in c.out @@ -324,9 +324,8 @@ def package_info(self): .with_settings("build_type")}) c.run("create dep") - c.run(f"install app -c tools.cmake.cmakedeps:new={new_value} -g CMakeDeps", - assert_error=True) - assert "ERROR: Error in generator 'CMakeDeps': dep/0.1: Cannot obtain 'location' " \ + c.run(f"install app -g CMakeConfigDeps", assert_error=True) + assert "ERROR: Error in generator 'CMakeConfigDeps': dep/0.1: Cannot obtain 'location' " \ "for library 'dep'" in c.out def test_custom_file_targetname(self): @@ -350,7 +349,7 @@ def package_info(self): c.run("create dep") c.run("create pkg") - c.run(f"install app -c tools.cmake.cmakedeps:new={new_value} -g CMakeDeps") + c.run(f"install app -g CMakeConfigDeps") targets_cmake = c.load("app/pkg-Targets-release.cmake") assert "find_dependency(MyDep REQUIRED CONFIG)" in targets_cmake assert 'set_property(TARGET pkg::pkg APPEND PROPERTY INTERFACE_LINK_LIBRARIES\n' \ @@ -772,7 +771,7 @@ class Matrix(ConanFile): generators = "CMakeToolchain" exports_sources = "src/*", "CMakeLists.txt" - generators = "CMakeDeps", "CMakeToolchain" + generators = "CMakeConfigDeps", "CMakeToolchain" def build(self): cmake = CMake(self) @@ -902,7 +901,7 @@ def package_info(self): class EngineHeader(ConanFile): settings = "os", "compiler", "build_type", "arch" requires = "engine/1.0" - generators = "CMakeDeps", "CMakeToolchain" + generators = "CMakeConfigDeps", "CMakeToolchain" def build(self): cmake = CMake(self) cmake.configure() @@ -921,7 +920,7 @@ def build(self): "CMakeLists.txt": cmake, "src/app.cpp": gen_function_cpp(name="main", includes=["engine"], calls=["engine"])}, clean_first=True) - c.run(f"build . -c tools.cmake.cmakedeps:new={new_value}") + c.run(f"build .") assert "Conan: Target declared imported STATIC library 'matrix::matrix'" in c.out assert "Conan: Target declared imported INTERFACE library 'engine::engine'" in c.out @@ -962,7 +961,7 @@ def package_info(self): from conan.tools.cmake import CMake, cmake_layout class EngineHeader(ConanFile): settings = "os", "compiler", "build_type", "arch" - generators = "CMakeDeps", "CMakeToolchain" + generators = "CMakeConfigDeps", "CMakeToolchain" def requirements(self): v = "1_0" if self.settings.build_type == "Debug" else "1_1" self.requires(f"engine/{v}") @@ -987,13 +986,13 @@ def build(self): "CMakeLists.txt": cmake, "src/app.cpp": gen_function_cpp(name="main", includes=["engine"], calls=["engine"])}, clean_first=True) - c.run(f"build . -c tools.cmake.cmakedeps:new={new_value}") + c.run(f"build .") assert "engine/1_1" in c.out assert "engine/1_0" not in c.out assert "Conan: Target declared imported INTERFACE library 'engine::engine'" in c.out assert "Engine 1_1!" in c.out - c.run(f"build . -c tools.cmake.cmakedeps:new={new_value} -s build_type=Debug") + c.run(f"build . -s build_type=Debug") assert "engine/1_1" not in c.out assert "engine/1_0" in c.out assert "Conan: Target declared imported INTERFACE library 'engine::engine'" in c.out @@ -1010,7 +1009,7 @@ def test_tool_requires(self): "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_settings("build_type") .with_tool_requires("tool/0.1")}) c.run("create tool") - c.run(f"install pkg -g CMakeDeps -c tools.cmake.cmakedeps:new={new_value}") + c.run(f"install pkg -g CMakeConfigDeps") assert "find_package(tool) # Optional. This is a tool-require, " \ "can't link its targets" in c.out assert "target_link_libraries" not in c.out @@ -1070,7 +1069,7 @@ def package_info(self): class Conan(ConanFile): settings = "os", "compiler", "build_type", "arch" - generators = "CMakeToolchain", "CMakeDeps" + generators = "CMakeToolchain", "CMakeConfigDeps" {requires} = "myfunctions/1.0" def build(self): @@ -1086,7 +1085,7 @@ def build(self): client.save({"conanfile.py": consumer, "CMakeLists.txt": cmakelists}, clean_first=True) - client.run(f"build . -c tools.cmake.cmakedeps:new={new_value}") + client.run(f"build .") assert "Hello myfunction!!!!" in client.out @@ -1170,7 +1169,7 @@ def package_info(self): class Consumer(ConanFile): settings = "os", "compiler", "arch", "build_type" requires = "protobuf/0.1" - generators = "CMakeToolchain", "CMakeDeps" + generators = "CMakeToolchain", "CMakeConfigDeps" def layout(self): cmake_layout(self) @@ -1209,7 +1208,7 @@ def build(self): def test_requires(self, protobuf): c = protobuf - c.run(f"build . --build=missing -c tools.cmake.cmakedeps:new={new_value}") + c.run(f"build . --build=missing") assert "Conan: Target declared imported STATIC library 'protobuf::protobuf'" in c.out assert "Conan: Target declared imported executable 'Protobuf::Protocompile'" in c.out assert "Protoc RELEASE generating out.c!!!!!" in c.out @@ -1224,7 +1223,7 @@ class Consumer(ConanFile): settings = "os", "compiler", "arch", "build_type" requires = "protobuf/0.1" tool_requires = "protobuf/0.1" - generators = "CMakeToolchain", "CMakeDeps" + generators = "CMakeToolchain", "CMakeConfigDeps" def layout(self): cmake_layout(self) @@ -1235,8 +1234,7 @@ def build(self): """) c = protobuf c.save({"conanfile.py": consumer}) - c.run("build . -s:h build_type=Debug --build=missing " - f"-c tools.cmake.cmakedeps:new={new_value}") + c.run("build . -s:h build_type=Debug --build=missing") assert "Conan: Target declared imported STATIC library 'protobuf::protobuf'" in c.out assert "Conan: Target declared imported executable 'Protobuf::Protocompile'" in c.out @@ -1250,8 +1248,7 @@ def build(self): assert "protobuf: Debug!" in c.out assert "protobuf: Release!" not in c.out - c.run("build . --build=missing " - f"-c tools.cmake.cmakedeps:new={new_value}") + c.run("build . --build=missing") assert "Conan: Target declared imported STATIC library 'protobuf::protobuf'" in c.out assert "Conan: Target declared imported executable 'Protobuf::Protocompile'" in c.out @@ -1393,13 +1390,13 @@ def test_check_c_source_compiles(self, matrix_client): consumer = textwrap.dedent(""" from conan import ConanFile - from conan.tools.cmake import CMakeDeps + from conan.tools.cmake import CMakeConfigDeps class PkgConan(ConanFile): settings = "os", "arch", "compiler", "build_type" requires = "matrix/1.0" generators = "CMakeToolchain", def generate(self): - deps = CMakeDeps(self) + deps = CMakeConfigDeps(self) deps.set_property("matrix", "cmake_additional_variables_prefixes", ["MyMatrix"]) deps.generate() """) @@ -1421,7 +1418,7 @@ def generate(self): c.save({"conanfile.py": consumer, "CMakeLists.txt": cmakelist}, clean_first=True) - c.run(f"install . -c tools.cmake.cmakedeps:new={new_value}") + c.run(f"install .") preset = "conan-default" if platform.system() == "Windows" else "conan-release" c.run_command(f"cmake --preset {preset} ") @@ -1451,7 +1448,7 @@ def package_info(self): class PkgConan(ConanFile): settings = "os", "arch", "compiler", "build_type" requires = "dep/0.1" - generators = "CMakeToolchain", "CMakeDeps" + generators = "CMakeToolchain", "CMakeConfigDeps" def build(self): deps = CMake(self) deps.configure() @@ -1466,7 +1463,7 @@ def build(self): c.save({"conanfile.py": consumer, "CMakeLists.txt": cmakelist}, clean_first=True) - c.run(f"build . -c tools.cmake.cmakedeps:new={new_value}", assert_error=not found) + c.run(f"build .", assert_error=not found) if not found: assert f"Conan: Error: 'dep' required COMPONENT '{components}' not found" in c.out @@ -1486,7 +1483,7 @@ def package_info(self): """) c.save({"conanfile.py": dep}) c.run("create .") - c.run(f"install --requires=dep/0.1 -g CMakeDeps -c tools.cmake.cmakedeps:new={new_value}") + c.run(f"install --requires=dep/0.1 -g CMakeConfigDeps") cmake = c.load("dep-config.cmake") assert 'set(dep_PACKAGE_PROVIDED_COMPONENTS MyC1 MyC2 c3)' in cmake @@ -1505,7 +1502,7 @@ def package_info(self): """) c.save({"conanfile.py": dep}) c.run("create .") - c.run(f"install --requires=dep/0.1 -g CMakeDeps -c tools.cmake.cmakedeps:new={new_value}") + c.run(f"install --requires=dep/0.1 -g CMakeConfigDeps") cmake = c.load("dep-config.cmake") assert 'set(dep_PACKAGE_PROVIDED_COMPONENTS MyCompC1 MyC2 c3)' in cmake @@ -1524,9 +1521,9 @@ def package_info(self): """) c.save({"conanfile.py": dep}) c.run("create .") - args = f"-g CMakeDeps -c tools.cmake.cmakedeps:new={new_value}" - c.run(f"install --requires=dep/0.1 {args}", assert_error=True) - assert "Error in generator 'CMakeDeps': dep/0.1 " 'cpp_info has both .exe and .libs' in c.out + c.run(f"install --requires=dep/0.1 -g CMakeConfigDeps", assert_error=True) + assert ("Error in generator 'CMakeConfigDeps': dep/0.1 " + 'cpp_info has both .exe and .libs' in c.out) def test_exe_no_location(self): c = TestClient() @@ -1540,9 +1537,9 @@ def package_info(self): """) c.save({"conanfile.py": dep}) c.run("create .") - args = f"-g CMakeDeps -c tools.cmake.cmakedeps:new={new_value}" - c.run(f"install --requires=dep/0.1 {args}", assert_error=True) - assert "Error in generator 'CMakeDeps': dep/0.1 cpp_info has .exe and no .location" in c.out + c.run(f"install --requires=dep/0.1 -g CMakeConfigDeps", assert_error=True) + assert ("Error in generator 'CMakeConfigDeps': " + "dep/0.1 cpp_info has .exe and no .location") in c.out def test_check_exe_wrong_type(self): c = TestClient() @@ -1557,8 +1554,7 @@ def package_info(self): """) c.save({"conanfile.py": dep}) c.run("create .") - args = f"-g CMakeDeps -c tools.cmake.cmakedeps:new={new_value}" - c.run(f"install --requires=dep/0.1 {args}", assert_error=True) + c.run(f"install --requires=dep/0.1 -g CMakeConfigDeps", assert_error=True) assert "dep/0.1 cpp_info incorrect .type shared-library for .exe myexe" in c.out @@ -1593,7 +1589,7 @@ def package_info(self): from conan.tools.cmake import CMake class Pkg(ConanFile): requires = "matrix/1.0" - generators = "CMakeToolchain", "CMakeDeps" + generators = "CMakeToolchain", "CMakeConfigDeps" settings = "os", "compiler", "build_type", "arch" def build(self): cmake = CMake(self) @@ -1603,7 +1599,7 @@ def build(self): "CMakeLists.txt": cmake, "subdir/CMakeLists.txt": subcmake}, clean_first=True) - c.run(f"build . -c tools.cmake.cmakedeps:new={new_value}") + c.run(f"build .") assert "find_package(matrix)" in c.out assert "target_link_libraries(... matrix::matrix)" in c.out assert "Conan: Target declared imported INTERFACE library 'matrix::matrix'" in c.out @@ -1630,7 +1626,7 @@ def test_find_package_casing_non_fallback(): from conan.tools.cmake import CMake, cmake_layout class Pkg(ConanFile): requires = "hello/1.0" - generators = "CMakeToolchain", "CMakeDeps" + generators = "CMakeToolchain", "CMakeConfigDeps" settings = "os", "compiler", "build_type", "arch" def layout(self): cmake_layout(self) @@ -1644,7 +1640,7 @@ def build(self): client.run(f"create .") client.save({"conanfile.py": consumer, "CMakeLists.txt": cmakelists}) - client.run(f"build . -c tools.cmake.cmakedeps:new={new_value}", assert_error=True) + client.run(f"build .", assert_error=True) assert 'Could not find a package configuration file provided by "HellO"' in client.out diff --git a/test/integration/cache/backup_sources_test.py b/test/integration/cache/backup_sources_test.py index c00c7de750f..f959b4c4796 100644 --- a/test/integration/cache/backup_sources_test.py +++ b/test/integration/cache/backup_sources_test.py @@ -190,8 +190,10 @@ def source(self): f"core.sources:upload_url={self.file_server.fake_url}/backups/\n" f"core.sources:exclude_urls=['{self.file_server.fake_url}/mycompanystorage/', '{self.file_server.fake_url}/mycompanystorage2/']"}) self.client.run("source .") - assert f"Sources for {self.file_server.fake_url}/internet/myfile.txt found in remote backup" in self.client.out - assert f"File {self.file_server.fake_url}/mycompanystorage/mycompanyfile.txt not found in {self.file_server.fake_url}/backups/" in self.client.out + assert (f"Sources for {self.file_server.fake_url}/internet/myfile.txt found in " + f"remote backup") in self.client.out + assert (f"File {self.file_server.fake_url}/mycompanystorage/mycompanyfile.txt not " + f"found in {self.file_server.fake_url}/backups/") in self.client.out # Ensure defaults backup folder works if it's not set in global.conf # (The rest is needed to exercise the rest of the code) @@ -199,8 +201,10 @@ def source(self): {"global.conf": f"core.sources:download_urls=['{self.file_server.fake_url}/backups/', 'origin']\n" f"core.sources:exclude_urls=['{self.file_server.fake_url}/mycompanystorage/', '{self.file_server.fake_url}/mycompanystorage2/']"}) self.client.run("source .") - assert f"Sources for {self.file_server.fake_url}/internet/myfile.txt found in remote backup" in self.client.out - assert f"File {self.file_server.fake_url}/mycompanystorage/mycompanyfile.txt not found in {self.file_server.fake_url}/backups/" in self.client.out + assert (f"Sources for {self.file_server.fake_url}/internet/myfile.txt found in " + f"remote backup") in self.client.out + assert (f"File {self.file_server.fake_url}/mycompanystorage/mycompanyfile.txt not " + f"found in {self.file_server.fake_url}/backups/") in self.client.out def test_unknown_handling(self): http_server_base_folder_internet = os.path.join(self.file_server.store, "internet") @@ -269,9 +273,11 @@ def source(self): rmdir(self.download_cache_folder) self.client.run("source .") - assert f"Sources for {self.file_server.fake_url}/internet/myfile.txt found in origin" in self.client.out + assert (f"Sources for {self.file_server.fake_url}/internet/myfile.txt found " + f"in origin") in self.client.out self.client.run("source .") - assert f"Source {self.file_server.fake_url}/internet/myfile.txt retrieved from local download cache" in self.client.out + assert (f"Source {self.file_server.fake_url}/internet/myfile.txt retrieved from " + f"local download cache") in self.client.out def test_download_origin_last(self): http_server_base_folder_internet = os.path.join(self.file_server.store, "internet") @@ -297,16 +303,18 @@ def source(self): self.client.save({"conanfile.py": conanfile}) self.client.run("create . -vv") - assert f"WARN: File {self.file_server.fake_url}/internet/myfile.txt not found in {self.file_server.fake_url}/backup/" in self.client.out - assert f"Downloaded {self.file_server.fake_url}/internet/myfile.txt from {self.file_server.fake_url}/internet/myfile.txt" + assert (f"WARN: File {self.file_server.fake_url}/internet/myfile.txt not found " + f"in {self.file_server.fake_url}/backup/") in self.client.out self.client.run("upload * -c -r=default") rmdir(self.download_cache_folder) self.client.run("source .") - assert f"Sources for {self.file_server.fake_url}/internet/myfile.txt found in remote backup" in self.client.out + assert (f"Sources for {self.file_server.fake_url}/internet/myfile.txt found in " + f"remote backup") in self.client.out self.client.run("source .") - assert f"Source {self.file_server.fake_url}/internet/myfile.txt retrieved from local download cache" in self.client.out + assert (f"Source {self.file_server.fake_url}/internet/myfile.txt retrieved from local " + f"download cache") in self.client.out def test_sources_backup_server_error_500(self): conanfile = textwrap.dedent(f""" @@ -498,7 +506,8 @@ def source(self): remove(os.path.join(http_server_base_folder_internet, "myfile.txt")) self.client.run("source .") - assert f"Sources for {self.file_server.fake_url}/internet/myfile.txt found in remote backup {self.file_server.fake_url}/backup/" in self.client.out + assert (f"Sources for {self.file_server.fake_url}/internet/myfile.txt found in remote " + f"backup {self.file_server.fake_url}/backup/") in self.client.out # And if the first one has them, prefer it before others in the list save(os.path.join(http_server_base_folder_downloader, sha256), @@ -507,7 +516,8 @@ def source(self): load(os.path.join(http_server_base_folder_backup, sha256 + ".json"))) rmdir(self.download_cache_folder) self.client.run("source .") - assert f"Sources for {self.file_server.fake_url}/internet/myfile.txt found in remote backup {self.file_server.fake_url}/downloader/" in self.client.out + assert (f"Sources for {self.file_server.fake_url}/internet/myfile.txt found in remote " + f"backup {self.file_server.fake_url}/downloader/") in self.client.out def test_list_urls_miss(self): def custom_download(this, url, *args, **kwargs): # noqa @@ -566,7 +576,8 @@ def source(self): self.client.save({"conanfile.py": conanfile}) self.client.run("create .") - assert f"Sources for {self.file_server.fake_url}/internal_error/myfile.txt found in remote backup {self.file_server.fake_url}/backup2/" in self.client.out + assert (f"Sources for {self.file_server.fake_url}/internal_error/myfile.txt " + f"found in remote backup {self.file_server.fake_url}/backup2/") in self.client.out def test_ok_when_origin_authorization_error(self): client = TestClient(default_server_user=True, light=True) @@ -591,7 +602,7 @@ def __init__(self, store=None): @staticmethod def _attach_to(app, store): # noqa @app.route("/internet/", method=["GET"]) - def get_internet_file(file): + def get_internet_file(_): return HTTPError(401, "You Are Not Allowed Here") @app.route("/downloader1/", method=["GET"]) @@ -631,11 +642,13 @@ def source(self): client.save({"conanfile.py": conanfile}) client.run("create .") - assert f"Sources for {http_server.fake_url}/internet/myfile.txt found in remote backup {http_server.fake_url}/downloader2/" in client.out + assert (f"Sources for {http_server.fake_url}/internet/myfile.txt found in remote" + f" backup {http_server.fake_url}/downloader2/") in client.out # TODO: Check better message with Authentication error message assert "failed in 'origin'" in client.out - # Now try to upload once to the first backup server. It's configured so it has write permissions but no overwrite + # Now try to upload once to the first backup server. It's configured so + # it has write permissions but no overwrite client.run("upload * -c -r=default") upload_server_contents = os.listdir(http_server_base_folder_backup1) assert sha256 in upload_server_contents @@ -674,7 +687,8 @@ def source(self): self.client.save({"conanfile.py": conanfile}) self.client.run("create .") - assert f"Sources for {self.file_server.fake_url}/internet/myfile.txt found in remote backup {self.file_server.fake_url}/backup2/" in self.client.out + assert (f"Sources for {self.file_server.fake_url}/internet/myfile.txt found in remote " + f"backup {self.file_server.fake_url}/backup2/") in self.client.out assert "sha256 hash failed for" in self.client.out def test_export_then_upload_workflow(self): @@ -811,7 +825,7 @@ def source(self): @pytest.mark.parametrize("exception", [Exception, ConanException]) @pytest.mark.parametrize("upload", [True, False]) def test_backup_source_dirty_download_handle(self, exception, upload): - def custom_download(this, *args, **kwargs): + def custom_download(this, *args, **kwargs): # noqa raise exception() http_server_base_folder_internet = os.path.join(self.file_server.store, "internet") diff --git a/test/integration/command/cache/test_cache_sign.py b/test/integration/command/cache/test_cache_sign.py index 24767d3e89b..887e5438aab 100644 --- a/test/integration/command/cache/test_cache_sign.py +++ b/test/integration/command/cache/test_cache_sign.py @@ -20,6 +20,7 @@ def verify(ref, artifacts_folder, signature_folder, files, **kwargs): pass """) + def test_pkg_sign_no_plugin(): c = TestClient() c.save({"conanfile.py": GenConanfile("pkg", "0.1")}) diff --git a/test/integration/command/info/test_graph_info_graphical.py b/test/integration/command/info/test_graph_info_graphical.py index f757368c01f..da75b6decdc 100644 --- a/test/integration/command/info/test_graph_info_graphical.py +++ b/test/integration/command/info/test_graph_info_graphical.py @@ -173,7 +173,8 @@ def test_graph_conflict_loop(): c.run("export lib_c") c.run("graph info lib_x --format=html", assert_error=True, redirect_stdout="graph.html") # checked manually - #c.open("graph.html") + # c.open("graph.html") + def test_graph_missing_error(): c = TestClient() diff --git a/test/integration/conanfile/test_finalize_method.py b/test/integration/conanfile/test_finalize_method.py index 568c9d03af7..a48fdc179ac 100644 --- a/test/integration/conanfile/test_finalize_method.py +++ b/test/integration/conanfile/test_finalize_method.py @@ -318,7 +318,6 @@ def test_test_package_uses_created_tool_which_modifies_pkgfolder(self): tc.run(f"cache check-integrity {app_layout.reference}") assert "There are corrupted artifacts" not in tc.out - def test_multiple_instances_of_finalized_package(self): tc = TestClient(light=True) tc.save({"tool/conanfile.py": GenConanfile("tool", "1.0") @@ -340,6 +339,7 @@ def test_multiple_instances_of_finalized_package(self): # Finalize folder conan output should be printed only once assert tc.out.count("Finalized folder ") == 1 + class TestRemoteFlows: @pytest.fixture diff --git a/test/integration/remote/retry_test.py b/test/integration/remote/retry_test.py index 09316cda7a3..93e9cb358e7 100644 --- a/test/integration/remote/retry_test.py +++ b/test/integration/remote/retry_test.py @@ -33,7 +33,7 @@ def __init__(self, status_code, content): self.retry = 0 self.retry_wait = 0 - def put(self, *args, **kwargs): + def put(self, *args, **kwargs): # noqa return self.response diff --git a/test/integration/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps.py b/test/integration/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps.py index fa2afa42f40..b12fffac0b0 100644 --- a/test/integration/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps.py +++ b/test/integration/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps.py @@ -4,8 +4,6 @@ from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient -new_value = "will_break_next" - def test_cmakedeps_direct_deps_paths(): c = TestClient() @@ -29,10 +27,10 @@ def package_info(self): class PkgConan(ConanFile): requires = "lib/1.0" settings = "os", "arch", "compiler", "build_type" - generators = "CMakeDeps" + generators = "CMakeConfigDeps" """) c.save({"conanfile.py": conanfile}, clean_first=True) - c.run(f"install . -c tools.cmake.cmakedeps:new={new_value}") + c.run(f"install .") cmake_paths = c.load("conan_cmakedeps_paths.cmake") assert "set(CMAKE_FIND_PACKAGE_PREFER_CONFIG ON)" in cmake_paths assert re.search(r"list\(PREPEND CMAKE_PROGRAM_PATH \".*/bin\"", cmake_paths) # default @@ -77,10 +75,10 @@ def package_info(self): class PkgConan(ConanFile): requires = "libb/1.0" settings = "os", "arch", "compiler", "build_type" - generators = "CMakeDeps" + generators = "CMakeConfigDeps" """) c.save({"conanfile.py": conanfile}, clean_first=True) - c.run(f"install . -c tools.cmake.cmakedeps:new={new_value}") + c.run(f"install .") cmake_paths = c.load("conan_cmakedeps_paths.cmake") assert re.search(r"list\(PREPEND CMAKE_PROGRAM_PATH \".*/libb.*/p/binb\"\)", cmake_paths) assert not re.search(r"list\(PREPEND CMAKE_PROGRAM_PATH /bina\"", cmake_paths) @@ -132,12 +130,12 @@ def package_info(self): class PkgConan(ConanFile): requires = "liba/1.0", "libb/1.0" settings = "os", "arch", "compiler", "build_type" - generators = "CMakeDeps" + generators = "CMakeConfigDeps" """) c.save({"conanfile.py": conanfile}, clean_first=True) # Now with a deployment - c.run(f"install . -c tools.cmake.cmakedeps:new={new_value} --deployer=full_deploy") + c.run(f"install . --deployer=full_deploy") cmake_paths = c.load("conan_cmakedeps_paths.cmake") assert 'set(libb_DIR "${CMAKE_CURRENT_LIST_DIR}/full_deploy/host/libb/1.0")' in cmake_paths assert ('set(CONAN_RUNTIME_LIB_DIRS "$<$:${CMAKE_CURRENT_LIST_DIR}' @@ -214,8 +212,7 @@ def package_info(self): c.save({"conanfile.py": conanfile}) c.run("create .") - c.run(f"install --requires=lib/system -g CMakeConfigDeps " - f"-c tools.cmake.cmakedeps:new={new_value}") + c.run(f"install --requires=lib/system -g CMakeConfigDeps") cmake = c.load("lib-Targets-release.cmake") assert "add_library(lib::lib INTERFACE IMPORTED)" in cmake assert "set_property(TARGET lib::lib APPEND PROPERTY INTERFACE_LINK_LIBRARIES\n" \ @@ -234,10 +231,10 @@ def package_info(self): c.save({"conanfile.py": conanfile, "test_package/conanfile.py": GenConanfile().with_test("pass") .with_settings("build_type") - .with_generator("CMakeDeps")}) + .with_generator("CMakeConfigDeps")}) c.run("create . --name=pkg --version=0.1") - assert "CMakeDeps: cmake_set_interface_link_directories is legacy, not necessary" in c.out - c.run(f"create . --name=pkg --version=0.1 -c tools.cmake.cmakedeps:new={new_value}") + assert "CMakeConfigDeps: cmake_set_interface_link_directories deprecated" in c.out + c.run(f"create . --name=pkg --version=0.1") assert "CMakeConfigDeps: cmake_set_interface_link_directories deprecated and invalid. " \ "The package 'package_info()' must correctly define the (CPS) information" in c.out @@ -255,8 +252,8 @@ def package_info(self): c.save({"conanfile.py": conanfile, "test_package/conanfile.py": GenConanfile().with_settings("build_type") .with_test("pass") - .with_generator("CMakeDeps")}) - c.run(f"create . --name=pkg --version=0.1 -c tools.cmake.cmakedeps:new={new_value}") + .with_generator("CMakeConfigDeps")}) + c.run(f"create . --name=pkg --version=0.1") # it doesn't break assert "find_package(pkg)" in c.out @@ -283,9 +280,9 @@ def package_info(self): "pkg/conanfile.py": conanfile, "pkg/test_package/conanfile.py": GenConanfile().with_settings("build_type") .with_test("pass") - .with_generator("CMakeDeps")}) + .with_generator("CMakeConfigDeps")}) c.run("create dep") - c.run(f"create pkg --name=pkg --version=0.1 -c tools.cmake.cmakedeps:new={new_value}") + c.run(f"create pkg --name=pkg --version=0.1") # it doesn't break assert "find_package(pkg)" in c.out @@ -303,12 +300,11 @@ def package_info(self): c.save({"dep/conanfile.py": GenConanfile("dep", "0.1"), "pkg/conanfile.py": conanfile, "pkg/test_package/conanfile.py": GenConanfile().with_settings("build_type") - .with_generator("CMakeDeps") + .with_generator("CMakeConfigDeps") .with_test("pass")}) c.run("create dep") - c.run(f"create pkg --name=pkg --version=0.1 -c tools.cmake.cmakedeps:new={new_value}", - assert_error=True) - assert ("ERROR: Error in generator 'CMakeDeps': pkg/0.1 recipe cpp_info did .requires to " + c.run(f"create pkg --name=pkg --version=0.1", assert_error=True) + assert ("ERROR: Error in generator 'CMakeConfigDeps': pkg/0.1 recipe cpp_info did .requires to " "'dep::lib' but component 'lib' not found in dep") in c.out @@ -335,7 +331,7 @@ def package_info(self): from conan import ConanFile class TestPkg(ConanFile): settings = "os", "compiler", "arch", "build_type" - generators = "VirtualRunEnv", "CMakeDeps" + generators = "VirtualRunEnv", "CMakeConfigDeps" def requirements(self): self.requires(self.tested_reference_str) @@ -346,9 +342,8 @@ def test(self): c.save({"dependent/conanfile.py": dependent_conanfile, "main/conanfile.py": conanfile, "main/test_package/conanfile.py": test_package}) - c.run("create ./dependent/ --name=dependent --version=0.1 " - f"-c tools.cmake.cmakedeps:new={new_value}") - c.run(f"create ./main/ --name=pkg --version=0.1 -c tools.cmake.cmakedeps:new={new_value}") + c.run("create ./dependent/ --name=dependent --version=0.1") + c.run(f"create ./main/ --name=pkg --version=0.1") def test_cmake_find_mode_deprecated(): @@ -365,8 +360,7 @@ def package_info(self): """) tc.save({"conanfile.py": dep}) tc.run("create .") - args = f"-g CMakeDeps -c tools.cmake.cmakedeps:new={new_value}" - tc.run(f"install --requires=dep/0.1 {args}") + tc.run(f"install --requires=dep/0.1 -g CMakeConfigDeps") assert "CMakeConfigDeps does not support module find mode" @@ -406,8 +400,7 @@ def package_info(self): """) tc.save({"conanfile.py": dep}) tc.run("create .") - args = f"-g CMakeDeps -c tools.cmake.cmakedeps:new={new_value}" - tc.run(f"install --requires=dep/0.1 {args}") + tc.run(f"install --requires=dep/0.1 -g CMakeConfigDeps") dep = tc.load("dep-Targets-release.cmake") assert "find_dependency(MyOpenMPI REQUIRED )" in dep assert "set_property(TARGET dep::dep APPEND PROPERTY INTERFACE_LINK_LIBRARIES\n" \ @@ -442,8 +435,7 @@ def package_info(self): """) tc.save({"conanfile.py": dep}) tc.run("create .") - args = f"-g CMakeDeps -c tools.cmake.cmakedeps:new={new_value}" - tc.run(f"install --requires=dep/0.1 {args}") + tc.run(f"install --requires=dep/0.1 -g CMakeConfigDeps") dep = tc.load("dep-Targets-release.cmake") assert "find_dependency(MyOpenMPI REQUIRED )" in dep assert "set_property(TARGET dep::mycomp APPEND PROPERTY INTERFACE_LINK_LIBRARIES\n" \ @@ -462,7 +454,7 @@ def test_requires_to_application(self): class TestPkg(ConanFile): settings = "os", "compiler", "arch", "build_type" - generators = "CMakeDeps", "CMakeToolchain" + generators = "CMakeConfigDeps", "CMakeToolchain" def requirements(self): self.requires(self.tested_reference_str) @@ -475,7 +467,7 @@ def test(self): "libtool/conanfile.py": conanfile, "libtool/test_package/conanfile.py": test_package}) c.run("create automake") - c.run(f"create libtool -c tools.cmake.cmakedeps:new={new_value}") + c.run(f"create libtool") targets = c.load("libtool/test_package/libtool-Targets-release.cmake") # The libtool shouldn't depend on the automake::automake target assert "automake::automake" not in targets @@ -513,8 +505,7 @@ def package_info(self): c.run("create automake") c.run("create libtool") - c.run("install --requires=libtool/0.1 -g CMakeDeps " - f"-c tools.cmake.cmakedeps:new={new_value}") + c.run("install --requires=libtool/0.1 -g CMakeConfigDeps") targets = c.load("libtool-Targets-release.cmake") # The libtool shouldn't depend on the automake::automake target assert "automake::automake" not in targets @@ -541,8 +532,7 @@ def package_info(self): "libtool/conanfile.py": conanfile}) c.run("create automake") c.run("create libtool") - c.run("install --requires=libtool/0.1 -g CMakeDeps " - f"-c tools.cmake.cmakedeps:new={new_value}") + c.run("install --requires=libtool/0.1 -g CMakeConfigDeps") targets = c.load("libtool-Targets-release.cmake") # The libtool shouldn't depend on the automake::automake target assert "automake::automake" not in targets @@ -577,8 +567,7 @@ def package_info(self): "libtool/conanfile.py": conanfile}) c.run("create automake") c.run("create libtool") - c.run("install --requires=libtool/0.1 -g CMakeDeps " - f"-c tools.cmake.cmakedeps:new={new_value}") + c.run("install --requires=libtool/0.1 -g CMakeConfigDeps") targets = c.load("libtool-Targets-release.cmake") # The libtool shouldn't depend on the automake::automake target assert "automake::myapp" not in targets @@ -600,7 +589,7 @@ def package_info(self): """), "conanfile.py": textwrap.dedent(""" from conan import ConanFile - from conan.tools.cmake import CMakeDeps, CMake + from conan.tools.cmake import CMakeConfigDeps, CMake class Pkg(ConanFile): name = "pkg" version = "1.0" @@ -609,14 +598,14 @@ class Pkg(ConanFile): requires = "dep/1.0" def generate(self): - deps = CMakeDeps(self) + deps = CMakeConfigDeps(self) deps.set_property("dep", "cmake_target_aliases", ["alias", "dep::other_name"]) deps.set_property("dep::mycomp", "cmake_target_aliases", ["component_alias", "dep::my_aliased_component"]) deps.generate() """)}) tc.run("create dep") - tc.run(f"install . -c tools.cmake.cmakedeps:new={new_value}") + tc.run(f"install .") targets_data = tc.load('dep-Targets-release.cmake') assert "add_library(dep::dep" in targets_data assert "add_library(alias" in targets_data @@ -649,7 +638,7 @@ def package_info(self): client.save({"conanfile.py": conanfile}) client.run("create .") - client.run(f"install --requires=pkg/0.1 -g CMakeDeps -c tools.cmake.cmakedeps:new={new_value} " + client.run(f"install --requires=pkg/0.1 -g CMakeConfigDeps " """-c tools.cmake.cmaketoolchain:extra_variables="{'BAR': 9}" """) target = client.load("pkg-config.cmake") assert 'set(BAR' not in target @@ -676,7 +665,7 @@ def package_info(self): client.save({"conanfile.py": conanfile}) client.run("create .") - client.run(f"install --requires=pkg/0.1 -g CMakeDeps -c tools.cmake.cmakedeps:new={new_value}") + client.run(f"install --requires=pkg/0.1 -g CMakeConfigDeps") target = client.load("pkg-Targets-release.cmake") assert 'add_library(pkg::base INTERFACE IMPORTED)' in target assert "# Requirement pkg::comp -> pkg::base (Full link: True)" in target @@ -820,7 +809,7 @@ def test_legacy_libraries(self): .with_package_file("lib/mylib2.a", "library") .with_package_info({"components": {"mypkg": {"libs": ["mylib1"]}, "lib2": {"libs": ["mylib2"]}}}) - }) + }) tc.run("create") tc.run("install --requires=mypkg/1.0 -g CMakeConfigDeps") mypkg_config = tc.load("mypkg-config.cmake") diff --git a/test/integration/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps_frameworks.py b/test/integration/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps_frameworks.py index f21e110c256..be5d443e639 100644 --- a/test/integration/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps_frameworks.py +++ b/test/integration/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps_frameworks.py @@ -5,8 +5,6 @@ from conan.test.utils.tools import TestClient -new_value = "will_break_next" - @pytest.mark.skipif(platform.system() != "Darwin", reason="Only OSX") def test_package_framework_needs_location(): @@ -40,8 +38,9 @@ def requirements(self): 'test_package/conanfile.py': test_conanfile, 'conanfile.py': conanfile }) - client.run(f"create . -c tools.cmake.cmakedeps:new={new_value}", assert_error=True) - assert "Error in generator 'CMakeConfigDeps': cpp_info.location missing for framework MyFramework" in client.out + client.run(f"create .", assert_error=True) + assert ("Error in generator 'CMakeConfigDeps': cpp_info.location" + " missing for framework MyFramework") in client.out def test_framework_only_component_generates_target(): @@ -63,6 +62,6 @@ def package_info(self): tc = TestClient() tc.save({"conanfile.py": conanfile}) tc.run("create .") - tc.run(f"install --requires=frame/1.0 -g CMakeConfigDeps -c=tools.cmake.cmakedeps:new={new_value}") + tc.run(f"install --requires=frame/1.0 -g CMakeConfigDeps") targets = tc.load("frame-Targets-release.cmake") assert "add_library(frame::frame INTERFACE IMPORTED)" in targets diff --git a/test/integration/toolchains/microsoft/test_nmaketoolchain.py b/test/integration/toolchains/microsoft/test_nmaketoolchain.py index 0cf6722b9b8..420cbde4d57 100644 --- a/test/integration/toolchains/microsoft/test_nmaketoolchain.py +++ b/test/integration/toolchains/microsoft/test_nmaketoolchain.py @@ -1,4 +1,3 @@ -import os import platform import textwrap diff --git a/test/integration/tools/system/package_manager_test.py b/test/integration/tools/system/package_manager_test.py index 883b9bd3e4f..292038d4fca 100644 --- a/test/integration/tools/system/package_manager_test.py +++ b/test/integration/tools/system/package_manager_test.py @@ -139,7 +139,7 @@ def test_tools_install_mode_check(tool_class): context_mock.return_value = "host" tool = tool_class(conanfile) with pytest.raises(ConanException) as exc_info: - def fake_check(*args, **kwargs): + def fake_check(*args, **kwargs): # noqa return ["package1", "package2"] from conan.tools.system.package_manager import _SystemPackageManagerTool with patch.object(_SystemPackageManagerTool, 'check', MagicMock(side_effect=fake_check)): @@ -196,8 +196,8 @@ def test_dnf_yum_return_code_100(tool_class, result): context_mock.return_value = "host" tool = tool_class(conanfile) - def fake_run(command, win_bash=False, subsystem=None, env=None, ignore_errors=False, - quiet=False): + def fake_run(command, win_bash=False, subsystem=None, env=None, ignore_errors=False, # noqa + quiet=False): # noqa assert command == result return 100 if "check-update" in command else 0 @@ -252,7 +252,7 @@ def test_tools_install_mode_install_different_archs(tool_class, arch_host, resul context_mock.return_value = "host" tool = tool_class(conanfile) - def fake_check(*args, **kwargs): + def fake_check(*args, **kwargs): # noqa return ["package1", "package2"] from conan.tools.system.package_manager import _SystemPackageManagerTool with patch.object(_SystemPackageManagerTool, 'check', MagicMock(side_effect=fake_check)): @@ -294,7 +294,7 @@ def test_tools_install_mode_install_different_archs_with_version(tool_class, arc context_mock.return_value = "host" tool = tool_class(conanfile) - def fake_check(*args, **kwargs): + def fake_check(*args, **kwargs): # noqa return ["package1=0.1", "package2=0.2"] from conan.tools.system.package_manager import _SystemPackageManagerTool with patch.object(_SystemPackageManagerTool, 'check', MagicMock(side_effect=fake_check)): @@ -335,7 +335,7 @@ def test_tools_install_mode_install_to_build_machine_arch(tool_class, arch_host, context_mock.return_value = "host" tool = tool_class(conanfile) - def fake_check(*args, **kwargs): + def fake_check(*args, **kwargs): # noqa return ["package1", "package2"] from conan.tools.system.package_manager import _SystemPackageManagerTool with patch.object(_SystemPackageManagerTool, 'check', MagicMock(side_effect=fake_check)): @@ -377,7 +377,7 @@ def test_tools_install_mode_install_to_build_machine_arch_with_version(tool_clas context_mock.return_value = "host" tool = tool_class(conanfile) - def fake_check(*args, **kwargs): + def fake_check(*args, **kwargs): # noqa return ["package1=0.1", "package2=0.2"] from conan.tools.system.package_manager import _SystemPackageManagerTool with patch.object(_SystemPackageManagerTool, 'check', MagicMock(side_effect=fake_check)): @@ -404,7 +404,7 @@ def test_tools_install_archless(tool_class, result): context_mock.return_value = "host" tool = tool_class(conanfile, arch_names={}) - def fake_check(*args, **kwargs): + def fake_check(*args, **kwargs): # noqa return ["package1", "package2"] from conan.tools.system.package_manager import _SystemPackageManagerTool with patch.object(_SystemPackageManagerTool, 'check', MagicMock(side_effect=fake_check)): @@ -431,7 +431,7 @@ def test_tools_install_archless_with_version(tool_class, result): context_mock.return_value = "host" tool = tool_class(conanfile, arch_names={}) - def fake_check(*args, **kwargs): + def fake_check(*args, **kwargs): # noqa return ["package1=0.1", "package2=0.2"] from conan.tools.system.package_manager import _SystemPackageManagerTool with patch.object(_SystemPackageManagerTool, 'check', MagicMock(side_effect=fake_check)): diff --git a/test/unittests/client/migrations/test_migrator.py b/test/unittests/client/migrations/test_migrator.py index f8224cff5a5..6c05cd1f0de 100644 --- a/test/unittests/client/migrations/test_migrator.py +++ b/test/unittests/client/migrations/test_migrator.py @@ -4,7 +4,7 @@ import pytest from conan.errors import ConanMigrationError -from conans.migrations import Migrator +from conan.internal.api.migrations import Migrator from conan.test.utils.test_files import temp_folder From 4f5febbdc29f2714cee875fd39c054decb289f2e Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Mon, 13 Apr 2026 09:43:11 +0200 Subject: [PATCH 088/110] Forward underlying system package manager error messages (#19858) * Check package manager in path * Simplification: just raise the shell message error to user * Single run invokation * Fix lint warnings * Improve output --- conan/tools/system/package_manager.py | 13 +++++-- .../tools/system/package_manager_test.py | 35 +++++++++++++++++-- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/conan/tools/system/package_manager.py b/conan/tools/system/package_manager.py index 412569ea0f9..b57a0d0df06 100644 --- a/conan/tools/system/package_manager.py +++ b/conan/tools/system/package_manager.py @@ -1,4 +1,5 @@ import platform +from io import StringIO from conan.tools.build import cross_building from conan.internal.graph.graph import CONTEXT_BUILD @@ -108,9 +109,17 @@ def run(self, method, *args, **kwargs): def _conanfile_run(self, command, accepted_returns, quiet=True): # When checking multiple packages, this is too noisy - ret = self._conanfile.run(command, ignore_errors=True, quiet=quiet) + # Capture output and show it only on failure. + stdout_buf = StringIO() if quiet else None + stderr_buf = StringIO() if quiet else None + ret = self._conanfile.run(command, ignore_errors=True, quiet=quiet, stdout=stdout_buf, stderr=stderr_buf) if ret not in accepted_returns: - raise ConanException("Command '%s' failed" % command) + msg = f"Command '{command}' failed with exit code {ret}" + if stderr_buf is not None and stderr_buf.getvalue(): + msg += f"\nstderr: {stderr_buf.getvalue().strip()}" + if stdout_buf is not None and stdout_buf.getvalue(): + msg += f"\nstdout: {stdout_buf.getvalue().strip()}" + raise ConanException(msg) return ret def install_substitutes(self, *args, **kwargs): diff --git a/test/integration/tools/system/package_manager_test.py b/test/integration/tools/system/package_manager_test.py index 292038d4fca..5e29e45776e 100644 --- a/test/integration/tools/system/package_manager_test.py +++ b/test/integration/tools/system/package_manager_test.py @@ -95,6 +95,35 @@ def test_conf_tool_skips_default_detection_message_on_unknown_distro(): Apt(conanfile) get_default_mock.assert_not_called() +@pytest.mark.parametrize("use_quiet_check", [True, False]) +def test_package_manager_not_found(use_quiet_check): + """Failed runs surface the shell's error (e.g. command not found) when output is captured.""" + conanfile = ConanFileMock() + conanfile.settings = Settings() + conanfile.conf.define("tools.system.package_manager:tool", "apt-get") + conanfile.conf.define("tools.system.package_manager:mode", "install") + + def fake_run(command, stdout=None, stderr=None, ignore_errors=False, env="", quiet=False, **kwargs): + if quiet and stderr is not None: + stderr.write("sh: apt-get: command not found\n") + return 127 + + conanfile.run = fake_run + with mock.patch('conan.ConanFile.context', new_callable=PropertyMock) as context_mock: + context_mock.return_value = "host" + tool = Apt(conanfile) + with pytest.raises(ConanException) as exc_info: + if use_quiet_check: + tool.check(["pkg"]) + else: + tool.install(["pkg"], check=False) + + msg = str(exc_info.value) + assert "failed with exit code 127" in msg + if use_quiet_check: + assert "stderr:" in msg + assert "sh: apt-get: command not found" in msg + @pytest.mark.parametrize("sudo, sudo_askpass, expected_str", [ (True, True, "sudo -A "), @@ -197,7 +226,7 @@ def test_dnf_yum_return_code_100(tool_class, result): tool = tool_class(conanfile) def fake_run(command, win_bash=False, subsystem=None, env=None, ignore_errors=False, # noqa - quiet=False): # noqa + quiet=False, **kwargs): # noqa assert command == result return 100 if "check-update" in command else 0 @@ -210,13 +239,13 @@ def fake_run(command, win_bash=False, subsystem=None, env=None, ignore_errors=Fa tool = tool_class(conanfile) def fake_run(command, win_bash=False, subsystem=None, env=None, ignore_errors=False, - quiet=False): + quiet=False, **kwargs): return 55 if "check-update" in command else 0 conanfile.run = fake_run with pytest.raises(ConanException) as exc_info: tool.update() - assert f"Command '{result}' failed" == str(exc_info.value) + assert f"Command '{result}' failed with exit code 55" == str(exc_info.value) @pytest.mark.parametrize("tool_class, arch_host, result", [ From e3c6499e28f73ff21b9bed74d1aba84068254bb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= <5364255+AbrilRBS@users.noreply.github.com> Date: Mon, 13 Apr 2026 11:32:41 +0200 Subject: [PATCH 089/110] Revert quote changes for nmake defines (#19859) * Revert quote changes for nmake defines * Bump patch * Fix tests --- conan/tools/microsoft/nmakedeps.py | 4 ++-- conan/tools/microsoft/nmaketoolchain.py | 2 +- test/integration/toolchains/microsoft/test_nmakedeps.py | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/conan/tools/microsoft/nmakedeps.py b/conan/tools/microsoft/nmakedeps.py index fe416885e01..b96fb064722 100644 --- a/conan/tools/microsoft/nmakedeps.py +++ b/conan/tools/microsoft/nmakedeps.py @@ -5,7 +5,7 @@ from conan.tools.env import Environment -def format_defines(defines): +def format_defines(defines, toolchain=False): def is_hex_or_numeric(s): try: # Check for Hexadecimal (base 16) @@ -22,7 +22,7 @@ def is_hex_or_numeric(s): macro, value = define.split("=", 1) if value and not is_hex_or_numeric(value): # value quotes are escaped - value = f'\\"{value}\\"' + value = f'\\"{value}\\"' if toolchain else f'\"{value}\"' define = f"{macro}#{value}" formated_defines.append(f'/D"{define}"') return formated_defines diff --git a/conan/tools/microsoft/nmaketoolchain.py b/conan/tools/microsoft/nmaketoolchain.py index c016291547e..104e5a76192 100644 --- a/conan/tools/microsoft/nmaketoolchain.py +++ b/conan/tools/microsoft/nmaketoolchain.py @@ -58,7 +58,7 @@ def _cl(self): defines.extend(self.extra_defines) return (["/nologo"] + self._format_options(bt_flags + rt_flags + cflags + cxxflags) + - format_defines(defines)) + format_defines(defines, toolchain=True)) @property def _link(self): diff --git a/test/integration/toolchains/microsoft/test_nmakedeps.py b/test/integration/toolchains/microsoft/test_nmakedeps.py index abed9daf82e..338b40bfd85 100644 --- a/test/integration/toolchains/microsoft/test_nmakedeps.py +++ b/test/integration/toolchains/microsoft/test_nmakedeps.py @@ -43,9 +43,9 @@ def package_info(self): # Checking that defines are added to CL for flag in ( r'/D"TEST_DEFINITION1"', '/D"TEST_DEFINITION2#0"', - r'/D"TEST_DEFINITION3#"', r'/D"TEST_DEFINITION4#\"foo\""', - r'/D"TEST_DEFINITION5#\"__declspec(dllexport)\""', - r'/D"TEST_DEFINITION6#\"foo bar\""', + r'/D"TEST_DEFINITION3#"', '/D"TEST_DEFINITION4#"foo""', + r'/D"TEST_DEFINITION5#"__declspec(dllexport)""', + r'/D"TEST_DEFINITION6#"foo bar""', r'/D"TEST_DEFINITION7#7"', r'/D"TEST_WINVER#0x0601"' ): From 912a4f2711847fa2f3b816dc8094a093f49eda50 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 14 Apr 2026 11:59:07 +0200 Subject: [PATCH 090/110] remove pytest version limit (#19864) * remove pytest version limit * fix --- conans/requirements_dev.txt | 2 +- .../cmake/cmakedeps/test_cmakedeps_components_names.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/conans/requirements_dev.txt b/conans/requirements_dev.txt index 7d8ca9b1989..0a3919cf382 100644 --- a/conans/requirements_dev.txt +++ b/conans/requirements_dev.txt @@ -1,4 +1,4 @@ -pytest>=7, <8.0.0 +pytest pytest-xdist # To launch in N cores with pytest -n WebTest>=3.0.0, <4 bottle diff --git a/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_components_names.py b/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_components_names.py index b9ce1acf3b1..8ac66581aac 100644 --- a/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_components_names.py +++ b/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_components_names.py @@ -10,7 +10,6 @@ from conan.test.utils.tools import TestClient -@pytest.mark.tool("cmake") @pytest.fixture(scope="module") def setup_client_with_greetings(): """ From e54f73eaba63ae9fbd55161df25003d5db3a2d25 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 14 Apr 2026 16:27:07 +0200 Subject: [PATCH 091/110] refactor for loader api-helper (#19866) --- conan/api/conan_api.py | 7 ++++++- conan/api/subapi/cache.py | 2 +- conan/api/subapi/config.py | 2 +- conan/api/subapi/export.py | 2 +- conan/api/subapi/graph.py | 8 ++++---- conan/api/subapi/local.py | 6 +++--- conan/api/subapi/report.py | 2 +- conan/api/subapi/upload.py | 4 ++-- conan/api/subapi/workspace.py | 6 +++--- conan/internal/runner/docker.py | 2 +- 10 files changed, 23 insertions(+), 18 deletions(-) diff --git a/conan/api/conan_api.py b/conan/api/conan_api.py index c0f2f63458d..0d450defb3e 100644 --- a/conan/api/conan_api.py +++ b/conan/api/conan_api.py @@ -211,6 +211,11 @@ def editable_packages(self): # These are just the global editables, not including workspace ones return self._editable_packages + @property + def loader(self): + _, _, load, _ = self.get_loader() + return load + def get_loader(self): ws_editables = self._conan_api.workspace.packages() editable_packages = self._editable_packages.update_copy(ws_editables) @@ -230,4 +235,4 @@ def get_loader(self): pyreq_loader = PyRequireLoader(proxy, range_resolver, self.global_conf) # This is caching too! loader = ConanFileLoader(pyreq_loader, conanfile_helpers) - return proxy, range_resolver, loader + return proxy, range_resolver, loader, None diff --git a/conan/api/subapi/cache.py b/conan/api/subapi/cache.py index 7e43faaff17..8bc021adde3 100644 --- a/conan/api/subapi/cache.py +++ b/conan/api/subapi/cache.py @@ -193,7 +193,7 @@ def sign(self, package_list): "information on how to configure the plugin, please read the documentation at " "https://docs.conan.io/2/reference/extensions/package_signing.html.") - _, _, loader = self._api_helpers.get_loader() + loader = self._api_helpers.loader preparator = PackagePreparator(loader, self._api_helpers.cache, self._api_helpers.remote_manager, self._api_helpers.global_conf) diff --git a/conan/api/subapi/config.py b/conan/api/subapi/config.py index d8af964b5a3..d3a4e564894 100644 --- a/conan/api/subapi/config.py +++ b/conan/api/subapi/config.py @@ -211,7 +211,7 @@ def fetch_packages(self, requires, lockfile=None, remotes=None, profile=None): remotes = conan_api.remotes.list() if remotes is None else remotes profile_host = profile_build = profile or conan_api.profiles.get_profile([]) - proxy, range_resolver, loader = self._helpers.get_loader() + proxy, range_resolver, loader, _ = self._helpers.get_loader() cache = self._helpers.cache ConanOutput().title("Fetching requested configuration packages") diff --git a/conan/api/subapi/export.py b/conan/api/subapi/export.py index ca8a74cbf93..86473e4220c 100644 --- a/conan/api/subapi/export.py +++ b/conan/api/subapi/export.py @@ -43,7 +43,7 @@ def export(self, path, name: str = None, version: str = None, user: str = None, :return: A tuple of the exported RecipeReference and a ConanFile object """ ConanOutput().title("Exporting recipe to the cache") - _, _, loader = self._helpers.get_loader() + loader = self._helpers.loader hook_manager = self._helpers.hook_manager return cmd_export(loader,self._helpers.cache, hook_manager, self._helpers.global_conf, path, name, version, user, channel, graph_lock=lockfile, remotes=remotes) diff --git a/conan/api/subapi/graph.py b/conan/api/subapi/graph.py index cfa182b0641..1caa230bbe0 100644 --- a/conan/api/subapi/graph.py +++ b/conan/api/subapi/graph.py @@ -20,7 +20,7 @@ def _load_root_consumer_conanfile(self, path, profile_host, profile_build, name=None, version=None, user=None, channel=None, update=None, remotes=None, lockfile=None, is_build_require=False): - _, _, loader = self._helpers.get_loader() + loader = self._helpers.loader if path.endswith(".py"): conanfile = loader.load_consumer(path, @@ -66,7 +66,7 @@ def load_root_test_conanfile(self, path, tested_reference, profile_host, profile # necessary for correct resolution and update of remote python_requires - _, _, loader = self._helpers.get_loader() + loader = self._helpers.loader profile_host.options.scope(tested_reference) # do not try apply lock_python_requires for test_package/conanfile.py consumer @@ -90,7 +90,7 @@ def _load_root_virtual_conanfile(self, profile_host, profile_build, requires, to python_requires=None): if not python_requires and not requires and not tool_requires: raise ConanException("Provide requires or tool_requires") - _, _, loader = self._helpers.get_loader() + loader = self._helpers.loader conanfile = loader.load_virtual(requires=requires, tool_requires=tool_requires, python_requires=python_requires, @@ -184,7 +184,7 @@ def load_graph(self, root_node, profile_host, profile_build, lockfile=None, remo assert profile_host is not None assert profile_build is not None - proxy, range_resolver, loader = self._helpers.get_loader() + proxy, range_resolver, loader, _ = self._helpers.get_loader() remotes = remotes or [] cache = self._helpers.cache diff --git a/conan/api/subapi/local.py b/conan/api/subapi/local.py index 01c572954ee..dbcc4ead3b0 100644 --- a/conan/api/subapi/local.py +++ b/conan/api/subapi/local.py @@ -70,7 +70,7 @@ def editable_add(self, path, name=None, version=None, user=None, channel=None, c :return: RecipeReference of the added package """ path = self.get_conanfile_path(path, cwd, py=True) - _, _, loader = self._helpers.get_loader() + loader = self._helpers.loader conanfile = loader.load_named(path, name, version, user, channel, remotes=remotes) if conanfile.name is None or conanfile.version is None: raise ConanException("Editable package recipe should declare its name and version") @@ -115,7 +115,7 @@ def source(self, path, name=None, version=None, user=None, channel=None, :param channel: The channel of the package. If not defined, it is taken from conanfile :param remotes: The remotes to resolve possible ``python-requires`` for this recipe if needed. """ - _, _, loader = self._helpers.get_loader() + loader = self._helpers.loader conanfile = loader.load_consumer(path, name=name, version=version, user=user, channel=channel, graph_lock=None, remotes=remotes) @@ -181,7 +181,7 @@ def test(conanfile) -> None: def inspect(self, conanfile_path, remotes, lockfile, name=None, version=None, user=None, channel=None): - _, _, loader = self._helpers.get_loader() + loader = self._helpers.loader conanfile = loader.load_named(conanfile_path, name=name, version=version, user=user, channel=channel, remotes=remotes, graph_lock=lockfile) return conanfile diff --git a/conan/api/subapi/report.py b/conan/api/subapi/report.py index 21dcd960fae..94882a13829 100644 --- a/conan/api/subapi/report.py +++ b/conan/api/subapi/report.py @@ -85,7 +85,7 @@ def _source(path_to_conanfile, reference): def _configure_source(conan_api, hook_manager, conanfile_path, ref, remotes): - _, _, loader = conan_api._api_helpers.get_loader() # noqa + loader = conan_api._api_helpers.loader # noqa conanfile = loader.load_consumer(conanfile_path, name=ref.name, version=str(ref.version), user=ref.user, channel=ref.channel, graph_lock=None, remotes=remotes) diff --git a/conan/api/subapi/upload.py b/conan/api/subapi/upload.py index c8243790b45..c18d532449d 100644 --- a/conan/api/subapi/upload.py +++ b/conan/api/subapi/upload.py @@ -35,7 +35,7 @@ def check_upstream(self, package_list: PackagesList, remote: Remote, :parameter force: If ``True``, it will skip the check and mark that all items need to be uploaded. A ``force_upload`` key will be added to the entries that will be uploaded. """ - _, _, loader = self._api_helpers.get_loader() + loader = self._api_helpers.loader for ref, _ in package_list.items(): layout = self._api_helpers.cache.recipe_layout(ref) conanfile_path = layout.conanfile() @@ -63,7 +63,7 @@ def prepare(self, package_list: PackagesList, enabled_remotes: List[Remote], if metadata and metadata != [''] and '' in metadata: raise ConanException("Empty string and patterns can not be mixed for metadata.") - _, _, loader = self._api_helpers.get_loader() + loader = self._api_helpers.loader preparator = PackagePreparator(loader, self._api_helpers.cache, self._api_helpers.remote_manager, self._api_helpers.global_conf) diff --git a/conan/api/subapi/workspace.py b/conan/api/subapi/workspace.py index 00d7adeb912..8cc88d37a9b 100644 --- a/conan/api/subapi/workspace.py +++ b/conan/api/subapi/workspace.py @@ -141,7 +141,7 @@ def packages(self): def open(self, ref, remotes, cwd=None): cwd = cwd or os.getcwd() - proxy, _, loader = self._conan_api._api_helpers.get_loader() # noqa + proxy, _, loader, _ = self._conan_api._api_helpers.get_loader() # noqa ref = RecipeReference.loads(ref) if isinstance(ref, str) else ref recipe = proxy.get_recipe(ref, remotes, update=False, check_update=False) @@ -193,7 +193,7 @@ def add(self, path, name=None, version=None, user=None, channel=None, cwd=None, """ self._check_ws() full_path = self._conan_api.local.get_conanfile_path(path, cwd, py=True) - _, _, loader = self._conan_api._api_helpers.get_loader() # noqa + loader = self._conan_api._api_helpers.loader # noqa conanfile = loader.load_named(full_path, name, version, user, channel, remotes=remotes) if conanfile.name is None or conanfile.version is None: raise ConanException("Editable package recipe should declare its name and version") @@ -329,7 +329,7 @@ def find_folder(ref): if root_class is not None: conanfile = root_class(f"{WORKSPACE_PY} base project Conanfile") # To inject things like cmd_wrapper to the consumer conanfile, so self.run() works - _, _, loader = self._conan_api._api_helpers.get_loader() # noqa + loader = self._conan_api._api_helpers.loader # noqa helpers = loader._conanfile_helpers # noqa conanfile._conan_helpers = helpers conanfile._conan_is_consumer = True diff --git a/conan/internal/runner/docker.py b/conan/internal/runner/docker.py index 01fab2cc5b9..d21ed8a021e 100644 --- a/conan/internal/runner/docker.py +++ b/conan/internal/runner/docker.py @@ -244,7 +244,7 @@ def _run_command(self, command: str, workdir: Optional[str] = None, verbose: boo return stdout_log, stderr_log def _get_volumes_and_docker_path(self) -> tuple[dict, str]: - _, _, loader = self.conan_api._api_helpers.get_loader() # noqa + loader = self.conan_api._api_helpers.loader # noqa remotes = self.conan_api.remotes.list(self.args.remote) if not self.args.no_remote else [] conanfile = loader.load_consumer(self.abs_host_path / "conanfile.py", remotes=remotes) abs_docker_base_path = Path('/') / self.docker_user_name / 'conanrunner' From 44bac65a15188209c57ef2ef6465d3bd28ea940d Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 15 Apr 2026 10:19:47 +0200 Subject: [PATCH 092/110] fix error option output (#19867) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix error option output * Apply suggestion from @AbrilRBS --------- Co-authored-by: Abril Rincón Blanco <5364255+AbrilRBS@users.noreply.github.com> --- conan/cli/commands/list.py | 2 +- test/integration/command/list/list_test.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/conan/cli/commands/list.py b/conan/cli/commands/list.py index 37c642e8380..1ab1afa7291 100644 --- a/conan/cli/commands/list.py +++ b/conan/cli/commands/list.py @@ -34,7 +34,7 @@ def print_serial(item, indent=None, color_index=None): if isinstance(item, dict): for k, v in item.items(): if isinstance(v, (str, int)): - if "error" in k.lower(): + if k.lower() in ("error", "pkgsign_error"): color = Color.BRIGHT_RED k = "ERROR" elif k.lower() == "warning": diff --git a/test/integration/command/list/list_test.py b/test/integration/command/list/list_test.py index 579485f4287..fd137526d59 100644 --- a/test/integration/command/list/list_test.py +++ b/test/integration/command/list/list_test.py @@ -995,3 +995,10 @@ def test_list_local_recipe_index(): assert "ERROR: Recipe 'pkg/0.1@a' not found" in c.out c.run("list 'pkg%0.1#a@b/c' -r=local") assert "ERROR: Recipe 'pkg%0.1' not found" in c.out + +def test_list_error_option(): + c = TestClient(default_server_user=False) + c.save({"conanfile.py": GenConanfile("pkg", "1.0").with_option("my_error_option", [1, 2])}) + c.run("create . -o my_error_option=1") + c.run("list pkg/1.0#*:*") + assert "my_error_option: 1" in c.out From c3bc19c6dc9e4fd533484b9988fc3c129fcdb099 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= <5364255+AbrilRBS@users.noreply.github.com> Date: Wed, 15 Apr 2026 10:59:29 +0200 Subject: [PATCH 093/110] Test for absolute core.sources:download_cache (#19869) --- conan/internal/rest/caching_file_downloader.py | 2 +- test/integration/cache/backup_sources_test.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/conan/internal/rest/caching_file_downloader.py b/conan/internal/rest/caching_file_downloader.py index 297a0698d21..fff4c8489af 100644 --- a/conan/internal/rest/caching_file_downloader.py +++ b/conan/internal/rest/caching_file_downloader.py @@ -54,7 +54,7 @@ def _caching_download(self, urls, file_path, # regular local shared download cache, not using Conan backup sources servers backups_urls = backups_urls or ["origin"] if download_cache_folder and not os.path.isabs(download_cache_folder): - raise ConanException("core.download:download_cache must be an absolute path") + raise ConanException("core.sources:download_cache must be an absolute path") download_cache = DownloadCache(download_cache_folder) cached_path = download_cache.source_path(sha256) diff --git a/test/integration/cache/backup_sources_test.py b/test/integration/cache/backup_sources_test.py index f959b4c4796..0b165939d8d 100644 --- a/test/integration/cache/backup_sources_test.py +++ b/test/integration/cache/backup_sources_test.py @@ -899,3 +899,19 @@ def source(self): self.client.run("upload * -c -r=default") assert "No backup sources files to upload" in self.client.out assert sha256 + ".dirty" not in os.listdir(os.path.join(self.download_cache_folder, "s")) + + def test_absolute_core_sources_conf(self): + client = TestClient(light=True) + client.save_home( + {"global.conf": f"core.sources:download_cache=relative\n" + "core.sources:download_urls=['origin']"}) + conanfile = textwrap.dedent(f""" + from conan import ConanFile + from conan.tools.files import download + class Pkg(ConanFile): + def source(self): + download(self, "badbad", "myfile.txt", sha256="sha256") + """) + client.save({"conanfile.py": conanfile}) + client.run("source .", assert_error=True) + assert "core.sources:download_cache must be an absolute path" in client.out From c2ec220462883d7a751507f543db1e2472cca09b Mon Sep 17 00:00:00 2001 From: James Date: Wed, 15 Apr 2026 12:26:13 +0200 Subject: [PATCH 094/110] api docstrings (#19871) * api docstrings * more --- conan/api/conan_api.py | 6 ++++-- conan/api/subapi/command.py | 14 ++++++++++++++ conan/api/subapi/profiles.py | 20 +++++++++++++++++++- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/conan/api/conan_api.py b/conan/api/conan_api.py index 0d450defb3e..d51d33434a7 100644 --- a/conan/api/conan_api.py +++ b/conan/api/conan_api.py @@ -71,10 +71,12 @@ def __init__(self, cache_folder=None): self.config: ConfigAPI = ConfigAPI(self, self._api_helpers) #: Used to interact with remotes self.remotes: RemotesAPI = RemotesAPI(self, self._api_helpers) - self.command = CommandAPI(self) + #: Used to call other commands + self.command: CommandAPI = CommandAPI(self) #: Used to get latest refs and list refs of recipes and packages self.list: ListAPI = ListAPI(self, self._api_helpers) - self.profiles = ProfilesAPI(self, self._api_helpers) + #: Used to process and load Conan profiles + self.profiles: ProfilesAPI = ProfilesAPI(self, self._api_helpers) #: Used to install binaries, sources, deploy packages and more self.install: InstallAPI = InstallAPI(self, self._api_helpers) self.graph = GraphAPI(self, self._api_helpers) diff --git a/conan/api/subapi/command.py b/conan/api/subapi/command.py index f0ec75a80b7..2c59d5a0c23 100644 --- a/conan/api/subapi/command.py +++ b/conan/api/subapi/command.py @@ -6,12 +6,26 @@ class CommandAPI: + """ This CommandAPI is useful to be able to launch full commands from the ConanAPI + + Sometimes some commands are built using several calls to the ConanAPI. If we want + to reuse the same functionality, then we would have to copy all that code into our + own commands. + Instead of doing that, it is possible to call Conan commands using this API, via + the ``run()`` method. + """ def __init__(self, conan_api): self._conan_api = conan_api self.cli = None def run(self, cmd): + """ Runs another Conan command via API + + :param cmd: Conan command to run. It can be either a string, or a list of strings. + :return: It will return what that command returns. Note that different commands can + return different things, so the caller needs to process it accordingly. + """ if isinstance(cmd, str): cmd = shlex.split(cmd) if isinstance(cmd, list): diff --git a/conan/api/subapi/profiles.py b/conan/api/subapi/profiles.py index 10b4d4f666a..dd679c3f53e 100644 --- a/conan/api/subapi/profiles.py +++ b/conan/api/subapi/profiles.py @@ -13,6 +13,8 @@ class ProfilesAPI: + """ This ProfilesAPI is used to list, manage and load Conan profiles + """ def __init__(self, conan_api, api_helpers): self._conan_api = conan_api @@ -77,6 +79,16 @@ def get_profile(self, profiles, settings=None, options=None, conf=None, cwd=None """ Computes a Profile as the result of aggregating all the user arguments, first it loads the "profiles", composing them in order (last profile has priority), and finally adding the individual settings, options (priority over the profiles) + + :param profiles: the list of profiles to load + :param settings: list of "key=value" settings to define the profile. Patterns allowed as + "pkg-pattern:key=value" + :param options: list of "key=value" options. Patterns allowed as "pkg-pattern:key=value" + :param conf: list of "key=value" configurations. Following "conf" definitions, patterns + are allowed as "pkg-pattern:key=value", values that are lists or dictionaries might be + allowed, and configuration operations like ``+=`` for appending are allowed. + :param cwd: the current working directory. If None, os.getcwd() will be used. + :param context: the context, "build" or "host" to which this profile belongs """ assert isinstance(profiles, list), "Please provide a list of profiles" global_conf = self._api_helpers.global_conf @@ -127,7 +139,8 @@ def get_path(self, profile, cwd=None, exists=True): def list(self): """ - List all the profiles file sin the cache + List all the profiles files in the cache + :return: an alphabetically ordered list of profile files in the default cache location """ # List is to be extended (directories should not have a trailing slash) @@ -151,6 +164,11 @@ def list(self): @staticmethod def detect(): """ + Detects a possible default profile. + + The output of this detection is not guaranteed to be complete or stable, it might + change in future releases, following the same rules as the "conan profile detect" command. + :return: an automatically detected Profile, with a "best guess" of the system settings """ profile = Profile() From de9f9c9340082e67b988d1ab7a2207f7ebc0c668 Mon Sep 17 00:00:00 2001 From: Carlos Zoido Date: Wed, 15 Apr 2026 14:10:25 +0200 Subject: [PATCH 095/110] Warn when credentials env vars are ignored due to anonymous server access (#19872) add warning --- conan/api/subapi/remotes.py | 10 +++++++ .../command/remote/test_remote_users.py | 29 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/conan/api/subapi/remotes.py b/conan/api/subapi/remotes.py index 51dd9f5be94..a15b82b0802 100644 --- a/conan/api/subapi/remotes.py +++ b/conan/api/subapi/remotes.py @@ -278,6 +278,16 @@ def user_auth(self, remote: Remote, with_user=False, force=False): return self._api_helpers.remote_manager.check_credentials(remote, force) user, token, _ = localdb.get_login(remote.url) + if not force and user is None: + remote_upper = remote.name.replace("-", "_").upper() + candidate_vars = [f"CONAN_LOGIN_USERNAME_{remote_upper}", "CONAN_LOGIN_USERNAME", + f"CONAN_PASSWORD_{remote_upper}", "CONAN_PASSWORD"] + found_vars = [v for v in candidate_vars if os.getenv(v)] + if found_vars: + ConanOutput().warning( + f"Remote '{remote.name}' accepted anonymous access. {', '.join(found_vars)} " + f"environment variables were not used. Use '--force' to force authentication." + ) return user diff --git a/test/integration/command/remote/test_remote_users.py b/test/integration/command/remote/test_remote_users.py index dfd42522875..bd913369b46 100644 --- a/test/integration/command/remote/test_remote_users.py +++ b/test/integration/command/remote/test_remote_users.py @@ -6,6 +6,7 @@ from unittest.mock import patch from conan.internal.api.remotes.localdb import LocalDB +from conan.internal.rest.rest_client_v2 import RestV2Methods from conan.test.assets.genconanfile import GenConanfile from conan.test.utils.tools import TestClient, TestServer from conan.test.utils.env import environment_update @@ -492,3 +493,31 @@ def test_auth_after_logout(self): c.run("remote auth *") assert "Remote 'default' needs authentication, obtaining credentials" in c.out assert "user: myuser" in c.out + + def test_remote_auth_env_vars_anonymous_warning(self): + """https://github.com/conan-io/conan/issues/19807 + Warn when env vars are set but server accepts anonymous access""" + server = TestServer(users={"admin": "password"}) + c = TestClient(light=True, servers={"default": server}) + + # env vars set + anonymous server → warning with var names + with patch.object(RestV2Methods, "check_credentials", return_value=None): + with environment_update({"CONAN_LOGIN_USERNAME": "admin", "CONAN_PASSWORD": "password"}): + c.run("remote auth default") + + assert "CONAN_LOGIN_USERNAME" in c.out + assert "environment variables were not used. Use '--force'" in c.out + + # env vars set + --force → authenticated, no warning + with environment_update({"CONAN_LOGIN_USERNAME": "admin", "CONAN_PASSWORD": "password"}): + c.run("remote auth default --force") + + assert "user: admin" in c.out + assert "environment variables were not used" not in c.out + + # no env vars + anonymous server → no warning + c.run("remote logout default") + with patch.object(RestV2Methods, "check_credentials", return_value=None): + c.run("remote auth default") + + assert "environment variables were not used" not in c.out From 19fe11265d5260a097bdd59e6a262f53bccc7d18 Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Thu, 16 Apr 2026 10:47:11 +0200 Subject: [PATCH 096/110] tools.build:install_strip now accepts a list of possible build systems (#19874) * tools.build:install_strip now accepts a list of possible build systems --- conan/internal/model/conf.py | 3 +- conan/tools/cmake/cmake.py | 5 ++- conan/tools/gnu/autotools.py | 6 ++- conan/tools/meson/meson.py | 7 +++- .../tools/cmake/test_cmake_install.py | 37 +++++++++++++++++++ test/unittests/tools/gnu/autotools_test.py | 17 ++++++--- test/unittests/tools/meson/test_meson.py | 20 +++++----- 7 files changed, 77 insertions(+), 18 deletions(-) diff --git a/conan/internal/model/conf.py b/conan/internal/model/conf.py index 620bd4b0f67..65bffc073d2 100644 --- a/conan/internal/model/conf.py +++ b/conan/internal/model/conf.py @@ -156,7 +156,8 @@ "tools.build:rcflags": "List of extra RC (resource compiler) flags used by different toolchains like CMakeToolchain, MSBuildToolchain and MesonToolchain", "tools.build:linker_scripts": "List of linker script files to pass to the linker used by different toolchains like CMakeToolchain, AutotoolsToolchain, and MesonToolchain", # Toolchain installation - "tools.build:install_strip": "(boolean) Strip the binaries when installing them with CMake, Meson and Autotools", + "tools.build:install_strip": "(boolean or list) True/False to strip on install for every CMake, Meson and Autotools " + "integration, or a list of 'cmake', 'meson', 'autotools' to strip only for those.", # Package ID composition "tools.info.package_id:confs": "List of existing configuration to be part of the package ID", } diff --git a/conan/tools/cmake/cmake.py b/conan/tools/cmake/cmake.py index 46e8fc8bc94..e4576fee3ba 100644 --- a/conan/tools/cmake/cmake.py +++ b/conan/tools/cmake/cmake.py @@ -249,7 +249,10 @@ def install(self, build_type=None, component=None, cli_args=None, stdout=None, s "deprecated, use 'tools.build:install_strip' instead.", warn_tag="deprecated") - do_strip = self._conanfile.conf.get("tools.build:install_strip", check_type=bool) + try: + do_strip = self._conanfile.conf.get("tools.build:install_strip", check_type=bool) + except ConanException: + do_strip = "cmake" in self._conanfile.conf.get("tools.build:install_strip", check_type=list) if do_strip or deprecated_install_strip: arg_list.append("--strip") diff --git a/conan/tools/gnu/autotools.py b/conan/tools/gnu/autotools.py index a8aa7757951..0a04a6b93cb 100644 --- a/conan/tools/gnu/autotools.py +++ b/conan/tools/gnu/autotools.py @@ -1,6 +1,7 @@ import os import re +from conan.errors import ConanException from conan.tools.build import build_jobs, cmd_args_to_string, load_toolchain_args from conan.internal.subsystems import subsystem_path, deduce_subsystem from conan.tools.files import chdir @@ -99,7 +100,10 @@ def install(self, args=None, target=None, makefile=None): """ if target is None: target = "install" - do_strip = self._conanfile.conf.get("tools.build:install_strip", check_type=bool) + try: + do_strip = self._conanfile.conf.get("tools.build:install_strip", check_type=bool) + except ConanException: + do_strip = "autotools" in self._conanfile.conf.get("tools.build:install_strip", check_type=list) if do_strip: target += "-strip" args = args if args else [] diff --git a/conan/tools/meson/meson.py b/conan/tools/meson/meson.py index bca877e6624..3cb3b8d2a9f 100644 --- a/conan/tools/meson/meson.py +++ b/conan/tools/meson/meson.py @@ -1,5 +1,6 @@ import os +from conan.errors import ConanException from conan.tools.build import build_jobs from conan.tools.meson.toolchain import MesonToolchain @@ -85,7 +86,11 @@ def install(self, cli_args=None): verbosity = self._install_verbosity if verbosity: cmd += " " + verbosity - if self._conanfile.conf.get("tools.build:install_strip", check_type=bool): + try: + do_strip = self._conanfile.conf.get("tools.build:install_strip", check_type=bool) + except ConanException: + do_strip = "meson" in self._conanfile.conf.get("tools.build:install_strip", check_type=list) + if do_strip: cmd += " --strip" if cli_args: cmd += " " + " ".join(cli_args) diff --git a/test/unittests/tools/cmake/test_cmake_install.py b/test/unittests/tools/cmake/test_cmake_install.py index 2203070dbe9..35870191bdb 100644 --- a/test/unittests/tools/cmake/test_cmake_install.py +++ b/test/unittests/tools/cmake/test_cmake_install.py @@ -1,5 +1,6 @@ import pytest +from conan.errors import ConanException from conan.internal.default_settings import default_settings_yml from conan.tools.cmake import CMake from conan.tools.cmake.presets import write_cmake_presets @@ -80,6 +81,42 @@ def test_run_install_strip(config, deprecated): assert "--strip" in conanfile.command +@pytest.mark.parametrize("value, do_raise, do_strip", + [("cmake", True, False), + ("{'cmake': True', 'meson': False}", True, False), + (None, False, False), + ("['cmake']", True, True), + ("['autotools', 'meson']", True, False)]) +def test_run_install_strip_check_conf_values(value, do_raise, do_strip): + """ + Testing that ``tools.build:install_strip`` only accepts bool or list, + and that the install/strip rule is called when the value is correct. + """ + settings = Settings.loads(default_settings_yml) + settings.os = "Linux" + settings.arch = "x86_64" + settings.build_type = "Release" + settings.compiler = "gcc" + settings.compiler.version = "11" + + conanfile = ConanFileMock() + conanfile.conf = Conf() + conanfile.conf.define("tools.build:install_strip", value) + conanfile.folders.generators = "." + conanfile.folders.set_base_generators(temp_folder()) + conanfile.settings = settings + conanfile.folders.set_base_package(temp_folder()) + write_cmake_presets(conanfile, "toolchain", "Unix Makefiles", {}) + cmake = CMake(conanfile) + if do_raise: + with pytest.raises(ConanException) as exc_info: + cmake.install() + assert "tools.build:install_strip must be a list-like object" in str(exc_info.value) + else: + cmake.install() + assert "--strip" not in conanfile.command if not do_strip else "--strip" in conanfile.command + + def test_run_install_cli_args(): """ Testing that the passing cli_args to install works diff --git a/test/unittests/tools/gnu/autotools_test.py b/test/unittests/tools/gnu/autotools_test.py index 2708e4418e3..47194817bca 100644 --- a/test/unittests/tools/gnu/autotools_test.py +++ b/test/unittests/tools/gnu/autotools_test.py @@ -37,11 +37,18 @@ def test_source_folder_works(chdir_mock): assert conanfile.command == 'autoreconf -bar foo' -@pytest.mark.parametrize("install_strip", [False, True]) -def test_install_strip(install_strip): +@pytest.mark.parametrize("install_strip, expect_strip", [ + (False, False), + (True, True), + (["autotools"], True), + (["cmake"], False), + (["meson"], False), + (["cmake", "meson", "autotools"], True), +]) +def test_install_strip(install_strip, expect_strip): """ - When the configuration `tools.build:install_strip` is set to True, - the Autotools install command should invoke the `install-strip` target. + When `tools.build:install_strip` is True or lists ``autotools``, the install command + should use the ``install-strip`` target; list values only strip for named backends. """ folder = temp_folder() os.chdir(folder) @@ -59,7 +66,7 @@ def test_install_strip(install_strip): autotools = Autotools(conanfile) autotools.install() - assert ('install-strip' in str(conanfile.command)) == install_strip + assert ('install-strip' in str(conanfile.command)) == expect_strip def test_configure_arguments(): diff --git a/test/unittests/tools/meson/test_meson.py b/test/unittests/tools/meson/test_meson.py index b0cf0e82895..cf1ee1501ce 100644 --- a/test/unittests/tools/meson/test_meson.py +++ b/test/unittests/tools/meson/test_meson.py @@ -67,13 +67,15 @@ def test_meson_to_cppstd_flag(compiler, compiler_version, cppstd, expected): assert to_cppstd_flag(ConanFileMock(), compiler, compiler_version, cppstd) == expected -def test_meson_install_strip(): - """When the configuration `tools.build:install_strip` is set to True, - the Meson install command should include the `--strip` option. - """ - c = ConfDefinition() - c.loads("tools.build:install_strip=True") - +@pytest.mark.parametrize("conf_line, expect_strip", [ + (True, True), + (False, False), + (['meson'], True), + (['meson', 'cmake'], True), + (['autotools', 'cmake'], False), +]) +def test_meson_install_strip(conf_line, expect_strip): + """``tools.build:install_strip`` as True or a list containing ``meson`` adds ``--strip``.""" settings = MockSettings({"build_type": "Release", "compiler": "gcc", "compiler.version": "7", @@ -81,7 +83,7 @@ def test_meson_install_strip(): "arch": "x86_64"}) conanfile = ConanFileMock() conanfile.settings = settings - conanfile.conf = c.get_conanfile_conf(None) + conanfile.conf.define("tools.build:install_strip", conf_line) conanfile.folders.generators = "." conanfile.folders.set_base_generators(temp_folder()) conanfile.folders.set_base_package(temp_folder()) @@ -89,7 +91,7 @@ def test_meson_install_strip(): meson = Meson(conanfile) meson.install() - assert '--strip' in str(conanfile.command) + assert ('--strip' in str(conanfile.command)) == expect_strip def test_meson_install_cli_args(): """When the `cli_args` are provided, the Meson install command should include them. From 028c0c79cfc444853982aeeca92f53370ecb0a4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= <5364255+AbrilRBS@users.noreply.github.com> Date: Thu, 16 Apr 2026 13:35:44 +0200 Subject: [PATCH 097/110] Modernize build-order tests to use order-by (#19878) * Modernize build-order tests to use order-by * Modernize build-order tests to use order-by --- .../test_install_test_build_require.py | 4 +- .../command/info/test_info_build_order.py | 40 +++++++++++-------- .../graph/ux/loop_detection_test.py | 2 +- test/integration/lockfile/test_ci.py | 18 +++++---- .../integration/lockfile/test_ci_overrides.py | 19 +++++---- .../integration/lockfile/test_ci_revisions.py | 13 +++--- test/integration/lockfile/test_options.py | 4 +- .../integration/package_id/compatible_test.py | 4 +- 8 files changed, 60 insertions(+), 44 deletions(-) diff --git a/test/integration/build_requires/test_install_test_build_require.py b/test/integration/build_requires/test_install_test_build_require.py index 830b77b9747..b79616e3134 100644 --- a/test/integration/build_requires/test_install_test_build_require.py +++ b/test/integration/build_requires/test_install_test_build_require.py @@ -217,8 +217,8 @@ def build_requirements(self): c.assert_listed_binary({"tool/1.0": (win_pkg_id, "Build")}, build=True) c.run("graph build-order --requires=tool/1.0 -s:b os=Windows -s:h os=Linux --build=* " - "--format=json", redirect_stdout="o.json") - order = json.loads(c.load("o.json")) + "--order-by=recipe --format=json", redirect_stdout="o.json") + order = json.loads(c.load("o.json"))['order'] package1 = order[0][0]["packages"][0][0] package2 = order[0][0]["packages"][1][0] assert package1["package_id"] == win_pkg_id diff --git a/test/integration/command/info/test_info_build_order.py b/test/integration/command/info/test_info_build_order.py index f21fe24d3b8..9bfeddef414 100644 --- a/test/integration/command/info/test_info_build_order.py +++ b/test/integration/command/info/test_info_build_order.py @@ -15,6 +15,7 @@ def test_info_build_order(): "consumer/conanfile.txt": "[requires]\npkg/0.1"}) c.run("export dep --name=dep --version=0.1") c.run("export pkg --name=pkg --version=0.1") + # Old legacy syntax c.run("graph build-order consumer --build=missing --format=json") bo_json = json.loads(c.stdout) @@ -76,6 +77,7 @@ def test_info_build_order(): assert bo_json["order"] == result # test html format + # old legacy syntax c.run("graph build-order consumer --build=missing --format=html") assert "" in c.stdout c.run("graph build-order consumer --order-by=recipe --build=missing --format=html") @@ -162,7 +164,7 @@ def test_info_build_order_build_require(): "consumer/conanfile.txt": "[requires]\npkg/0.1"}) c.run("export dep --name=dep --version=0.1") c.run("export pkg --name=pkg --version=0.1") - c.run("graph build-order consumer --build=missing --format=json") + c.run("graph build-order consumer --build=missing --format=json --order-by=recipe") bo_json = json.loads(c.stdout) result = [ [ @@ -209,7 +211,7 @@ def test_info_build_order_build_require(): ] ] - assert bo_json == result + assert bo_json['order'] == result def test_info_build_order_options(): @@ -226,7 +228,7 @@ def test_info_build_order_options(): c.run("export dep1 --name=dep1 --version=0.1") c.run("export dep2 --name=dep2 --version=0.1") - c.run("graph build-order consumer --build=missing --format=json") + c.run("graph build-order consumer --build=missing --format=json --order-by=recipe") bo_json = json.loads(c.stdout) result = [ [ @@ -265,7 +267,7 @@ def test_info_build_order_options(): ]]} ] ] - assert bo_json == result + assert bo_json['order'] == result def test_info_build_order_merge_multi_product(): @@ -277,8 +279,10 @@ def test_info_build_order_merge_multi_product(): c.run("export dep --name=dep --version=0.1") c.run("export pkg --name=pkg --version=0.1") c.run("export pkg --name=pkg --version=0.2") - c.run("graph build-order consumer1 --build=missing --format=json", redirect_stdout="bo1.json") - c.run("graph build-order consumer2 --build=missing --format=json", redirect_stdout="bo2.json") + c.run("graph build-order consumer1 --build=missing --format=json --order-by=recipe", + redirect_stdout="bo1.json") + c.run("graph build-order consumer2 --build=missing --format=json --order-by=recipe", + redirect_stdout="bo2.json") c.run("graph build-order-merge --file=bo1.json --file=bo2.json --format=json", redirect_stdout="bo3.json") @@ -349,7 +353,7 @@ def test_info_build_order_merge_multi_product(): ] ] - assert bo_json == result + assert bo_json['order'] == result # test that html format for build-order-merge generates something c.run("graph build-order-merge --file=bo1.json --file=bo2.json --format=html") @@ -455,9 +459,9 @@ def requirements(self): c.run("export dep --name=depwin --version=0.1") c.run("export dep --name=depnix --version=0.1") c.run("export pkg --name=pkg --version=0.1") - c.run("graph build-order consumer --format=json --build=missing -s os=Windows", + c.run("graph build-order consumer --format=json --build=missing -s os=Windows --order-by=recipe", redirect_stdout="bo_win.json") - c.run("graph build-order consumer --format=json --build=missing -s os=Linux", + c.run("graph build-order consumer --format=json --build=missing -s os=Linux --order-by=recipe", redirect_stdout="bo_nix.json") c.run("graph build-order-merge --file=bo_win.json --file=bo_nix.json --format=json", redirect_stdout="bo3.json") @@ -542,7 +546,7 @@ def requirements(self): ] ] - assert bo_json == result + assert bo_json['order'] == result def test_info_build_order_lockfile_location(): @@ -555,7 +559,7 @@ def test_info_build_order_lockfile_location(): c.run("create dep") c.run("lock create pkg --lockfile-out=myconan.lock") assert os.path.exists(os.path.join(c.current_folder, "myconan.lock")) - c.run("graph build-order pkg --lockfile=myconan.lock --lockfile-out=myconan2.lock") + c.run("graph build-order pkg --lockfile=myconan.lock --lockfile-out=myconan2.lock --order-by=recipe") assert os.path.exists(os.path.join(c.current_folder, "myconan2.lock")) @@ -627,7 +631,8 @@ def export(self): """) c.save({"conanfile.py": dep}) c.run("export .") - c.run("graph build-order --requires=dep/0.1 --format=json", assert_error=True) + c.run("graph build-order --requires=dep/0.1 --format=json --order-by=recipe", + assert_error=True) assert "ImportError" in c.out assert "It is possible that this recipe is not Conan 2.0 ready" in c.out @@ -713,7 +718,7 @@ def test_build_order_merge_reduce(self, order): def test_error_reduced(self): c = TestClient() c.save({"conanfile.py": GenConanfile("liba", "0.1")}) - c.run("graph build-order . --format=json", redirect_stdout="bo1.json") + c.run("graph build-order . --order-by=recipe --format=json", redirect_stdout="bo1.json") c.run("graph build-order . --order-by=recipe --reduce --format=json", redirect_stdout="bo2.json") c.run(f"graph build-order-merge --file=bo1.json --file=bo2.json", assert_error=True) @@ -725,6 +730,7 @@ def test_error_reduced(self): def test_error_different_orders(self): c = TestClient() c.save({"conanfile.py": GenConanfile("liba", "0.1")}) + # old syntax c.run("graph build-order . --format=json", redirect_stdout="bo1.json") c.run("graph build-order . --order-by=recipe --format=json", redirect_stdout="bo2.json") c.run("graph build-order . --order-by=configuration --format=json", @@ -873,7 +879,7 @@ def validate(self): for approach in ("--build=missing -s foo/*:compiler.cppstd=17", '--build="compatible:foo/*" --build="missing:bar/*"', '--build="missing:foo/*" --build="compatible:foo/*" --build="missing:bar/*"'): - c.run(f'graph build-order --require=foo/1.0 --require=bar/1.0 -pr:a profile {approach}') + c.run(f'graph build-order --require=foo/1.0 --require=bar/1.0 -pr:a profile {approach} --order-by=recipe') c.assert_listed_binary({"foo/1.0": ["4e2ae338231ae18d0d43b9e119404d2b2c416758", "Build"], "bar/1.0": ["5e4ffcc1ff33697a4ee96f66f0d2228ec458f25c", "Build"]}) c.assert_listed_binary({"foo/1.0": ["4e2ae338231ae18d0d43b9e119404d2b2c416758", "Build"]}, @@ -906,11 +912,11 @@ def test_build_order_path_reqs_mixed_args(): # This used not to crash in previous versions # Also make sure we can properly used them separately tc = TestClient(light=True) - tc.run("graph build-order . --requires=foo/1.0", assert_error=True) + tc.run("graph build-order . --requires=foo/1.0 --order-by=recipe", assert_error=True) assert "ERROR: --requires and --tool-requires arguments are incompatible with [path] '.' argument" in tc.out - tc.run("graph build-order --requires=foo/1.0", assert_error=True) + tc.run("graph build-order --requires=foo/1.0 --order-by=recipe", assert_error=True) assert "Package 'foo/1.0' not resolved: No remote defined" in tc.out - tc.run("graph build-order .", assert_error=True) + tc.run("graph build-order . --order-by=recipe", assert_error=True) assert "Conanfile not found" in tc.out diff --git a/test/integration/graph/ux/loop_detection_test.py b/test/integration/graph/ux/loop_detection_test.py index d83a4546dcd..8a15dc65620 100644 --- a/test/integration/graph/ux/loop_detection_test.py +++ b/test/integration/graph/ux/loop_detection_test.py @@ -42,7 +42,7 @@ def test_install_order_infinite_loop(): assert "tool/1.0 (Build) -> ['fmt/1.0']" in c.out # Graph build-order fails in the same way - c.run("graph build-order tool -pr:h=tool_profile -b=missing", + c.run("graph build-order tool -pr:h=tool_profile -b=missing --order-by=recipe", assert_error=True) assert "ERROR: There is a loop in the graph" in c.out assert "fmt/1.0 (Build) -> ['tool/1.0']" in c.out diff --git a/test/integration/lockfile/test_ci.py b/test/integration/lockfile/test_ci.py index 25e1dd1a8c8..c6393f7dbf2 100644 --- a/test/integration/lockfile/test_ci.py +++ b/test/integration/lockfile/test_ci.py @@ -324,10 +324,11 @@ def test_single_config_decentralized(client_setup): # Now lets build the application, to see everything ok c.run("graph build-order --requires=app1/0.1@ --lockfile=app1_b_changed.lock " - "--build=missing --format=json -s os=Windows", redirect_stdout="build_order.json") + "--build=missing --format=json -s os=Windows --order-by=recipe", + redirect_stdout="build_order.json") json_file = c.load("build_order.json") - to_build = json.loads(json_file) + to_build = json.loads(json_file)['order'] level0 = to_build[0] assert len(level0) == 1 pkgawin = level0[0] @@ -391,14 +392,16 @@ def test_multi_config_decentralized(client_setup): # Now lets build the application, to see everything ok, for all the configs c.run("graph build-order --requires=app1/0.1@ --lockfile=app1_win.lock " - "--build=missing --format=json -s os=Windows", redirect_stdout="app1_win.json") + "--build=missing --format=json -s os=Windows --order-by=recipe", + redirect_stdout="app1_win.json") c.run("graph build-order --requires=app1/0.1@ --lockfile=app1_nix.lock " - "--build=missing --format=json -s os=Linux", redirect_stdout="app1_nix.json") + "--build=missing --format=json -s os=Linux --order-by=recipe", + redirect_stdout="app1_nix.json") c.run("graph build-order-merge --file=app1_win.json --file=app1_nix.json" " --format=json", redirect_stdout="build_order.json") json_file = c.load("build_order.json") - to_build = json.loads(json_file) + to_build = json.loads(json_file)['order'] level0 = to_build[0] assert len(level0) == 2 pkgawin = level0[0] @@ -499,8 +502,9 @@ def test_single_config_decentralized_overrides(): assert len(lock["overrides"]) == 1 assert set(lock["overrides"]["toolc/1.0"]) == {"toolc/3.0", "toolc/2.0", None} - c.run("graph build-order pkgc --lockfile=pkgc/conan.lock --format=json --build=missing") - to_build = json.loads(c.stdout) + c.run("graph build-order pkgc --lockfile=pkgc/conan.lock --format=json --build=missing" + " --order-by=recipe") + to_build = json.loads(c.stdout)['order'] for level in to_build: for elem in level: for package in elem["packages"][0]: # assumes no dependencies between packages diff --git a/test/integration/lockfile/test_ci_overrides.py b/test/integration/lockfile/test_ci_overrides.py index a40f9153973..4c057a28c68 100644 --- a/test/integration/lockfile/test_ci_overrides.py +++ b/test/integration/lockfile/test_ci_overrides.py @@ -144,8 +144,9 @@ def test_single_config_decentralized_overrides(): assert len(lock["overrides"]) == 1 assert set(lock["overrides"]["toolc/1.0"]) == {"toolc/3.0", "toolc/2.0", None} - c.run("graph build-order pkgc --lockfile=pkgc/conan.lock --format=json --build=missing") - to_build = json.loads(c.stdout) + c.run("graph build-order pkgc --lockfile=pkgc/conan.lock --format=json --build=missing" + " --order-by=recipe") + to_build = json.loads(c.stdout)['order'] for level in to_build: for elem in level: for package in elem["packages"][0]: # assumes no dependencies between packages @@ -195,8 +196,9 @@ def test_single_config_decentralized_overrides_nested(): assert lock["overrides"] == {"libf/1.0": ["libf/3.0"], "libf/2.0": ["libf/3.0"]} - c.run("graph build-order pkga --lockfile=pkga/conan.lock --format=json --build=missing") - to_build = json.loads(c.stdout) + c.run("graph build-order pkga --lockfile=pkga/conan.lock --format=json --build=missing" + " --order-by=recipe") + to_build = json.loads(c.stdout)['order'] for level in to_build: for elem in level: ref = elem["ref"] @@ -278,8 +280,9 @@ def test_single_config_decentralized_overrides_multi(forced): else: assert set(lock["overrides"]["libf/2.0"]) == {"libf/4.0", "libf/3.0"} - c.run("graph build-order pkgc --lockfile=pkgc/conan.lock --format=json --build=missing") - to_build = json.loads(c.stdout) + c.run("graph build-order pkgc --lockfile=pkgc/conan.lock --format=json --build=missing" + " --order-by=recipe") + to_build = json.loads(c.stdout)['order'] for level in to_build: for elem in level: ref = elem["ref"] @@ -359,9 +362,9 @@ def test_single_config_decentralized_overrides_multi_replace_requires(replace_pa # overrides will be different everytime, just checking that things can be built c.run("graph build-order pkgc --lockfile=pkgc/conan.lock --format=json -pr:b=profile " - "--build=missing") + "--build=missing --order-by=recipe") - to_build = json.loads(c.stdout) + to_build = json.loads(c.stdout)['order'] for level in to_build: for elem in level: ref = elem["ref"] diff --git a/test/integration/lockfile/test_ci_revisions.py b/test/integration/lockfile/test_ci_revisions.py index 7cb8a9be7d6..27432b7634f 100644 --- a/test/integration/lockfile/test_ci_revisions.py +++ b/test/integration/lockfile/test_ci_revisions.py @@ -319,9 +319,10 @@ def test_single_config_decentralized(client_setup): # Now lets build the application, to see everything ok c.run("graph build-order --requires=app1/0.1@ --lockfile=app1_b_changed.lock " - "--build=missing --format=json -s os=Windows", redirect_stdout="build_order.json") + "--build=missing --format=json -s os=Windows --order-by=recipe", + redirect_stdout="build_order.json") json_file = c.load("build_order.json") - to_build = json.loads(json_file) + to_build = json.loads(json_file)['order'] level0 = to_build[0] assert len(level0) == 1 pkgawin = level0[0] @@ -384,14 +385,16 @@ def test_multi_config_decentralized(client_setup): # Now lets build the application, to see everything ok, for all the configs c.run("graph build-order --requires=app1/0.1@ --lockfile=app1_win.lock " - "--build=missing --format=json -s os=Windows", redirect_stdout="app1_win.json") + "--build=missing --format=json -s os=Windows --order-by=recipe", + redirect_stdout="app1_win.json") c.run("graph build-order --requires=app1/0.1@ --lockfile=app1_nix.lock " - "--build=missing --format=json -s os=Linux", redirect_stdout="app1_nix.json") + "--build=missing --format=json -s os=Linux --order-by=recipe", + redirect_stdout="app1_nix.json") c.run("graph build-order-merge --file=app1_win.json --file=app1_nix.json" " --format=json", redirect_stdout="build_order.json") json_file = c.load("build_order.json") - to_build = json.loads(json_file) + to_build = json.loads(json_file)['order'] level0 = to_build[0] assert len(level0) == 2 pkgawin = level0[0] diff --git a/test/integration/lockfile/test_options.py b/test/integration/lockfile/test_options.py index 23ecbe30623..e03b9df290b 100644 --- a/test/integration/lockfile/test_options.py +++ b/test/integration/lockfile/test_options.py @@ -34,10 +34,10 @@ class Meta(ConanFile): client.run("lock create --requires=nano/1.0@ --build=*") client.run("graph build-order --requires=nano/1.0@ " "--lockfile-out=conan.lock --build=missing " - "--format=json", redirect_stdout="build_order.json") + "--format=json --order-by=recipe", redirect_stdout="build_order.json") json_file = client.load("build_order.json") - to_build = json.loads(json_file) + to_build = json.loads(json_file)['order'] ffmpeg = to_build[0][0] ref = ffmpeg["ref"] options = " ".join(f"-o {option}" for option in ffmpeg["packages"][0][0]["options"]) diff --git a/test/integration/package_id/compatible_test.py b/test/integration/package_id/compatible_test.py index 0ae4acdd9e3..6768220937d 100644 --- a/test/integration/package_id/compatible_test.py +++ b/test/integration/package_id/compatible_test.py @@ -614,8 +614,8 @@ def validate_build(self): c.run("export libb") settings = "-s os=Windows -s compiler=gcc -s compiler.version=11 " \ "-s compiler.libcxx=libstdc++11 -s compiler.cppstd=11" - c.run(f"graph build-order --requires=libb/0.1 {settings} --format=json", assert_error=True, - redirect_stdout="build_order.json") + c.run(f"graph build-order --requires=libb/0.1 {settings} --format=json --order-by=recipe", + assert_error=True) c.assert_listed_binary({"liba/0.1": ("bb33db23c961978d08dc0cdd6bc786b45b3e5943", "Missing"), "libb/0.1": ("144910d65b27bcbf7d544201f5578555bbd0376e", "Missing")}) From 1ee0b08b61880747e6ebdf43bcc0b7f460d511d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= <5364255+AbrilRBS@users.noreply.github.com> Date: Thu, 16 Apr 2026 13:38:53 +0200 Subject: [PATCH 098/110] Allow patterns in `--update` flag (#19856) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Pattern for ref name * Simplify diff * Update help text· --- conan/cli/args.py | 6 ++++-- conan/internal/graph/proxy.py | 5 ++++- test/integration/cache/cache2_update_test.py | 7 +++---- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/conan/cli/args.py b/conan/cli/args.py index 4e4b148a506..6ea92ee82d5 100644 --- a/conan/cli/args.py +++ b/conan/cli/args.py @@ -54,10 +54,12 @@ def add_common_install_arguments(parser): help='Do not use remote, resolve exclusively in the cache') update_help = ("Will install newer versions and/or revisions in the local cache " - "for the given reference name, or all references in the graph if no argument is supplied. " + "for the given references whose name matches the given pattern, " + "or all references in the graph if no argument is supplied. " "When using version ranges, it will install the latest version that " "satisfies the range. It will update to the " - "latest revision for the resolved version range.") + "latest revision for the resolved version range. " + "The consumer pattern (&) has no effect, and users should not specify versions.") group.add_argument("-u", "--update", action="append", nargs="?", help=update_help, const="*") add_profiles_args(parser) diff --git a/conan/internal/graph/proxy.py b/conan/internal/graph/proxy.py index 910970932b3..7b5a05f6358 100644 --- a/conan/internal/graph/proxy.py +++ b/conan/internal/graph/proxy.py @@ -1,3 +1,5 @@ +from fnmatch import fnmatch + from conan.api.output import ConanOutput from conan.internal.cache.conan_reference_layout import BasicLayout from conan.internal.graph.graph import (RECIPE_DOWNLOADED, RECIPE_INCACHE, RECIPE_NEWER, @@ -180,4 +182,5 @@ def should_update_reference(reference, update): if isinstance(update, bool): return update # Legacy syntax had --update without pattern, it manifests as a "*" pattern - return any(name == "*" or reference.name == name for name in update) + return any(fnmatch(reference.name, pattern) + for pattern in update) diff --git a/test/integration/cache/cache2_update_test.py b/test/integration/cache/cache2_update_test.py index 9acb947dd84..07209372afc 100644 --- a/test/integration/cache/cache2_update_test.py +++ b/test/integration/cache/cache2_update_test.py @@ -443,7 +443,6 @@ def test_version_ranges(self): @pytest.mark.parametrize("update,result", [ - # Not a real pattern, works to support legacy syntax ["*", {"liba/1.1": "Downloaded (default)", "libb/1.1": "Downloaded (default)"}], ["libc", {"liba/1.0": "Cache", @@ -458,9 +457,9 @@ def test_version_ranges(self): "libb/1.0": "Cache"}], ["", {"liba/1.0": "Cache", "libb/1.0": "Cache"}], - # Patterns not supported, only full name match - ["lib*", {"liba/1.0": "Cache", - "libb/1.0": "Cache"}], + # Patterns supported, but only on name + ["lib*", {"liba/1.1": "Downloaded (default)", + "libb/1.1": "Downloaded (default)"}], ["liba/*", {"liba/1.0": "Cache", "libb/1.0": "Cache"}], # None only passes legacy --update without args, From ffcb0405945ebe23087dd29d2e4976079df24680 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= <5364255+AbrilRBS@users.noreply.github.com> Date: Fri, 17 Apr 2026 12:16:22 +0200 Subject: [PATCH 099/110] Remove some deprecated features (#19877) * Remove deprecated Node::dependencies method * Remove deprecated cmake_set_interface_link_directories property * Remove deprecated methods from PackagesList * Remove deprecated deploy folder in conan home * Remove deprecated message for order-by in conan graph info build-order * Remove deprecated detect_compiler method in detect api * Remove deprecated system_tools profile section * Remove deprecated boolean powershell conf values * Revert "Remove deprecated message for order-by in conan graph info build-order" This reverts commit 1c09c83f7ac63077bc8e7f4d8e26d095e158f3c9. * Last test changes * Remove deoply usage in test * Remove last true for test --- conan/api/model/list.py | 51 ------------------- conan/internal/api/detect/detect_api.py | 7 --- conan/internal/api/profile/profile_loader.py | 6 +-- conan/internal/cache/home_paths.py | 5 -- conan/internal/graph/graph.py | 7 --- conan/internal/model/conf.py | 2 +- .../cmakeconfigdeps/target_configuration.py | 7 --- .../templates/target_configuration.py | 11 +--- conan/tools/env/environment.py | 17 +------ conan/tools/microsoft/visual.py | 14 +---- .../functional/command/test_install_deploy.py | 6 +-- .../env/test_virtualenv_powershell.py | 29 ++--------- .../cache/test_home_special_char.py | 2 +- test/integration/graph/test_system_tools.py | 7 ++- .../cmakeconfigdeps/test_cmakeconfigdeps.py | 20 -------- 15 files changed, 17 insertions(+), 174 deletions(-) diff --git a/conan/api/model/list.py b/conan/api/model/list.py index 63572209e0f..8b7bd1cc9e0 100644 --- a/conan/api/model/list.py +++ b/conan/api/model/list.py @@ -246,13 +246,6 @@ def only_recipes(self) -> None: for rrev_dict in ref_dict.get("revisions", {}).values(): rrev_dict.pop("packages", None) - def add_refs(self, refs): - ConanOutput().warning("PackagesLists.add_refs() non-public, non-documented method will be " - "removed, use .add_ref() instead", warn_tag="deprecated") - # RREVS alreday come in ASCENDING order, so upload does older revisions first - for ref in refs: - self.add_ref(ref) - def add_ref(self, ref: RecipeReference) -> None: """ Adds a new RecipeReference to a package list @@ -264,13 +257,6 @@ def add_ref(self, ref: RecipeReference) -> None: if ref.timestamp: rev_dict["timestamp"] = ref.timestamp - def add_prefs(self, rrev, prefs): - ConanOutput().warning("PackageLists.add_prefs() non-public, non-documented method will be " - "removed, use .add_pref() instead", warn_tag="deprecated") - # Prevs already come in ASCENDING order, so upload does older revisions first - for p in prefs: - self.add_pref(p) - def add_pref(self, pref: PkgReference, pkg_info: dict = None) -> None: """ Add a PkgReference to an already existing RecipeReference inside a package list @@ -287,30 +273,6 @@ def add_pref(self, pref: PkgReference, pkg_info: dict = None) -> None: if pkg_info is not None: package_dict["info"] = pkg_info - def add_configurations(self, confs): - ConanOutput().warning("PackageLists.add_configurations() non-public, non-documented method " - "will be removed, use .add_pref() instead", - warn_tag="deprecated") - for pref, conf in confs.items(): - rev_dict = self.recipe_dict(pref.ref) - try: - rev_dict["packages"][pref.package_id]["info"] = conf - except KeyError: # If package_id does not exist, do nothing, only add to existing prefs - pass - - def refs(self): - ConanOutput().warning("PackageLists.refs() non-public, non-documented method will be " - "removed, use .items() instead", warn_tag="deprecated") - result = {} - for ref, ref_dict in self._data.items(): - for rrev, rrev_dict in ref_dict.get("revisions", {}).items(): - t = rrev_dict.get("timestamp") - recipe = RecipeReference.loads(f"{ref}#{rrev}") # TODO: optimize this - if t is not None: - recipe.timestamp = t - result[recipe] = rrev_dict - return result - def items(self) -> Iterable[Tuple[RecipeReference, Dict[PkgReference, Dict]]]: """Iterate over the contents of the package list. @@ -371,19 +333,6 @@ def package_dict(self, pref: PkgReference): ref_dict = self.recipe_dict(pref.ref) return ref_dict["packages"][pref.package_id]["revisions"][pref.revision] - @staticmethod - def prefs(ref, recipe_bundle): - ConanOutput().warning("PackageLists.prefs() non-public, non-documented method will be " - "removed, use .items() instead", warn_tag="deprecated") - result = {} - for package_id, pkg_bundle in recipe_bundle.get("packages", {}).items(): - prevs = pkg_bundle.get("revisions", {}) - for prev, prev_bundle in prevs.items(): - t = prev_bundle.get("timestamp") - pref = PkgReference(ref, package_id, prev, t) - result[pref] = prev_bundle - return result - def serialize(self): """ Serialize the instance to a dictionary.""" return copy.deepcopy(self._data) diff --git a/conan/internal/api/detect/detect_api.py b/conan/internal/api/detect/detect_api.py index e39b81132ac..a59b92f5229 100644 --- a/conan/internal/api/detect/detect_api.py +++ b/conan/internal/api/detect/detect_api.py @@ -522,13 +522,6 @@ def detect_gcc_compiler(compiler_exe="gcc"): return None, None, None -def detect_compiler(): - ConanOutput(scope="detect_api").warning("detect_compiler() is deprecated, " - "use detect_default_compiler()", warn_tag="deprecated") - compiler, version, _ = detect_default_compiler() - return compiler, version - - def detect_intel_compiler(compiler_exe="icx"): try: ret, out = detect_runner(f'"{compiler_exe}" --version') diff --git a/conan/internal/api/profile/profile_loader.py b/conan/internal/api/profile/profile_loader.py index addf1f71b06..1428a079b51 100644 --- a/conan/internal/api/profile/profile_loader.py +++ b/conan/internal/api/profile/profile_loader.py @@ -229,7 +229,6 @@ class _ProfileValueParser: def get_profile(profile_text, base_profile=None): # Trying to strip comments might be problematic if things contain # doc = TextINIParse(profile_text, allowed_fields=["tool_requires", - "system_tools", # DEPRECATED: platform_tool_requires "platform_requires", "platform_tool_requires", "settings", "options", "conf", "buildenv", "runenv", @@ -241,10 +240,7 @@ def get_profile(profile_text, base_profile=None): tool_requires = _ProfileValueParser._parse_tool_requires(doc) doc_platform_requires = doc.platform_requires or "" - doc_platform_tool_requires = doc.platform_tool_requires or doc.system_tools or "" - if doc.system_tools: - ConanOutput().warning("Profile [system_tools] is deprecated," - " please use [platform_tool_requires]", warn_tag="deprecated") + doc_platform_tool_requires = doc.platform_tool_requires or "" def parse_replaces(replaces): result = [RecipeReference.loads(r) for r in replaces.splitlines()] diff --git a/conan/internal/cache/home_paths.py b/conan/internal/cache/home_paths.py index d7b81820d61..e85802b5768 100644 --- a/conan/internal/cache/home_paths.py +++ b/conan/internal/cache/home_paths.py @@ -22,11 +22,6 @@ def global_conf_path(self): @property def deployers_path(self): - deploy = os.path.join(self._home, _EXTENSIONS_FOLDER, "deploy") - if os.path.exists(deploy): - ConanOutput().warning("Use 'deployers' cache folder for deployers instead of 'deploy'", - warn_tag="deprecated") - return deploy return os.path.join(self._home, _EXTENSIONS_FOLDER, "deployers") @property diff --git a/conan/internal/graph/graph.py b/conan/internal/graph/graph.py index 9af88d7b07a..56c53ce2fc6 100644 --- a/conan/internal/graph/graph.py +++ b/conan/internal/graph/graph.py @@ -73,13 +73,6 @@ def __init__(self, ref, conanfile, context, recipe=None, path=None, test=False): self.skipped_build_requires = False self.editable_output_folder = None # In case this node is editable - @property - def dependencies(self): - ConanOutput().warning("Node.dependencies is private and shouldn't be used. It is now " - "node.edges. Please fix your code, Node.dependencies will be removed " - "in future versions", warn_tag="deprecated") - return self.edges - def subgraph(self): nodes = [self] opened = [self] diff --git a/conan/internal/model/conf.py b/conan/internal/model/conf.py index 65bffc073d2..87ae9a360ea 100644 --- a/conan/internal/model/conf.py +++ b/conan/internal/model/conf.py @@ -143,7 +143,7 @@ "tools.apple:enable_bitcode": "(boolean) Enable/Disable Bitcode Apple Clang flags", "tools.apple:enable_arc": "(boolean) Enable/Disable ARC Apple Clang flags", "tools.apple:enable_visibility": "(boolean) Enable/Disable Visibility Apple Clang flags", - "tools.env.virtualenv:powershell": "If specified, it generates PowerShell launchers (.ps1). Use this configuration setting the PowerShell executable you want to use (e.g., 'powershell.exe' or 'pwsh'). Setting it to True or False is deprecated as of Conan 2.11.0.", + "tools.env.virtualenv:powershell": "If specified, it generates PowerShell launchers (.ps1). Use this configuration setting the PowerShell executable you want to use (e.g., 'powershell.exe' or 'pwsh')", "tools.env:dotenv": "(Experimental) Generate dotenv environment files", "tools.env:deactivation_mode": "(Experimental) If 'function', generate a deactivate function instead of a script to unset the environment variables", # Compilers/Flags configurations diff --git a/conan/tools/cmake/cmakeconfigdeps/target_configuration.py b/conan/tools/cmake/cmakeconfigdeps/target_configuration.py index 29f19d95da1..d7b16c21cdb 100644 --- a/conan/tools/cmake/cmakeconfigdeps/target_configuration.py +++ b/conan/tools/cmake/cmakeconfigdeps/target_configuration.py @@ -22,13 +22,6 @@ def __init__(self, cmakedeps, conanfile, require, full_cpp_info): self._full_cpp_info = full_cpp_info def content(self): - auto_link = self._cmakedeps.get_property("cmake_set_interface_link_directories", - self._conanfile, check_type=bool) - if auto_link: - out = self._cmakedeps._conanfile.output # noqa - out.warning("CMakeConfigDeps: cmake_set_interface_link_directories deprecated and " - "invalid. The package 'package_info()' must correctly define the (CPS) " - "information", warn_tag="deprecated") t = Template(self._template, trim_blocks=True, lstrip_blocks=True, undefined=jinja2.StrictUndefined) return t.render(self._context) diff --git a/conan/tools/cmake/cmakedeps/templates/target_configuration.py b/conan/tools/cmake/cmakedeps/templates/target_configuration.py index d31cb1dc118..2ed26cd04f3 100644 --- a/conan/tools/cmake/cmakedeps/templates/target_configuration.py +++ b/conan/tools/cmake/cmakedeps/templates/target_configuration.py @@ -26,22 +26,13 @@ def context(self): components_names = [(components_target_name.replace("::", "_"), components_target_name) for components_target_name in components_targets_names] - is_win = self.conanfile.settings.get_safe("os") == "Windows" - auto_link = self.cmakedeps.get_property("cmake_set_interface_link_directories", - self.conanfile, check_type=bool) - if auto_link: - out = self.cmakedeps._conanfile.output # noqa - out.warning("CMakeDeps: cmake_set_interface_link_directories is legacy, not necessary", - warn_tag="deprecated") - return {"pkg_name": self.pkg_name, "root_target_name": self.root_target_name, "config_suffix": self.config_suffix, "config": self.configuration.upper(), "deps_targets_names": ";".join(deps_targets_names), "components_names": components_names, - "configuration": self.cmakedeps.configuration, - "set_interface_link_directories": auto_link and is_win} + "configuration": self.cmakedeps.configuration} @property def template(self): diff --git a/conan/tools/env/environment.py b/conan/tools/env/environment.py index 41ed9c8f7ac..87228d727a3 100644 --- a/conan/tools/env/environment.py +++ b/conan/tools/env/environment.py @@ -52,8 +52,7 @@ def environment_wrap_command(conanfile, env_filenames, env_folder, cmd, subsyste raise ConanException("Cannot wrap command with different envs," "{} - {}".format(bats+ps1s, shs)) - powershell = conanfile.conf.get("tools.env.virtualenv:powershell") or "powershell.exe" - powershell = "powershell.exe" if powershell is True else powershell + powershell = conanfile.conf.get("tools.env.virtualenv:powershell", default="powershell.exe") if bats: launchers = " && ".join('"{}"'.format(b) for b in bats) @@ -586,19 +585,7 @@ def save_script(self, filename): is_ps1 = ext == ".ps1" else: # Need to deduce it automatically is_bat = self._subsystem == WINDOWS - try: - is_ps1 = self._conanfile.conf.get("tools.env.virtualenv:powershell", check_type=bool) - if is_ps1 is not None: - ConanOutput().warning( - "Boolean values for 'tools.env.virtualenv:powershell' are deprecated. " - "Please specify 'powershell.exe' or 'pwsh' instead, appending arguments if needed " - "(for example: 'powershell.exe -argument'). " - "To unset this configuration, use `tools.env.virtualenv:powershell=!`, which matches " - "the previous 'False' behavior.", - warn_tag="deprecated" - ) - except ConanException: - is_ps1 = self._conanfile.conf.get("tools.env.virtualenv:powershell", check_type=str) + is_ps1 = self._conanfile.conf.get("tools.env.virtualenv:powershell", check_type=str) if is_ps1: filename = filename + ".ps1" is_bat = False diff --git a/conan/tools/microsoft/visual.py b/conan/tools/microsoft/visual.py index 1c56de02a07..4343b24445c 100644 --- a/conan/tools/microsoft/visual.py +++ b/conan/tools/microsoft/visual.py @@ -166,19 +166,7 @@ def generate(self, scope="build"): create_env_script(conanfile, content, conan_vcvars_bat, scope) _create_deactivate_vcvars_file(conanfile, conan_vcvars_bat) - try: - is_ps1 = self._conanfile.conf.get("tools.env.virtualenv:powershell", check_type=bool) - if is_ps1 is not None: - ConanOutput().warning( - "Boolean values for 'tools.env.virtualenv:powershell' are deprecated. " - "Please specify 'powershell.exe' or 'pwsh' instead, appending arguments " - "if needed (for example: 'powershell.exe -argument'). " - "To unset this configuration, use `tools.env.virtualenv:powershell=!`, " - "which matches the previous 'False' behavior.", - warn_tag="deprecated" - ) - except ConanException: - is_ps1 = self._conanfile.conf.get("tools.env.virtualenv:powershell", check_type=str) + is_ps1 = self._conanfile.conf.get("tools.env.virtualenv:powershell", check_type=str) if is_ps1: content_ps1 = textwrap.dedent(rf""" if (-not $env:VSCMD_ARG_VCVARS_VER){{ diff --git a/test/functional/command/test_install_deploy.py b/test/functional/command/test_install_deploy.py index 2cb6841ab7d..b8a8ed325b0 100644 --- a/test/functional/command/test_install_deploy.py +++ b/test/functional/command/test_install_deploy.py @@ -62,7 +62,7 @@ def deploy(graph, output_folder, **kwargs): "CMakeLists.txt": cmake, "main.cpp": gen_function_cpp(name="main", includes=["matrix"], calls=["matrix"])}, clean_first=True) - pwsh = "-c tools.env.virtualenv:powershell=True" if powershell else "" + pwsh = "-c tools.env.virtualenv:powershell=powershell.exe" if powershell else "" c.run("install . -o *:shared=True " f"--deployer=deploy.py -of=mydeploy -g CMakeToolchain -g CMakeDeps {pwsh}") c.run("remove * -c") # Make sure the cache is clean, no deps there @@ -184,9 +184,9 @@ def deploy(graph, output_folder, **kwargs): conanfile = graph.root.conanfile conanfile.output.info("deploy cache!!") """) - save(os.path.join(c.cache_folder, "extensions", "deploy", "deploy_cache.py"), deploy_cache) + save(os.path.join(c.cache_folder, "extensions", "deployers", "deploy_cache.py"), deploy_cache) # This should never be called in this test, always the local is found first - save(os.path.join(c.cache_folder, "extensions", "deploy", "mydeploy.py"), "CRASH!!!!") + save(os.path.join(c.cache_folder, "extensions", "deployers", "mydeploy.py"), "CRASH!!!!") c.save({"conanfile.txt": "", "mydeploy.py": deploy1, "sub/mydeploy2.py": deploy2}) diff --git a/test/functional/toolchains/env/test_virtualenv_powershell.py b/test/functional/toolchains/env/test_virtualenv_powershell.py index 3041c3a6fa9..d5288a644fa 100644 --- a/test/functional/toolchains/env/test_virtualenv_powershell.py +++ b/test/functional/toolchains/env/test_virtualenv_powershell.py @@ -73,7 +73,7 @@ def build(self): @pytest.mark.skipif(platform.system() != "Windows", reason="Requires Windows powershell") -@pytest.mark.parametrize("powershell", [True, "powershell.exe", "pwsh"]) +@pytest.mark.parametrize("powershell", ["powershell.exe", "pwsh"]) def test_virtualenv_test_package(powershell): """ The test_package could crash if not cleaning correctly the test_package output folder. This will still crassh if the layout is not creating different build folders @@ -119,7 +119,7 @@ def test(self): assert "MYVC_CUSTOMVAR2=PATATA2" in client.out @pytest.mark.skipif(platform.system() != "Windows", reason="Requires Windows powershell") -@pytest.mark.parametrize("powershell", [True, "powershell.exe", "pwsh"]) +@pytest.mark.parametrize("powershell", ["powershell.exe", "pwsh"]) def test_vcvars(powershell): client = TestClient() conanfile = textwrap.dedent(r""" @@ -164,7 +164,7 @@ def build(self): @pytest.mark.skipif(platform.system() != "Windows", reason="Test for powershell") -@pytest.mark.parametrize("powershell", [True, "powershell.exe", "pwsh", "powershell.exe -NoProfile", "pwsh -NoProfile"]) +@pytest.mark.parametrize("powershell", ["powershell.exe", "pwsh", "powershell.exe -NoProfile", "pwsh -NoProfile"]) def test_concatenate_build_and_run_env(powershell): # this tests that if we have both build and run env, they are concatenated correctly when using # powershell @@ -211,29 +211,8 @@ class Pkg(ConanFile): assert "MYTOOL 1!!" in client.out -@pytest.mark.parametrize("powershell", [None, True, False]) -def test_powershell_deprecated_message(powershell): - client = TestClient(light=True) - conanfile = textwrap.dedent("""\ - from conan import ConanFile - class Pkg(ConanFile): - settings = "os" - name = "pkg" - version = "0.1" - """) - - client.save({"conanfile.py": conanfile}) - powershell_arg = f'-c tools.env.virtualenv:powershell={powershell}' if powershell is not None else "" - client.run(f'install . {powershell_arg}') - # only show message if the value is set to False or True if not set do not show message - if powershell is not None: - assert "Boolean values for 'tools.env.virtualenv:powershell' are deprecated" in client.out - else: - assert "Boolean values for 'tools.env.virtualenv:powershell' are deprecated" not in client.out - - @pytest.mark.skipif(platform.system() != "Windows", reason="Test for powershell") -@pytest.mark.parametrize("powershell", [True, "pwsh", "powershell.exe"]) +@pytest.mark.parametrize("powershell", ["pwsh", "powershell.exe"]) def test_powershell_quoting(powershell): client = TestClient(path_with_spaces=False) conanfile = textwrap.dedent("""\ diff --git a/test/integration/cache/test_home_special_char.py b/test/integration/cache/test_home_special_char.py index b6234c304cd..843d31e6664 100644 --- a/test/integration/cache/test_home_special_char.py +++ b/test/integration/cache/test_home_special_char.py @@ -66,6 +66,6 @@ def test_reuse_buildenv(client_with_special_chars): @pytest.mark.skipif(platform.system() != "Windows", reason="powershell only win") def test_reuse_buildenv_powershell(client_with_special_chars): c = client_with_special_chars - c.run("create . -c tools.env.virtualenv:powershell=True") + c.run("create . -c tools.env.virtualenv:powershell=powershell.exe") assert _path_chars in c.out assert "MYTOOL WORKS!!" in c.out diff --git a/test/integration/graph/test_system_tools.py b/test/integration/graph/test_system_tools.py index 5526bb9c267..623449d66f1 100644 --- a/test/integration/graph/test_system_tools.py +++ b/test/integration/graph/test_system_tools.py @@ -18,16 +18,15 @@ def test_system_tool_require(self, revision): assert f"tool/1.0{revision or '#platform'} - Platform" in client.out def test_system_tool_require_non_matching(self): - """ if what is specified in [system_tool_require] doesn't match what the recipe requires, then - the system_tool_require will not be used, and the recipe will use its declared version + """ if what is specified in [platform_tool_requires] doesn't match what the recipe requires, then + the platform_tool_requires will not be used, and the recipe will use its declared version """ client = TestClient(light=True) client.save({"tool/conanfile.py": GenConanfile("tool", "1.0"), "conanfile.py": GenConanfile("pkg", "1.0").with_tool_requires("tool/1.0"), - "profile": "[system_tools]\ntool/1.1"}) + "profile": "[platform_tool_requires]\ntool/1.1"}) client.run("create tool") client.run("create . -pr=profile") - assert "WARN: deprecated: Profile [system_tools] is deprecated" in client.out assert "tool/1.0#60ed6e65eae112df86da7f6d790887fd - Cache" in client.out @pytest.mark.parametrize("revision", ["", "#myrev"]) diff --git a/test/integration/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps.py b/test/integration/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps.py index b12fffac0b0..262050ac99a 100644 --- a/test/integration/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps.py +++ b/test/integration/toolchains/cmake/cmakeconfigdeps/test_cmakeconfigdeps.py @@ -219,26 +219,6 @@ def package_info(self): ' $<$:my_system_cool_lib>)' in cmake -def test_autolink_pragma(): - """https://github.com/conan-io/conan/issues/10837""" - c = TestClient() - conanfile = textwrap.dedent(""" - from conan import ConanFile - class Pkg(ConanFile): - def package_info(self): - self.cpp_info.set_property("cmake_set_interface_link_directories", True) - """) - c.save({"conanfile.py": conanfile, - "test_package/conanfile.py": GenConanfile().with_test("pass") - .with_settings("build_type") - .with_generator("CMakeConfigDeps")}) - c.run("create . --name=pkg --version=0.1") - assert "CMakeConfigDeps: cmake_set_interface_link_directories deprecated" in c.out - c.run(f"create . --name=pkg --version=0.1") - assert "CMakeConfigDeps: cmake_set_interface_link_directories deprecated and invalid. " \ - "The package 'package_info()' must correctly define the (CPS) information" in c.out - - def test_consuming_cpp_info_with_components_dependency_from_same_package(): c = TestClient() conanfile = textwrap.dedent(""" From 49f72a43d2acdb38b63ccc8b3f4f157d671d5d72 Mon Sep 17 00:00:00 2001 From: James Date: Fri, 17 Apr 2026 16:43:56 +0200 Subject: [PATCH 100/110] Refactor/sources caching downloaded (#19789) * refactor, tests not passing yet * wip * wip * wip * refactor --- .../internal/rest/caching_file_downloader.py | 162 ++++++++---------- test/integration/cache/backup_sources_test.py | 27 +-- 2 files changed, 85 insertions(+), 104 deletions(-) diff --git a/conan/internal/rest/caching_file_downloader.py b/conan/internal/rest/caching_file_downloader.py index fff4c8489af..f64d863d8a8 100644 --- a/conan/internal/rest/caching_file_downloader.py +++ b/conan/internal/rest/caching_file_downloader.py @@ -29,101 +29,78 @@ def __init__(self, conanfile): def download(self, urls, file_path, retry, retry_wait, verify_ssl, auth, headers, md5, sha1, sha256): download_cache_folder = self._global_conf.get("core.sources:download_cache") - backups_urls = self._global_conf.get("core.sources:download_urls", check_type=list) - if not (backups_urls or download_cache_folder) or not sha256: - # regular, non backup/caching download - if backups_urls or download_cache_folder: - self._output.warning("Cannot cache download() without sha256 checksum") - self._download_from_urls(urls, file_path, retry, retry_wait, verify_ssl, auth, headers, - md5, sha1, sha256) - else: - self._caching_download(urls, file_path, - retry, retry_wait, verify_ssl, auth, headers, md5, sha1, sha256, - download_cache_folder, backups_urls) - - def _caching_download(self, urls, file_path, - retry, retry_wait, verify_ssl, auth, headers, md5, sha1, sha256, - download_cache_folder, backups_urls): - """ - this download will first check in the local cache, if not there, it will go to the list - of backup_urls defined by user conf (by default ["origin"]), and iterate it until - something is found. - """ - # We are going to use the download_urls definition for backups - download_cache_folder = download_cache_folder or HomePaths(self._home_folder).default_sources_backup_folder - # regular local shared download cache, not using Conan backup sources servers - backups_urls = backups_urls or ["origin"] + source_origins = self._global_conf.get("core.sources:download_urls", check_type=list) + if source_origins and not download_cache_folder: + # If backups are defined, but the download cache is not defined, use a default one + download_cache_folder = HomePaths(self._home_folder).default_sources_backup_folder if download_cache_folder and not os.path.isabs(download_cache_folder): raise ConanException("core.sources:download_cache must be an absolute path") - - download_cache = DownloadCache(download_cache_folder) - cached_path = download_cache.source_path(sha256) - with download_cache.lock(sha256): - remove_if_dirty(cached_path) - - if os.path.exists(cached_path): - self._output.info(f"Source {urls} retrieved from local download cache") - else: - with set_dirty_context_manager(cached_path): - if None in backups_urls: - raise ConanException("Trying to download sources from None backup remote." - f" Remotes were: {backups_urls}") - for backup_url in backups_urls: - is_last = backup_url is backups_urls[-1] - if backup_url == "origin": # recipe defined URLs - if self._origin_download(urls, cached_path, retry, retry_wait, - verify_ssl, auth, headers, md5, sha1, sha256, - is_last): - break - else: - if self._backup_download(backup_url, backups_urls, sha256, cached_path, - urls, is_last): - break - - download_cache.update_backup_sources_json(cached_path, self._conanfile, urls) - # Everything good, file in the cache, just copy it to final destination - mkdir(os.path.dirname(file_path)) - shutil.copy2(cached_path, file_path) - - def _origin_download(self, urls, cached_path, retry, retry_wait, - verify_ssl, auth, headers, md5, sha1, sha256, is_last): - """ download from the internet, the urls provided by the recipe (mirrors). - """ - try: - self._download_from_urls(urls, cached_path, retry, retry_wait, - verify_ssl, auth, headers, md5, sha1, sha256) - except ConanException as e: - if is_last: - raise - else: - # TODO: Improve printing of AuthenticationException - self._output.warning(f"Sources for {urls} failed in 'origin': {e}") - self._output.warning("Checking backups") + if download_cache_folder and not sha256: + self._output.warning("Cannot cache download() without sha256 checksum") + download_cache_folder = None # Cannot cache + source_origins = source_origins or ["origin"] + if None in source_origins: + raise ConanException(f"Incorrect 'core.sources:download_urls' contains invalid 'None'" + f"url: {source_origins}") + + # First, see if it is already in the download cache + if download_cache_folder: + download_cache = DownloadCache(download_cache_folder) + download_path = download_cache.source_path(sha256) + with download_cache.lock(sha256): + remove_if_dirty(download_path) + + if os.path.exists(download_path): + self._output.info(f"Source {urls} retrieved from local download cache") + else: + # not in cache, we need to actually download from internet or backup servers + with set_dirty_context_manager(download_path): + self._do_download(source_origins, urls, download_path, retry, retry_wait, + verify_ssl, auth, headers, md5, sha1, sha256) + + # copy it to the package "source" folder + os.makedirs(os.path.dirname(file_path), exist_ok=True) + shutil.copy2(download_path, file_path) + download_cache.update_backup_sources_json(download_path, self._conanfile, urls) else: - if not is_last: - self._output.info(f"Sources for {urls} found in origin") - return True - - def _backup_download(self, backup_url, backups_urls, sha256, cached_path, urls, is_last): - """ download from a Conan backup sources file server, like an Artifactory generic repo - All failures are bad, except NotFound. The server must be live, working and auth, we - don't want silently skipping a backup because it is down. - """ - try: - backup_url = backup_url if backup_url.endswith("/") else backup_url + "/" - self._file_downloader.download(backup_url + sha256, cached_path, sha256=sha256) - self._file_downloader.download(backup_url + sha256 + ".json", cached_path + ".json") - self._output.info(f"Sources for {urls} found in remote backup {backup_url}") - return True - except NotFoundException: - if is_last: - raise NotFoundException(f"File {urls} not found in {backups_urls}") - else: - self._output.warning(f"File {urls} not found in {backup_url}") - except (AuthenticationException, ForbiddenException) as e: - raise ConanException(f"Authentication to source backup server '{backup_url}' " - f"failed: {e}. " - f"Please check your 'source_credentials.json'") + # This doesn't need to be dirty-protected, as the full "source" folder is protected + self._do_download(source_origins, urls, file_path, retry, retry_wait, verify_ssl, auth, + headers, md5, sha1, sha256) + + def _do_download(self, source_origins, urls, download_path, retry, retry_wait, verify_ssl, + auth, headers, md5, sha1, sha256): + # iterates the origins until one works + for backup_url in source_origins: + if backup_url == "origin": # download from the internet + try: + self._download_from_urls(urls, download_path, retry, retry_wait, verify_ssl, + auth, headers, md5, sha1, sha256) + return + except Exception as e: + if backup_url is source_origins[-1]: + raise + self._output.warning(f"Sources for {urls} failed in 'origin': {e}") + else: # Download from a backup server + try: + self._output.info(f"Checking backup: {backup_url}") + backup_url = backup_url if backup_url.endswith("/") else backup_url + "/" + # The download happens to the user download folder, not to the download cache + self._file_downloader.download(backup_url + sha256, download_path, + sha256=sha256, overwrite=True) + self._file_downloader.download(backup_url + sha256 + ".json", + download_path + ".json", overwrite=True) + self._output.info(f"Sources for {urls} found in remote backup {backup_url}") + return + except NotFoundException: + msg = f"Sources for {urls} not found in remote backup {backup_url}" + if backup_url is source_origins[-1]: + raise NotFoundException(msg) + else: + self._output.warning(msg) + except (AuthenticationException, ForbiddenException) as e: + raise ConanException(f"Authentication to source backup server '{backup_url}' " + f"failed: {e}. " + f"Please check your 'source_credentials.json'") def _download_from_urls(self, urls, file_path, retry, retry_wait, verify_ssl, auth, headers, md5, sha1, sha256): @@ -142,6 +119,7 @@ def _download_from_urls(self, urls, file_path, retry, retry_wait, verify_ssl, au else: self._file_downloader.download(url, file_path, retry, retry_wait, verify_ssl, auth, True, headers, md5, sha1, sha256) + self._output.info(f"Sources correctly downloaded from {url}") return # Success! Return to caller except Exception as error: if url != urls[-1]: # If it is not the last one, do not raise, warn and move to next diff --git a/test/integration/cache/backup_sources_test.py b/test/integration/cache/backup_sources_test.py index 0b165939d8d..4862907a75d 100644 --- a/test/integration/cache/backup_sources_test.py +++ b/test/integration/cache/backup_sources_test.py @@ -145,7 +145,7 @@ def source(self): self.client.save({"conanfile.py": conanfile}) self.client.run("create .", assert_error=True) - assert "Trying to download sources from None backup remote" in self.client.out + assert "Incorrect 'core.sources:download_urls' contains invalid 'None'" in self.client.out self.client.save_home( {"global.conf": f"core.sources:download_cache={self.download_cache_folder}\n" @@ -190,10 +190,11 @@ def source(self): f"core.sources:upload_url={self.file_server.fake_url}/backups/\n" f"core.sources:exclude_urls=['{self.file_server.fake_url}/mycompanystorage/', '{self.file_server.fake_url}/mycompanystorage2/']"}) self.client.run("source .") + assert (f"Sources for {self.file_server.fake_url}/internet/myfile.txt found in " f"remote backup") in self.client.out - assert (f"File {self.file_server.fake_url}/mycompanystorage/mycompanyfile.txt not " - f"found in {self.file_server.fake_url}/backups/") in self.client.out + assert (f"Sources for {self.file_server.fake_url}/mycompanystorage/mycompanyfile.txt not " + f"found in remote backup {self.file_server.fake_url}/backups/") in self.client.out # Ensure defaults backup folder works if it's not set in global.conf # (The rest is needed to exercise the rest of the code) @@ -203,8 +204,8 @@ def source(self): self.client.run("source .") assert (f"Sources for {self.file_server.fake_url}/internet/myfile.txt found in " f"remote backup") in self.client.out - assert (f"File {self.file_server.fake_url}/mycompanystorage/mycompanyfile.txt not " - f"found in {self.file_server.fake_url}/backups/") in self.client.out + assert (f"Sources for {self.file_server.fake_url}/mycompanystorage/mycompanyfile.txt not " + f"found in remote backup {self.file_server.fake_url}/backups/") in self.client.out def test_unknown_handling(self): http_server_base_folder_internet = os.path.join(self.file_server.store, "internet") @@ -273,8 +274,8 @@ def source(self): rmdir(self.download_cache_folder) self.client.run("source .") - assert (f"Sources for {self.file_server.fake_url}/internet/myfile.txt found " - f"in origin") in self.client.out + assert (f"Sources correctly downloaded from " + f"{self.file_server.fake_url}/internet/myfile.txt") in self.client.out self.client.run("source .") assert (f"Source {self.file_server.fake_url}/internet/myfile.txt retrieved from " f"local download cache") in self.client.out @@ -303,8 +304,10 @@ def source(self): self.client.save({"conanfile.py": conanfile}) self.client.run("create . -vv") - assert (f"WARN: File {self.file_server.fake_url}/internet/myfile.txt not found " - f"in {self.file_server.fake_url}/backup/") in self.client.out + assert (f"WARN: Sources for {self.file_server.fake_url}/internet/myfile.txt not found " + f"in remote backup {self.file_server.fake_url}/backup/") in self.client.out + assert (f"Downloaded {self.file_server.fake_url}/internet/myfile.txt " + f"from {self.file_server.fake_url}/internet/myfile.txt") self.client.run("upload * -c -r=default") rmdir(self.download_cache_folder) @@ -546,9 +549,9 @@ def source(self): client.save({"conanfile.py": conanfile}) client.run("source .", assert_error=True) assert "WARN: Sources for http://fake/myfile.txt failed in 'origin'" in client.out - assert "WARN: Checking backups" in client.out - assert "NotFoundException: File http://fake/myfile.txt " \ - "not found in ['origin', 'http://extrafake/']" in client.out + assert "Checking backup" in client.out + assert "NotFoundException: Sources for http://fake/myfile.txt " \ + "not found in remote backup http://extrafake/" in client.out def test_ok_when_origin_breaks_midway_list(self): http_server_base_folder_backup2 = os.path.join(self.file_server.store, "backup2") From 703eae0e25a781b858bbc1d28fdd963fc2cbf3f9 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 21 Apr 2026 07:04:29 +0200 Subject: [PATCH 101/110] checking config_requires from lockfiles at conan-install time (#19875) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * checking config_requires from lockfiles at conan-install time * api docstrings (#19871) * api docstrings * more * Warn when credentials env vars are ignored due to anonymous server access (#19872) add warning * tools.build:install_strip now accepts a list of possible build systems (#19874) * tools.build:install_strip now accepts a list of possible build systems * Modernize build-order tests to use order-by (#19878) * Modernize build-order tests to use order-by * Modernize build-order tests to use order-by * Allow patterns in `--update` flag (#19856) * Pattern for ref name * Simplify diff * Update help text· * Remove some deprecated features (#19877) * Remove deprecated Node::dependencies method * Remove deprecated cmake_set_interface_link_directories property * Remove deprecated methods from PackagesList * Remove deprecated deploy folder in conan home * Remove deprecated message for order-by in conan graph info build-order * Remove deprecated detect_compiler method in detect api * Remove deprecated system_tools profile section * Remove deprecated boolean powershell conf values * Revert "Remove deprecated message for order-by in conan graph info build-order" This reverts commit 1c09c83f7ac63077bc8e7f4d8e26d095e158f3c9. * Last test changes * Remove deoply usage in test * Remove last true for test * Refactor/sources caching downloaded (#19789) * refactor, tests not passing yet * wip * wip * wip * refactor * wip * review * added to other commands * review * wip --------- Co-authored-by: Carlos Zoido Co-authored-by: PerseoGI Co-authored-by: Abril Rincón Blanco <5364255+AbrilRBS@users.noreply.github.com> --- conan/api/subapi/lockfile.py | 12 ++ conan/cli/commands/audit.py | 1 + conan/cli/commands/build.py | 1 + conan/cli/commands/create.py | 1 + conan/cli/commands/export_pkg.py | 1 + conan/cli/commands/graph.py | 4 + conan/cli/commands/install.py | 1 + conan/cli/commands/lock.py | 2 + conan/cli/commands/test.py | 1 + conan/cli/commands/workspace.py | 4 + conan/internal/model/lockfile.py | 28 +++++ .../command/test_config_install_pkg.py | 13 +++ test/integration/command/config/__init__.py | 0 .../command/{ => config}/config_test.py | 0 .../command/config/test_config_install_pkg.py | 103 ++++++++++++++++++ 15 files changed, 172 insertions(+) create mode 100644 test/integration/command/config/__init__.py rename test/integration/command/{ => config}/config_test.py (100%) create mode 100644 test/integration/command/config/test_config_install_pkg.py diff --git a/conan/api/subapi/lockfile.py b/conan/api/subapi/lockfile.py index dc056a3bb46..63ee687f3dd 100644 --- a/conan/api/subapi/lockfile.py +++ b/conan/api/subapi/lockfile.py @@ -2,8 +2,10 @@ from conan.api.output import ConanOutput from conan.cli import make_abs_path +from conan.internal.cache.home_paths import HomePaths from conan.internal.graph.graph import Overrides from conan.errors import ConanException +from conan.internal.model.conanconfig import loadconanconfig from conan.internal.model.lockfile import Lockfile, LOCKFILE @@ -60,6 +62,16 @@ def get_lockfile(lockfile=None, conanfile_path=None, cwd=None, partial=False, ConanOutput().info("Using lockfile: '{}'".format(lockfile_path)) return graph_lock + def check_lockfile_config(self, lockfile): + """Verify that installed configurations are aligned with lockfile config_requires. + """ + if lockfile is None: + return + + config_version_path = HomePaths(self._conan_api.home_folder).config_version_path + refs = loadconanconfig(config_version_path) if os.path.exists(config_version_path) else [] + lockfile.check_config_requires(refs) + def update_lockfile_export(self, lockfile, conanfile, ref, is_build_require=False): # The package_type is not fully processed at export is_python_require = conanfile.package_type == "python-require" diff --git a/conan/cli/commands/audit.py b/conan/cli/commands/audit.py index 32654e1dd66..32d27d4b731 100644 --- a/conan/cli/commands/audit.py +++ b/conan/cli/commands/audit.py @@ -78,6 +78,7 @@ def audit_scan(conan_api: ConanAPI, parser, subparser, *args) -> dict: 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=args.lockfile_partial, overrides=overrides) + conan_api.lockfile.check_lockfile_config(lockfile) profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args) print_profiles(profile_host, profile_build) diff --git a/conan/cli/commands/build.py b/conan/cli/commands/build.py index d1a79cf9aaa..f6458885898 100644 --- a/conan/cli/commands/build.py +++ b/conan/cli/commands/build.py @@ -48,6 +48,7 @@ def build(conan_api, parser, *args): 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=args.lockfile_partial, overrides=overrides) + conan_api.lockfile.check_lockfile_config(lockfile) profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args) print_profiles(profile_host, profile_build) diff --git a/conan/cli/commands/create.py b/conan/cli/commands/create.py index 25d5c3930af..96d0140f983 100644 --- a/conan/cli/commands/create.py +++ b/conan/cli/commands/create.py @@ -45,6 +45,7 @@ def create(conan_api, parser, *args): cwd=cwd, partial=args.lockfile_partial, overrides=overrides) + conan_api.lockfile.check_lockfile_config(lockfile) remotes = conan_api.remotes.list(args.remote) if not args.no_remote else [] profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args) diff --git a/conan/cli/commands/export_pkg.py b/conan/cli/commands/export_pkg.py index f17b984cfae..21f684db254 100644 --- a/conan/cli/commands/export_pkg.py +++ b/conan/cli/commands/export_pkg.py @@ -43,6 +43,7 @@ def export_pkg(conan_api, parser, *args): lockfile = conan_api.lockfile.get_lockfile(lockfile=args.lockfile, conanfile_path=path, cwd=cwd, partial=args.lockfile_partial, overrides=overrides) + conan_api.lockfile.check_lockfile_config(lockfile) profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args) remotes = conan_api.remotes.list(args.remote) if not args.no_remote else [] output_folder = make_abs_path(args.output_folder, cwd) if args.output_folder else None diff --git a/conan/cli/commands/graph.py b/conan/cli/commands/graph.py index 0e94e186c33..84a5b8503f7 100644 --- a/conan/cli/commands/graph.py +++ b/conan/cli/commands/graph.py @@ -87,6 +87,7 @@ def graph_build_order(conan_api, parser, subparser, *args): cwd=cwd, partial=args.lockfile_partial, overrides=overrides) + conan_api.lockfile.check_lockfile_config(lockfile) profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args) if path: @@ -185,6 +186,7 @@ def graph_info(conan_api, parser, subparser, *args): cwd=cwd, partial=args.lockfile_partial, overrides=overrides) + conan_api.lockfile.check_lockfile_config(lockfile) profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args) print_profiles(profile_host, profile_build) @@ -266,6 +268,7 @@ def graph_explain(conan_api, parser, subparser, *args): cwd=cwd, partial=args.lockfile_partial, overrides=overrides) + conan_api.lockfile.check_lockfile_config(lockfile) profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args) if path: @@ -347,6 +350,7 @@ def graph_outdated(conan_api, parser, subparser, *args): cwd=cwd, partial=args.lockfile_partial, overrides=overrides) + conan_api.lockfile.check_lockfile_config(lockfile) profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args) if path: diff --git a/conan/cli/commands/install.py b/conan/cli/commands/install.py index 61643aeeab7..38cf27d2075 100644 --- a/conan/cli/commands/install.py +++ b/conan/cli/commands/install.py @@ -75,6 +75,7 @@ def _run_install_command(conan_api, args, cwd, return_install_error=True): 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=args.lockfile_partial, overrides=overrides) + conan_api.lockfile.check_lockfile_config(lockfile) profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args) print_profiles(profile_host, profile_build) diff --git a/conan/cli/commands/lock.py b/conan/cli/commands/lock.py index db27e806810..5161e43bb40 100644 --- a/conan/cli/commands/lock.py +++ b/conan/cli/commands/lock.py @@ -35,6 +35,7 @@ def lock_create(conan_api, parser, subparser, *args): 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) + conan_api.lockfile.check_lockfile_config(lockfile) profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args) if path: @@ -204,6 +205,7 @@ def lock_upgrade(conan_api, parser, subparser, *args): cwd=cwd, partial=True, overrides=overrides) if lockfile is None: raise ConanException("No lockfile specified and default conan.lock not found") + conan_api.lockfile.check_lockfile_config(lockfile) profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args) # Remove the lockfile entries that will be updated diff --git a/conan/cli/commands/test.py b/conan/cli/commands/test.py index ca8d461832b..d942c9da7bf 100644 --- a/conan/cli/commands/test.py +++ b/conan/cli/commands/test.py @@ -34,6 +34,7 @@ def test(conan_api, parser, *args): cwd=cwd, partial=args.lockfile_partial, overrides=overrides) + conan_api.lockfile.check_lockfile_config(lockfile) remotes = conan_api.remotes.list(args.remote) if not args.no_remote else [] profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args) diff --git a/conan/cli/commands/workspace.py b/conan/cli/commands/workspace.py index 18ab3fb9245..e9985631a6b 100644 --- a/conan/cli/commands/workspace.py +++ b/conan/cli/commands/workspace.py @@ -94,6 +94,7 @@ def workspace_complete(conan_api: ConanAPI, parser, subparser, *args): ws_folder = conan_api.workspace.folder() lockfile = conan_api.lockfile.get_lockfile(lockfile=args.lockfile, conanfile_path=ws_folder, cwd=None, partial=args.lockfile_partial) + conan_api.lockfile.check_lockfile_config(lockfile) profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args) print_profiles(profile_host, profile_build) @@ -171,6 +172,7 @@ def _install_build(conan_api: ConanAPI, parser, subparser, build, *args): ws_folder = conan_api.workspace.folder() lockfile = conan_api.lockfile.get_lockfile(lockfile=args.lockfile, conanfile_path=ws_folder, cwd=None, partial=args.lockfile_partial) + conan_api.lockfile.check_lockfile_config(lockfile) profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args) print_profiles(profile_host, profile_build) @@ -257,6 +259,7 @@ def workspace_super_install(conan_api: ConanAPI, parser, subparser, *args): lockfile = conan_api.lockfile.get_lockfile(lockfile=args.lockfile, conanfile_path=ws_folder, cwd=None, partial=args.lockfile_partial, overrides=overrides) + conan_api.lockfile.check_lockfile_config(lockfile) profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args) print_profiles(profile_host, profile_build) @@ -337,6 +340,7 @@ def workspace_create(conan_api: ConanAPI, parser, subparser, *args): ws_folder = conan_api.workspace.folder() lockfile = conan_api.lockfile.get_lockfile(lockfile=args.lockfile, conanfile_path=ws_folder, cwd=None, partial=args.lockfile_partial) + conan_api.lockfile.check_lockfile_config(lockfile) profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args) print_profiles(profile_host, profile_build) diff --git a/conan/internal/model/lockfile.py b/conan/internal/model/lockfile.py index d93a5f7436e..f5a54684266 100644 --- a/conan/internal/model/lockfile.py +++ b/conan/internal/model/lockfile.py @@ -317,6 +317,34 @@ def resolve_prev(self, node): if prevs: return prevs.get(node.package_id) + def check_config_requires(self, installed_refs): + # Validates that the given installed configuration packages satisfy the current lockfile + # For that to happen, the installed conf packages must match the lockfile ones + # Lockfile ones can be partial, like not containing recipe-revision + # And also all 'config_requires' in the lockfile must have a configuration package for them + # In case of a lockfile containing several constraints, one per package name must exist + lockfile_refs = set(self._conf_requires.refs()) + if not lockfile_refs: + return # If lockfile is not locking config_requires, do nothing, would break + + # Existing installed config packages must exist in the lockfile + not_locked = [r for r in installed_refs if r not in lockfile_refs] + if not_locked: + raise ConanException(f"Installed config packages {not_locked} not in the lockfile") + + # Config-requires in the lockfile must be installed, at least 1 per package name + # so first, group by package name + lockfile_refs_by_name = {} + for r in self._conf_requires.refs(): + lockfile_refs_by_name.setdefault(r.name, []).append(r) + not_installed = [] + for refs in lockfile_refs_by_name.values(): + if not any(r in installed_refs for r in refs): + not_installed.extend(refs) + if not_installed: + raise ConanException("There are config packages in lockfile " + f"'config_requires' not installed: {not_installed}") + def _resolve(self, require, locked_refs, resolve_prereleases, kind): version_range = require.version_range ref = require.ref diff --git a/test/functional/command/test_config_install_pkg.py b/test/functional/command/test_config_install_pkg.py index f3f4408a15f..65e79c58f9d 100644 --- a/test/functional/command/test_config_install_pkg.py +++ b/test/functional/command/test_config_install_pkg.py @@ -185,6 +185,19 @@ def test_lockfile(self, servers): result = json.loads(c.load("config.lock")) assert "myconf_a/0.2" in result["config_requires"][0] + def test_lockfile_multiple(self, servers): + c = TestClient(servers=servers, light=True) + c.run("config install-pkg myconf_a/0.1 --lockfile-out=config.lock") + c.run("config install-pkg myconf_a/0.2 --lockfile=config.lock --lockfile-out=config.lock " + "--lockfile-partial") + _check_conf(c, "myconf_a/0.2") + _check_conf_file(c, ["myconf_a/0.2"]) + + # The lockfile can contain both, not ideal, but possible + result = json.loads(c.load("config.lock")) + assert "myconf_a/0.2" in result["config_requires"][0] + assert "myconf_a/0.1" in result["config_requires"][1] + def test_package_id_effect(self): # full integration, as "test_config_package_id.py" tests from hardcoded cache json files c = TestClient(light=True) diff --git a/test/integration/command/config/__init__.py b/test/integration/command/config/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/integration/command/config_test.py b/test/integration/command/config/config_test.py similarity index 100% rename from test/integration/command/config_test.py rename to test/integration/command/config/config_test.py diff --git a/test/integration/command/config/test_config_install_pkg.py b/test/integration/command/config/test_config_install_pkg.py new file mode 100644 index 00000000000..d7bcc71946a --- /dev/null +++ b/test/integration/command/config/test_config_install_pkg.py @@ -0,0 +1,103 @@ +import json + +from conan.test.assets.genconanfile import GenConanfile +from conan.test.utils.tools import TestClient + + +class TestInstallLockfileConfigRequires: + """Tests for the alignment check between lockfile config_requires and installed configs.""" + + def test_install_no_lockfile(self): + """conan install without a lockfile skips the check entirely.""" + c = TestClient(light=True) + c.save({"conanfile.py": GenConanfile("pkg", "1.0")}) + c.save_home({"config_version.json": json.dumps( + {"config_version": ["myconf/1.0#aabbcc"]})}) + c.run("install") + assert "ERROR" not in c.out + + def test_install_empty_lockfile(self): + """conan install with an empty lockfile skips the check entirely.""" + c = TestClient(light=True) + c.save({"conanfile.py": GenConanfile("pkg", "1.0"), + "conan.lock": "{}"}) + c.save_home({"config_version.json": json.dumps( + {"config_version": ["myconf/1.0#aabbcc"]})}) + c.run("install") + assert "ERROR" not in c.out + + def test_install_missing_installed_config_error(self): + """Lockfile with config_requires not installed => Error""" + c = TestClient(light=True) + c.save({"conanfile.py": GenConanfile("pkg", "1.0"), + "conan.lock": json.dumps({ + "version": "0.5", + "config_requires": ["myconf/1.0#aabbcc", "other/2.1#rev2"] + })}) + c.run("install . --lockfile=conan.lock", assert_error=True) + assert ("ERROR: There are config packages in lockfile 'config_requires' " + "not installed: [myconf/1.0#aabbcc, other/2.1#rev2]") in c.out + + def test_install_missing_installed_config_error_same(self): + """Same as above but with multiple versions of same package""" + c = TestClient(light=True) + c.save({"conanfile.py": GenConanfile("pkg", "1.0"), + "conan.lock": json.dumps({ + "version": "0.5", + "config_requires": ["myconf/2.0#aabbcc", "myconf/1.0#aabbcc"] + })}) + c.run("install . --lockfile=conan.lock", assert_error=True) + assert ("ERROR: There are config packages in lockfile 'config_requires' " + "not installed: [myconf/2.0#aabbcc, myconf/1.0#aabbcc]") in c.out + + def test_install_lockfile_config_match(self): + """Installed config matches lockfile entry -> passes.""" + c = TestClient(light=True) + c.save({"conanfile.py": GenConanfile("pkg", "1.0"), + "conan.lock": json.dumps({ + "version": "0.5", + "config_requires": ["myconf/1.0#rev1"] + })}) + c.save_home({"config_version.json": json.dumps( + {"config_version": ["myconf/1.0#rev1"]})}) + c.run("install . --lockfile=conan.lock") + assert "ERROR" not in c.out + + def test_install_lockfile_config_partial_match(self): + """Lockfile dont lock down to the revision, only version""" + c = TestClient(light=True) + c.save({"conanfile.py": GenConanfile("pkg", "1.0"), + "conan.lock": json.dumps({ + "version": "0.5", + "config_requires": ["myconf/1.0"] + })}) + c.save_home({"config_version.json": json.dumps( + {"config_version": ["myconf/1.0#rev1"]})}) + c.run("install . --lockfile=conan.lock") + assert "ERROR" not in c.out + + def test_install_lockfile_config_revision_mismatch_error(self): + """Installed config has different revision than lockfile -> error.""" + c = TestClient(light=True) + c.save({"conanfile.py": GenConanfile("pkg", "1.0"), + "conan.lock": json.dumps({ + "version": "0.5", + "config_requires": ["myconf/1.0#rev_in_lockfile"] + })}) + c.save_home({"config_version.json": json.dumps( + {"config_version": ["myconf/1.0#rev"]})}) + c.run("install . --lockfile=conan.lock", assert_error=True) + assert "ERROR: Installed config packages [myconf/1.0#rev] not in the lockfile" in c.out + + def test_install_lockfile_config_version_mismatch(self): + """Installed config has different version than lockfile entry -> error.""" + c = TestClient(light=True) + c.save({"conanfile.py": GenConanfile("pkg", "1.0"), + "conan.lock": json.dumps({ + "version": "0.5", + "config_requires": ["myconf/2.0#aabbcc"] + })}) + c.save_home({"config_version.json": json.dumps( + {"config_version": ["myconf/1.0#aabbcc"]})}) + c.run("install . --lockfile=conan.lock", assert_error=True) + assert "ERROR: Installed config packages [myconf/1.0#aabbcc] not in the lockfile" in c.out From 490772b05d0f81398f006712ac08c19d38298db0 Mon Sep 17 00:00:00 2001 From: Alexander Krabler Date: Tue, 21 Apr 2026 08:00:15 +0200 Subject: [PATCH 102/110] detect_libcxx: Remove temporary directory usage, using safe parameters instead (#19838) * detect_libcxx: Remove temporary directory after use `detect_libxx` tries to detect the C++ standard library implementation. When running `conan profile detect`, a temporary folder is created, where a minimal C++ translation unit is stored. This unit gets compiled in order to test which standard library is in use. Due to the use of `tempfile.mkdtemp()`, cleanup must be done manually, but was omitted. Migrate to using a context manager instead (`tempfile.TemporaryDirectory`), cleaning automatically when the scope ends. Ignoring cleanup errors as it is not a huge problem when there are some remaining files still available. Remove unnecessary using-directive for `std` namespace, too. Changelog: omit Docs: omit * detect_libcxx: Remove temporary directory completely The temporary directory was used for two purposes: * Storing the input file (`main.cpp`). * Storing the output file (`a.out`). Both of these can be quite easily replaced: * The input can be passed via stdin to the compiler. `-xc++` is necessary to tell the compiler which language the input is, as autodetection from filename doesn't work anymore. `-` is used to tell the compiler to use stdin. * The output can be omitted by using `-fsyntax-only`, which tells the compiler to stop early and don't write output at all. The output is not relevant anyway, only the returncode and error message was used. Together, the whole chdir and temporary file logic is obsolete and can be removed. --- conan/internal/api/detect/detect_api.py | 37 ++++++++++++------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/conan/internal/api/detect/detect_api.py b/conan/internal/api/detect/detect_api.py index a59b92f5229..6e380a2e492 100644 --- a/conan/internal/api/detect/detect_api.py +++ b/conan/internal/api/detect/detect_api.py @@ -1,6 +1,7 @@ import os import platform import re +import subprocess import tempfile import textwrap @@ -223,29 +224,25 @@ def _detect_gcc_libcxx(version_, executable): main = textwrap.dedent(""" #include - using namespace std; static_assert(sizeof(std::string) != sizeof(void*), "using libstdc++"); int main(){} """) - t = tempfile.mkdtemp() - filename = os.path.join(t, "main.cpp") - save(filename, main) - old_path = os.getcwd() - os.chdir(t) - try: - error, out_str = detect_runner(f'"{executable}" main.cpp -std=c++11') - if error: - if "using libstdc++" in out_str: - output.info("gcc C++ standard library: libstdc++") - return "libstdc++" - # Other error, but can't know, lets keep libstdc++11 - output.warning("compiler.libcxx check error: %s" % out_str) - output.warning("Couldn't deduce compiler.libcxx for gcc>=5.1, assuming libstdc++11") - else: - output.info("gcc C++ standard library: libstdc++11") - return "libstdc++11" - finally: - os.chdir(old_path) + # -fsyntax-only to omit the output and stop early (but enough for our static_assert). + # -xc++ and - to tell the compiler to compile code from stdin and treat it as C++. + completed_process = subprocess.run([executable, "-std=c++11", "-fsyntax-only", "-xc++", "-"], + input=main, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, text=True) + error, out_str = completed_process.returncode, completed_process.stdout + if error: + if "using libstdc++" in out_str: + output.info("gcc C++ standard library: libstdc++") + return "libstdc++" + # Other error, but can't know, lets keep libstdc++11 + output.warning("compiler.libcxx check error: %s" % out_str) + output.warning("Couldn't deduce compiler.libcxx for gcc>=5.1, assuming libstdc++11") + else: + output.info("gcc C++ standard library: libstdc++11") + return "libstdc++11" # This is not really a detection in most cases # Get compiler C++ stdlib From a5cdf4c126908e543d60d07b481da21a24492c05 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 21 Apr 2026 10:32:39 +0200 Subject: [PATCH 103/110] poc of changing transitive statics package_id (#19705) * poc of changing transitive statics package_id * review * conf * wip * wip * new approach with required_conan_version>=2.28 * fix * wip * remove help line --- conan/internal/graph/compute_pid.py | 22 ++++++- conan/internal/graph/graph_binaries.py | 11 ++-- conan/internal/loader.py | 1 + conan/internal/model/conan_file.py | 1 + conan/internal/model/conf.py | 7 ++- conan/internal/model/requires.py | 19 ++++++- .../package_id_requires_modes_test.py | 57 +++++++++++++++++++ 7 files changed, 107 insertions(+), 11 deletions(-) diff --git a/conan/internal/graph/compute_pid.py b/conan/internal/graph/compute_pid.py index 94494f7f1cb..14c33f94a3c 100644 --- a/conan/internal/graph/compute_pid.py +++ b/conan/internal/graph/compute_pid.py @@ -6,9 +6,21 @@ from conan.internal.model.info import (ConanInfo, RequirementsInfo, RequirementInfo, PythonRequiresInfo) from conan.internal.model.pkg_type import PackageType +from conan.internal.model.version_range import VersionRange +from conan.internal.model.version import Version -def compute_package_id(node, modes, config_version, hook_manager): +def _compute_fix_transitive(conanfile, global_required_conan): + # fix for transitive static libraries + recipe_require_conan_version = global_required_conan or conanfile._conan_required_version # noqa + if recipe_require_conan_version: + version_range = VersionRange(recipe_require_conan_version) + for conditions in version_range.condition_sets: + conditions.prerelease = True + return not version_range.contains(Version("2.27.9"), resolve_prerelease=None) + + +def compute_package_id(node, modes, config_version, hook_manager, global_required_conan): """ Compute the binary package ID of this node """ @@ -20,10 +32,14 @@ def compute_package_id(node, modes, config_version, hook_manager): data = OrderedDict() build_data = OrderedDict() + + fix_transitive_static = _compute_fix_transitive(conanfile, global_required_conan) + for require, transitive in node.transitive_deps.items(): dep_node = transitive.node - require.deduce_package_id_mode(conanfile.package_type, dep_node, - non_embed_mode, embed_mode, build_mode, unknown_mode) + require.deduce_package_id_mode(conanfile, dep_node, + non_embed_mode, embed_mode, build_mode, unknown_mode, + fix_transitive_static) if require.package_id_mode is not None: req_info = RequirementInfo(dep_node.pref.ref, dep_node.pref.package_id, require.package_id_mode) diff --git a/conan/internal/graph/graph_binaries.py b/conan/internal/graph/graph_binaries.py index 09889f638f6..0249cfd1053 100644 --- a/conan/internal/graph/graph_binaries.py +++ b/conan/internal/graph/graph_binaries.py @@ -453,8 +453,9 @@ def _config_version(self): f" file in cache: {self._home_folder}: {e}") return RequirementsInfo(result) - def _evaluate_package_id(self, node, config_version): - compute_package_id(node, self._modes, config_version, self._hook_manager) + def _evaluate_package_id(self, node, config_version, global_required_conan): + compute_package_id(node, self._modes, config_version, self._hook_manager, + global_required_conan) # TODO: layout() execution don't need to be evaluated at GraphBuilder time. # it could even be delayed until installation time, but if we got enough info here for @@ -490,9 +491,10 @@ def _evaluate_single(n): root_pkg_type = deps_graph.root.edges[0].dst.conanfile.package_type \ if deps_graph.root.edges else None config_version = self._config_version() if root_pkg_type is not PackageType.CONF else None + global_required_conan = self._global_conf.get("core:required_conan_version") for level in levels[:-1]: # all levels but the last one, which is the single consumer for node in level: - self._evaluate_package_id(node, config_version) + self._evaluate_package_id(node, config_version, global_required_conan) # group by pref to paralelize, so evaluation is done only 1 per pref nodes = {} for node in level: @@ -521,7 +523,8 @@ def _evaluate_single(n): if node.path is not None: if node.path.endswith(".py"): # For .py we keep evaluating the package_id, validate(), etc - compute_package_id(node, self._modes, config_version, self._hook_manager) + compute_package_id(node, self._modes, config_version, self._hook_manager, + global_required_conan) # To support the ``[layout]`` in conanfile.txt if hasattr(node.conanfile, "layout"): with conanfile_exception_formatter(node.conanfile, "layout"): diff --git a/conan/internal/loader.py b/conan/internal/loader.py index 0dccf125a07..2e0dfeff703 100644 --- a/conan/internal/loader.py +++ b/conan/internal/loader.py @@ -57,6 +57,7 @@ def load_basic_module(self, conanfile_path, graph_lock=None, display="", remotes try: module, conanfile = _parse_conanfile(conanfile_path) + conanfile._conan_required_version = getattr(module, "required_conan_version", None) if isinstance(tested_python_requires, RecipeReference): if getattr(conanfile, "python_requires", None) == "tested_reference_str": conanfile.python_requires = tested_python_requires.repr_notime() diff --git a/conan/internal/model/conan_file.py b/conan/internal/model/conan_file.py index 313060cf40f..6c23642280b 100644 --- a/conan/internal/model/conan_file.py +++ b/conan/internal/model/conan_file.py @@ -59,6 +59,7 @@ class ConanFile: win_bash_run = None # For run scope _conan_is_consumer = False + _conan_required_version = None # #### Requirements requires = None diff --git a/conan/internal/model/conf.py b/conan/internal/model/conf.py index 87ae9a360ea..acf2b454ebd 100644 --- a/conan/internal/model/conf.py +++ b/conan/internal/model/conf.py @@ -17,8 +17,13 @@ from conan.internal.model.settings import SettingsItem from conan.internal.util.files import load, save +required_conan_version_msg = """\ +Raise if current version does not match the defined range. + - If required_conan_version>=2.28, bugfix https://github.com/conan-io/conan/pull/19705 for transitive static libraries package_id is applied + These behaviors also apply for 'required_conan_version' in recipes, but the global one has precedence.""" + BUILT_IN_CONFS = { - "core:required_conan_version": "Raise if current version does not match the defined range.", + "core:required_conan_version": required_conan_version_msg, "core:non_interactive": "Disable interactive user input, raises error if input necessary", "core:warnings_as_errors": "Treat warnings matching any of the patterns in this list as errors and then raise an exception. " "Current warning tags are 'network', 'deprecated'", diff --git a/conan/internal/model/requires.py b/conan/internal/model/requires.py index 62cbed2c1b3..d4c44dd8fb6 100644 --- a/conan/internal/model/requires.py +++ b/conan/internal/model/requires.py @@ -357,8 +357,8 @@ def transform_downstream(self, pkg_type, require, dep_pkg_type): downstream_require.direct = False return downstream_require - def deduce_package_id_mode(self, pkg_type, dep_node, non_embed_mode, embed_mode, build_mode, - unknown_mode): + def deduce_package_id_mode(self, conanfile, dep_node, non_embed_mode, embed_mode, build_mode, + unknown_mode, fix_transitive_static): # If defined by the ``require(package_id_mode=xxx)`` trait, that is higher priority # The "conf" values are defaults, no hard overrides if self.package_id_mode: @@ -374,6 +374,7 @@ def deduce_package_id_mode(self, pkg_type, dep_node, non_embed_mode, embed_mode, self.package_id_mode = build_mode return + pkg_type = conanfile.package_type if pkg_type is PackageType.HEADER: self.package_id_mode = "unrelated_mode" return @@ -391,8 +392,20 @@ def deduce_package_id_mode(self, pkg_type, dep_node, non_embed_mode, embed_mode, elif pkg_type is PackageType.STATIC: if dep_pkg_type is PackageType.HEADER: self.package_id_mode = embed_mode - else: + elif self.headers or not fix_transitive_static: self.package_id_mode = non_embed_mode + if not self.headers and not fix_transitive_static: + # Just to avoid multiple repeated warnings + warned = getattr(conanfile, "_conan_fix_transitive_static", False) + if not warned: + msg = ("Transitive dependencies with 'headers=False' effect in " + "'package_id' is not necessary and suboptimal. Use " + "required_conan_version='>=2.28' to activate it") + conanfile.output.warning(msg, warn_tag="risk") + conanfile._conan_fix_transitive_static = True + else: + self.package_id_mode = None + return if self.package_id_mode is None: self.package_id_mode = unknown_mode diff --git a/test/integration/package_id/package_id_requires_modes_test.py b/test/integration/package_id/package_id_requires_modes_test.py index dbffa8ad608..ea43050269e 100644 --- a/test/integration/package_id/package_id_requires_modes_test.py +++ b/test/integration/package_id/package_id_requires_modes_test.py @@ -216,3 +216,60 @@ def test_half_diamond_conflict(self, mode, pattern): c.run("create pkg") c.run("list pkg:*") assert f"liba/{pattern}" in c.out + + +class TestTransitiveStatic: + @pytest.mark.parametrize("apply_fix", [True, False, "conf"]) + def test_transitive_statics(self, apply_fix): + # https://github.com/conan-io/conan/issues/19664 + c = TestClient(light=True) + required_conan_version = 'required_conan_version = ">=2.28"' if apply_fix is True else "" + if apply_fix == "conf": + c.save_home({"global.conf": "core:required_conan_version=>=2.28"}) + libc = textwrap.dedent(f"""\ + from conan import ConanFile + + {required_conan_version} + class LibcConan(ConanFile): + name = "libc" + version = "1.0" + package_type = "static-library" + requires = "libb/1.0" + """) + c.save({"liba/conanfile.py": GenConanfile("liba", "1.0").with_package_type("static-library"), + "libb/conanfile.py": GenConanfile("libb", "1.0").with_package_type("static-library") + .with_requires("liba/1.0"), + "libc/conanfile.py": libc + }) + c.run("create liba") + c.run("create libb") + c.run(f"create libc") + if not apply_fix: + assert ("libc/1.0: WARN: risk: Transitive dependencies with " + "'headers=False' effect in 'package_id'") in c.out + c.run("list libc:*") + assert "libb/1.0.Z" in c.out + if apply_fix: + assert "liba/" not in c.out + else: + assert "liba/" in c.out + + def test_transitive_shared(self): + # https://github.com/conan-io/conan/issues/19664 + # This doesn't happen by default because the transitive shared do not propagate .libs + # linkage requirement trait + c = TestClient(light=True) + c.save({"liba/conanfile.py": GenConanfile("liba", "1.0").with_package_type("shared-library"), + "libb/conanfile.py": GenConanfile("libb", "1.0").with_package_type("shared-library") + .with_requires("liba/1.0"), + "libc/conanfile.py": GenConanfile("libc", "1.0").with_package_type("shared-library") + .with_requires("libb/1.0"), + }) + c.run("create liba") + c.run("create libb") + c.run("create libc") + + assert "libc/1.0: WARN" not in c.out + c.run("list libc:*") + assert "libb/1.0.Z" in c.out + assert "liba/" not in c.out From e726aad25e799d98c7287eb3c91ce7b1591bb324 Mon Sep 17 00:00:00 2001 From: PerseoGI Date: Tue, 21 Apr 2026 12:28:10 +0200 Subject: [PATCH 104/110] Scoped output: first approach (#19836) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * First approach for a contextual output * Support multi lines in messages * Simplify approach, limit to export command * Spaces only * Arrow with underline POC * Change approach to scope based output * Change scope color, remove underline and reduce changes * Disable contextual output behavior on tests and all commands but export * Fix old python issue * Simplify code * Remove ignore_indent paremeter * Add test case for scoped output * Close scoped output after export command * Update test/unittests/client/conan_output_test.py --------- Co-authored-by: Abril Rincón Blanco <5364255+AbrilRBS@users.noreply.github.com> --- conan/api/output.py | 40 +++++++++++++++++++++- conan/cli/commands/export.py | 7 +++- conan/test/utils/tools.py | 2 ++ test/unittests/client/conan_output_test.py | 34 ++++++++++++++++++ 4 files changed, 81 insertions(+), 2 deletions(-) diff --git a/conan/api/output.py b/conan/api/output.py index 520e5d560cb..aa9359587d8 100644 --- a/conan/api/output.py +++ b/conan/api/output.py @@ -101,6 +101,9 @@ class ConanOutput: _conan_output_level = LEVEL_STATUS _silent_warn_tags = [] _warnings_as_errors = [] + _last_scope_header = None + # Flag to enable/disable new ConanOutput contextual behavior + _scoped_recipe_output = None lock = Lock() def __init__(self, scope: str = ""): @@ -115,6 +118,8 @@ def __init__(self, scope: str = ""): # FIXME: This is needed because in testing we are redirecting the sys.stderr to a buffer # stream to capture it, so colorama is not there to strip the color bytes self._color = _color_enabled(self.stream) + if not scope: + ConanOutput._last_scope_header = None @classmethod def define_silence_warnings(cls, warnings): @@ -165,6 +170,14 @@ def define_log_level(cls, v): def level_allowed(cls, level): return cls._conan_output_level <= level + def _emit_scope_line_if_new(self): + """Print ``scope:`` once per scope change, when a message is about to be written.""" + scope = self._scope + if not scope or scope == ConanOutput._last_scope_header: + return + self.writeln(f"{scope}:", fg=Color.BRIGHT_BLUE) + ConanOutput._last_scope_header = scope + @property def color(self): return self._color @@ -176,6 +189,8 @@ def scope(self): @scope.setter def scope(self, out_scope): self._scope = out_scope + if not out_scope: + ConanOutput._last_scope_header = None @property def is_terminal(self): @@ -212,6 +227,18 @@ def login_msg(self, msg, newline=False): self._write_message(msg, newline=newline) return self + def _format_scoped_message(self, msg, fg=None, bg=None): + """Prefix every physical line with a short space gutter (block under the scope header). + Splitlines is used for handling multi-line messages, ensuring a gutter is added to each line + """ + parts = [] + for line in msg.splitlines(): + if self._color: + parts.append(f" {fg or ''}{bg or ''}{line}{Style.RESET_ALL}") + else: + parts.append(f" {line}") + return "\n".join(parts) + def _write_message(self, msg, fg=None, bg=None, newline=True): if isinstance(msg, dict): # For traces we can receive a dict already, we try to transform then into more natural @@ -220,7 +247,10 @@ def _write_message(self, msg, fg=None, bg=None, newline=True): msg = "=> {}".format(msg) # msg = json.dumps(msg, sort_keys=True, default=json_encoder) - if self._scope: + if ConanOutput._scoped_recipe_output and self._scope: + self._emit_scope_line_if_new() + ret = self._format_scoped_message(msg, fg, bg) + elif self._scope: if self._color: ret = f"{fg or ''}{bg or ''}{self._scope}: {msg}{Style.RESET_ALL}" else: @@ -292,15 +322,23 @@ def status(self, msg: str, fg: str = None, bg: str = None, newline: bool = True) def title(self, msg: str): """ Draws a title around the message, useful for important messages""" if self._conan_output_level <= LEVEL_NOTICE: + ConanOutput._last_scope_header = None + prev_scope = self._scope + self._scope = "" self._write_message("\n======== {} ========".format(msg), fg=Color.BRIGHT_MAGENTA) + self._scope = prev_scope return self def subtitle(self, msg: str): """ Draws a subtitle around the message, useful for important messages""" if self._conan_output_level <= LEVEL_NOTICE: + ConanOutput._last_scope_header = None + prev_scope = self._scope + self._scope = "" self._write_message("\n-------- {} --------".format(msg), fg=Color.BRIGHT_MAGENTA) + self._scope = prev_scope return self def highlight(self, msg: str): diff --git a/conan/cli/commands/export.py b/conan/cli/commands/export.py index cc6c3e53d00..963ee5cb6ca 100644 --- a/conan/cli/commands/export.py +++ b/conan/cli/commands/export.py @@ -2,7 +2,7 @@ import os from conan.api.model import MultiPackagesList, PackagesList -from conan.api.output import cli_out_write +from conan.api.output import ConanOutput, cli_out_write from conan.cli.command import conan_command, OnceArgument from conan.cli.args import add_reference_args @@ -43,6 +43,10 @@ def export(conan_api, parser, *args): help='Whether the provided reference is a build-require') args = parser.parse_args(*args) + # Only enable scoped output if None. If it is False, it means that + # we have explicitly disabled (e.g. tests), so we should not enable it + if ConanOutput._scoped_recipe_output is None: + ConanOutput._scoped_recipe_output = True cwd = os.getcwd() path = conan_api.local.get_conanfile_path(args.path, cwd, py=True) remotes = conan_api.remotes.list(args.remote) if not args.no_remote else [] @@ -64,6 +68,7 @@ def export(conan_api, parser, *args): pkglist = MultiPackagesList() pkglist.add("Local Cache", exported_list) + ConanOutput._scoped_recipe_output = None return { "pkglist": pkglist.serialize(), diff --git a/conan/test/utils/tools.py b/conan/test/utils/tools.py index 8ef1ebbeaea..6ae57eee6e0 100644 --- a/conan/test/utils/tools.py +++ b/conan/test/utils/tools.py @@ -23,6 +23,7 @@ from requests.exceptions import HTTPError from webtest.app import TestApp +from conan.api.output import ConanOutput from conan.api.subapi.audit import CONAN_CENTER_AUDIT_PROVIDER_NAME, _save_providers from conan.api.subapi.remotes import _save from conan.cli.exit_codes import SUCCESS @@ -531,6 +532,7 @@ def mock_get_pass(*args, **kwargs): yield def _run_cli(self, command_line, assert_error=False): + ConanOutput._scoped_recipe_output = False args = shlex.split(command_line) error = SUCCESS trace = None diff --git a/test/unittests/client/conan_output_test.py b/test/unittests/client/conan_output_test.py index ea35f762813..bdccdaa48de 100644 --- a/test/unittests/client/conan_output_test.py +++ b/test/unittests/client/conan_output_test.py @@ -2,6 +2,7 @@ from unittest import mock import pytest +import textwrap from conan.api.output import ConanOutput, init_colorama from conan.test.utils.mocks import RedirectedTestOutput @@ -48,3 +49,36 @@ def test_output_chainable(): assert "My title" in stderr.getvalue() assert "Worked" in stderr.getvalue() assert "But there was more that needed to be said" in stderr.getvalue() + + +def test_output_scoped(): + """ + Test that when scoped output is enabled, the scope is only printed once per scope change or title/subtitle is emitted + """ + stderr = RedirectedTestOutput() + with redirect_output(stderr): + ConanOutput._scoped_recipe_output = True + output = ConanOutput(scope="My package") + output.info("Hello") + output.highlight("Conan") + output.title("Title") + output.info("Package manager!") + output.subtitle("Subtitle") + output.highlight("Frog") + output = ConanOutput(scope="Other package") + output.info("Hello") + assert textwrap.dedent("""\ + My package: + Hello + Conan + + ======== Title ======== + My package: + Package manager! + + -------- Subtitle -------- + My package: + Frog + Other package: + Hello + """) == stderr.getvalue() From cf01c962e5279ebb6ed5d8c6694787acc733db0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= <5364255+AbrilRBS@users.noreply.github.com> Date: Tue, 21 Apr 2026 12:47:00 +0200 Subject: [PATCH 105/110] Colorize config command outputs (#19889) --- conan/cli/commands/config.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/conan/cli/commands/config.py b/conan/cli/commands/config.py index 0bcd1719700..d930a07ca10 100644 --- a/conan/cli/commands/config.py +++ b/conan/cli/commands/config.py @@ -1,7 +1,7 @@ import os from conan.api.model import Remote -from conan.api.output import cli_out_write +from conan.api.output import cli_out_write, Color from conan.cli import make_abs_path from conan.cli.command import conan_command, conan_subcommand, OnceArgument from conan.cli.formatters import default_json_formatter @@ -115,7 +115,8 @@ def config_install_pkg(conan_api, parser, subparser, *args): def _list_text_formatter(confs): for k, v in confs.items(): - cli_out_write(f"{k}: {v}") + cli_out_write(f"{k}: ", fg=Color.CYAN, endline="") + cli_out_write(v) @conan_subcommand(formatters={"text": cli_out_write}) From c3764a80f4661e94011fcbe088d22ab780b1ac1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= <5364255+AbrilRBS@users.noreply.github.com> Date: Tue, 21 Apr 2026 13:39:27 +0200 Subject: [PATCH 106/110] HTML updates for `conan report diff` and `conan graph info` (#19884) * Update html graph info * Audit * Show subgraph * Show subgraph exclusive edges * Fix missing nodes * Style information * Extension filter * Remove regex, fix extensions * Remove vonsole.log * Siggestions of style * Suggested changes * Apply suggestion from @memsharded * Back to black --------- Co-authored-by: James --- conan/cli/formatters/graph/info_graph_html.py | 133 +++++++++-- conan/cli/formatters/report/diff_html.py | 211 ++++++++++++++---- .../command/info/test_graph_info_graphical.py | 5 +- 3 files changed, 282 insertions(+), 67 deletions(-) diff --git a/conan/cli/formatters/graph/info_graph_html.py b/conan/cli/formatters/graph/info_graph_html.py index e2466d225b9..6ccf6692532 100644 --- a/conan/cli/formatters/graph/info_graph_html.py +++ b/conan/cli/formatters/graph/info_graph_html.py @@ -21,12 +21,46 @@ display: inline-block; font-size: 18px; } + label { + user-select: none + } + + #container { + display: grid; + grid-template-columns: 75% 25%; + grid-template-rows: 50px auto; + height: 100vh; + } + #mylegend { + border-bottom: 2px solid #f2f2f2; + grid-column-end: span 1; + font-size: 14px; + height: 100%; + } + #empty-sidebar, #sidebar { + background-color: #f9f9fe; + border: none; + min-height:100%; + height:0; + overflow-y: auto; + padding: 5px 10px; + } + #details { + background-color: #f3f3f3; + overflow-y: auto; + border: 1px solid #e4e4e4; + border-radius: 5px; + padding: 3px 5px; + } -
    -
    +
    +
    +
    +

    Controls

    +
    -
    +