Repository: conan-io/conan
Pull request #17642 (2025-01-27 12:47:54)

Feature/update resolution
Changelog: Omit
Docs: Omit

Close https://github.com/conan-io/conan/pull/17585
Close https://github.com/conan-io/conan/issues/17548


Patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 02a8ad199..ad2a250fb 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -16,17 +16,44 @@ concurrency:
   cancel-in-progress: true
 
 jobs:
+  set_python_versions:
+    runs-on: ubuntu-latest
+    outputs:
+      python_versions_linux_windows: ${{ steps.set_versions.outputs.python_versions_linux_windows }}
+      python_versions_macos: ${{ steps.set_versions.outputs.python_versions_macos }}
+    name: Determine Python versions
+    steps:
+      - name: Determine Python versions
+        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
+          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
     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
     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
     uses: ./.github/workflows/win-tests.yml
     name: Windows test suite
+    with:
+      python-versions: ${{ needs.set_python_versions.outputs.python_versions_linux_windows }}
 
   code_coverage:
     runs-on: ubuntu-latest
diff --git a/conan/api/model/list.py b/conan/api/model/list.py
index abd43e93c..c7149e1b2 100644
--- a/conan/api/model/list.py
+++ b/conan/api/model/list.py
@@ -16,9 +16,6 @@ class MultiPackagesList:
     def __init__(self):
         self.lists = {}
 
-    def setdefault(self, key, default):
-        return self.lists.setdefault(key, default)
-
     def __getitem__(self, name):
         try:
             return self.lists[name]
@@ -57,11 +54,6 @@ class MultiPackagesList:
         pkglist.lists = result
         return pkglist
 
-    @staticmethod
-    def from_graph(graph, graph_recipes=None, graph_binaries=None):
-        graph = {"graph": graph.serialize()}
-        return MultiPackagesList._define_graph(graph, graph_recipes, graph_binaries)
-
     @staticmethod
     def load_graph(graphfile, graph_recipes=None, graph_binaries=None):
         if not os.path.isfile(graphfile):
diff --git a/conan/api/output.py b/conan/api/output.py
index 25fa62698..6989abc07 100644
--- a/conan/api/output.py
+++ b/conan/api/output.py
@@ -175,7 +175,7 @@ class ConanOutput:
 
         return self
 
-    def _write_message(self, msg, fg=None, bg=None):
+    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
             # text
@@ -195,8 +195,11 @@ class ConanOutput:
         else:
             ret += "{}".format(msg)
 
+        if newline:
+            ret = "%s\n" % ret
+
         with self.lock:
-            self.stream.write("{}\n".format(ret))
+            self.stream.write(ret)
             self.stream.flush()
 
     def trace(self, msg):
diff --git a/conan/cli/commands/remote.py b/conan/cli/commands/remote.py
index 2770f3f36..a22529f54 100644
--- a/conan/cli/commands/remote.py
+++ b/conan/cli/commands/remote.py
@@ -278,7 +278,7 @@ 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* 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
@@ -288,7 +288,7 @@ def remote_auth(conan_api, parser, subparser, *args):
                                           " The pattern uses 'fnmatch' style wildcards.")
     subparser.add_argument("--with-user", action="store_true",
                            help="Only try to auth in those remotes that already "
-                                "have a username or a CONAN_LOGIN_ env-var defined")
+                                "have a username or a CONAN_LOGIN_USERNAME* env-var defined")
     subparser.add_argument("--force", action="store_true",
                            help="Force authentication for anonymous-enabled repositories. "
                                 "Can be used for force authentication in case your Artifactory "
diff --git a/conan/cps/cps.py b/conan/cps/cps.py
index 4fab7911a..d8c9fb15c 100644
--- a/conan/cps/cps.py
+++ b/conan/cps/cps.py
@@ -115,12 +115,16 @@ class CPS:
         self.description = None
         self.license = None
         self.website = None
+        self.prefix = None
 
     def serialize(self):
-        cps = {"cps_version": "0.12.0",
+        cps = {"cps_version": "0.13.0",
                "name": self.name,
                "version": self.version}
 
+        if self.prefix is not None:
+            cps["prefix"] = self.prefix
+
         # Supplemental
         for data in "license", "description", "website":
             if getattr(self, data, None):
@@ -143,6 +147,7 @@ class CPS:
     def deserialize(data):
         cps = CPS()
         cps.name = data.get("name")
+        cps.prefix = data.get("prefix")
         cps.version = data.get("version")
         cps.license = data.get("license")
         cps.description = data.get("description")
@@ -157,6 +162,7 @@ class CPS:
     @staticmethod
     def from_conan(dep):
         cps = CPS(dep.ref.name, str(dep.ref.version))
+        cps.prefix = dep.package_folder.replace("\\", "/")
         # supplemental
         cps.license = dep.license
         cps.description = dep.description
diff --git a/conan/internal/api/install/generators.py b/conan/internal/api/install/generators.py
index 61c0d7b85..fe553891f 100644
--- a/conan/internal/api/install/generators.py
+++ b/conan/internal/api/install/generators.py
@@ -11,6 +11,7 @@ from conans.util.files import save, mkdir, chdir
 
 _generators = {"CMakeToolchain": "conan.tools.cmake",
                "CMakeDeps": "conan.tools.cmake",
+               "CMakeConfigDeps": "conan.tools.cmake",
                "MesonToolchain": "conan.tools.meson",
                "MSBuildDeps": "conan.tools.microsoft",
                "MSBuildToolchain": "conan.tools.microsoft",
diff --git a/conan/internal/conan_app.py b/conan/internal/conan_app.py
index 1d9a8540f..72196af08 100644
--- a/conan/internal/conan_app.py
+++ b/conan/internal/conan_app.py
@@ -8,7 +8,6 @@ from conans.client.graph.range_resolver import RangeResolver
 from conans.client.loader import ConanFileLoader, load_python_file
 from conans.client.remote_manager import RemoteManager
 from conans.client.rest.auth_manager import ConanApiAuthManager
-from conans.client.rest.conan_requester import ConanRequester
 from conan.internal.api.remotes.localdb import LocalDB
 
 
diff --git a/conan/internal/model/conf.py b/conan/internal/model/conf.py
index f7b9bcc32..335d7c08c 100644
--- a/conan/internal/model/conf.py
+++ b/conan/internal/model/conf.py
@@ -28,6 +28,7 @@ BUILT_IN_CONFS = {
     "core.download:retry_wait": "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.",
     # Sources backup
     "core.sources:download_cache": "Folder to store the sources backup",
     "core.sources:download_urls": "List of URLs to download backup sources from",
diff --git a/conan/internal/runner/docker.py b/conan/internal/runner/docker.py
index c21bee192..b6d24f832 100644
--- a/conan/internal/runner/docker.py
+++ b/conan/internal/runner/docker.py
@@ -1,37 +1,68 @@
-from collections import namedtuple
+from argparse import Namespace
 import os
+import sys
 import json
 import platform
 import shutil
+from typing import Optional, NamedTuple, Dict, List
 import yaml
+from conan.api.conan_api import ConanAPI
 from conan.api.model import ListPattern
 from conan.api.output import Color, ConanOutput
 from conan.cli import make_abs_path
 from conan.internal.runner import RunnerException
 from conan.errors import ConanException
+from pathlib import Path
+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):
+    class Build(NamedTuple):
+        dockerfile: Optional[str] = None
+        build_context: Optional[str] = None
+        build_args: Optional[Dict[str, str]] = None
+        cache_from: Optional[List[str]] = None
+        platform: Optional[str] = None
+
+    class Run(NamedTuple):
+        name: Optional[str] = None
+        environment: Optional[Dict[str, str]] = None
+        user: Optional[str] = None
+        privileged: Optional[bool] = None
+        cap_add: Optional[List[str]] = None
+        security_opt: Optional[List[str]] = None
+        volumes: Optional[Dict[str, str]] = None
+        network: Optional[str] = None
+
+    image: Optional[str] = None
+    build: Build = Build()
+    run: Run = Run()
+
+    @staticmethod
+    def load(file_path: Optional[str]) -> '_ContainerConfig':
+        # Container config
+        # https://containers.dev/implementors/json_reference/
+        if not file_path:
+            return _ContainerConfig()
 
-def config_parser(file_path):
-    Build = namedtuple('Build', ['dockerfile', 'build_context', 'build_args', 'cache_from'])
-    Run = namedtuple('Run', ['name', 'environment', 'user', 'privileged', 'cap_add', 'security_opt', 'volumes', 'network'])
-    Conf = namedtuple('Conf', ['image', 'build', 'run'])
-    if file_path:
         def _instans_or_error(value, obj):
             if value and (not isinstance(value, obj)):
                 raise ConanException(f"docker runner configfile syntax error: {value} must be a {obj.__name__}")
             return value
         with open(file_path, 'r') as f:
             runnerfile = yaml.safe_load(f)
-        return Conf(
+        return _ContainerConfig(
             image=_instans_or_error(runnerfile.get('image'), str),
-            build=Build(
+            build=_ContainerConfig.Build(
                 dockerfile=_instans_or_error(runnerfile.get('build', {}).get('dockerfile'), str),
                 build_context=_instans_or_error(runnerfile.get('build', {}).get('build_context'), str),
                 build_args=_instans_or_error(runnerfile.get('build', {}).get('build_args'), dict),
                 cache_from=_instans_or_error(runnerfile.get('build', {}).get('cacheFrom'), list),
+                platform=_instans_or_error(runnerfile.get('build', {}).get('platform'), str),
             ),
-            run=Run(
+            run=_ContainerConfig.Run(
                 name=_instans_or_error(runnerfile.get('run', {}).get('name'), str),
                 environment=_instans_or_error(runnerfile.get('run', {}).get('containerEnv'), dict),
                 user=_instans_or_error(runnerfile.get('run', {}).get('containerUser'), str),
@@ -42,90 +73,120 @@ def config_parser(file_path):
                 network=_instans_or_error(runnerfile.get('run', {}).get('network'), str),
             )
         )
-    else:
-        return Conf(
-            image=None,
-            build=Build(dockerfile=None, build_context=None, build_args=None, cache_from=None),
-            run=Run(name=None, environment=None, user=None, privileged=None, cap_add=None,
-                    security_opt=None, volumes=None, network=None)
-        )
-
-
-def _docker_info(msg, error=False):
-    fg=Color.BRIGHT_MAGENTA
-    if error:
-        fg=Color.BRIGHT_RED
-    ConanOutput().status('\n┌'+'─'*(2+len(msg))+'┐', fg=fg)
-    ConanOutput().status(f'| {msg} |', fg=fg)
-    ConanOutput().status('└'+'─'*(2+len(msg))+'┘\n', fg=fg)
-
 
 class DockerRunner:
-    def __init__(self, conan_api, command, host_profile, build_profile, args, raw_args):
-        import docker.api.build
-        try:
-            docker_base_urls = [
-                None, # Default docker configuration, let the python library detect the socket
-                os.environ.get('DOCKER_HOST'), # Connect to socket defined in DOCKER_HOST
-                'unix:///var/run/docker.sock', # Default linux socket
-                f'unix://{os.path.expanduser("~")}/.rd/docker.sock' # Rancher linux socket
-            ]
-            for base_url in docker_base_urls:
-                try:
-                    ConanOutput().verbose(f'Trying to connect to docker "{base_url or "default"}" socket')
-                    self.docker_client = docker.DockerClient(base_url=base_url, version='auto')
-                    ConanOutput().verbose(f'Connected to docker "{base_url or "default"}" socket')
-                    break
-                except:
-                    continue
-            self.docker_api = self.docker_client.api
-            docker.api.build.process_dockerfile = lambda dockerfile, path: ('Dockerfile', dockerfile)
-        except:
-            raise ConanException("Docker Client failed to initialize."
-                                 "\n - Check if docker is installed and running"
-                                 "\n - Run 'pip install conan[runners]'")
+    def __init__(self, conan_api: ConanAPI, command: str, host_profile: Profile, build_profile: Profile, args: Namespace, raw_args: list[str]):
+        self.logger = ConanOutput()
+        self.docker_client = self._initialize_docker_client()
+        self.docker_api = self.docker_client.api
         self.conan_api = conan_api
         self.build_profile = build_profile
+        self.abs_host_path = self._get_abs_host_path(args.path)
         self.args = args
-        self.abs_host_path = make_abs_path(args.path)
         if args.format:
             raise ConanException("format argument is forbidden if running in a docker runner")
 
-        # Container config
-        # https://containers.dev/implementors/json_reference/
-        self.configfile = config_parser(host_profile.runner.get('configfile'))
+        self.configfile = _ContainerConfig.load(host_profile.runner.get('configfile'))
         self.dockerfile = host_profile.runner.get('dockerfile') or self.configfile.build.dockerfile
         self.docker_build_context = host_profile.runner.get('build_context') or self.configfile.build.build_context
+        self.platform = host_profile.runner.get('platform') or self.configfile.build.platform
         self.image = host_profile.runner.get('image') or self.configfile.image
         if not (self.dockerfile or self.image):
             raise ConanException("'dockerfile' or docker image name is needed")
         self.image = self.image or 'conan-runner-default'
-        self.name = self.configfile.run.name or f'conan-runner-{host_profile.runner.get("suffix", "docker")}'
+        self.name = self.configfile.run.name or host_profile.runner.get("name", "conan-runner-docker")
         self.remove = str(host_profile.runner.get('remove', 'false')).lower() == 'true'
         self.cache = str(host_profile.runner.get('cache', 'clean'))
+        if self.cache not in ['clean', 'copy', 'shared']:
+            raise ConanException(f'Invalid cache value: "{self.cache}". Valid values are: clean, copy, shared')
         self.container = None
+        self.raw_args = raw_args
+        self.command = command
+        self.runner_logger = RunnerOutput(self.name)
 
-        # Runner config>
-        self.abs_runner_home_path = os.path.join(self.abs_host_path, '.conanrunner')
-        self.docker_user_name = self.configfile.run.user or 'root'
-        self.abs_docker_path = os.path.join(f'/{self.docker_user_name}/conanrunner', os.path.basename(self.abs_host_path)).replace("\\","/")
-
-        # Update conan command and some paths to run inside the container
-        raw_args[raw_args.index(args.path)] = self.abs_docker_path
-        self.command = ' '.join([f'conan {command}'] + [f'"{raw_arg}"' if ' ' in raw_arg else raw_arg for raw_arg in raw_args] + ['-f json > create.json'])
-
-    def run(self):
+    def run(self) -> None:
         """
-        run conan inside a Docker continer
+        Run conan inside a Docker container
         """
-        if self.dockerfile:
-            _docker_info(f'Building the Docker image: {self.image}')
-            self.build_image()
-        volumes, environment = self.create_runner_environment()
-        error = False
+        self._build_image()
+        self._start_container()
+        try:
+            self._init_container()
+            self._run_command(self.command)
+            self._update_local_cache()
+        except RunnerException as e:
+            raise ConanException(f'"{e.command}" inside docker fail')
+        finally:
+            if self.container:
+                error = sys.exc_info()[0] is not None # Check if error has been raised
+                log = self.logger.error if error else self.logger.status
+                log('Stopping container')
+                self.container.stop()
+                if self.remove:
+                    log('Removing container')
+                    self.container.remove()
+
+    def _initialize_docker_client(self):
+        import docker
+        import docker.api.build
+
+        docker_base_urls = [
+            None,  # Default docker configuration
+            os.environ.get('DOCKER_HOST'),  # From DOCKER_HOST environment variable
+            'unix:///var/run/docker.sock',  # Default Linux socket
+            f'unix://{os.path.expanduser("~")}/.rd/docker.sock'  # Rancher Linux socket
+        ]
+
+        for base_url in docker_base_urls:
+            try:
+                self.logger.verbose(f'Trying to connect to Docker: "{base_url or "default"}"')
+                client = docker.DockerClient(base_url=base_url, version='auto')
+                self.logger.verbose(f'Connected to Docker: "{base_url or "default"}"')
+                docker.api.build.process_dockerfile = lambda dockerfile, path: ('Dockerfile', dockerfile)
+                return client
+            except Exception:
+                continue
+        raise ConanException("Docker Client failed to initialize."
+                             "\n - Check if docker is installed and running"
+                             "\n - Run 'pip install conan[runners]'")
+
+    def _get_abs_host_path(self, path: str) -> Path:
+        abs_path = make_abs_path(path)
+        if abs_path is None:
+            raise ConanException("Could not determine the absolute path.")
+        return Path(abs_path)
+
+    def _build_image(self) -> None:
+        if not self.dockerfile:
+            return
+        self.logger.status(f'Building the Docker image: {self.image}')
+        dockerfile_file_path = self.dockerfile
+        if os.path.isdir(self.dockerfile):
+            dockerfile_file_path = os.path.join(self.dockerfile, 'Dockerfile')
+        with open(dockerfile_file_path) as f:
+            build_path = self.docker_build_context or os.path.dirname(dockerfile_file_path)
+            self.logger.highlight(f"Dockerfile path: '{dockerfile_file_path}'")
+            self.logger.highlight(f"Docker build context: '{build_path}'\n")
+            docker_build_logs = self.docker_api.build(
+                path=build_path,
+                dockerfile=f.read(),
+                tag=self.image,
+                platform=self.platform,
+                buildargs=self.configfile.build.build_args,
+                cache_from=self.configfile.build.cache_from,
+            )
+        for chunk in docker_build_logs:
+            for line in chunk.decode("utf-8").split('\r\n'):
+                if line:
+                    stream = json.loads(line).get('stream')
+                    if stream:
+                        self.logger.status(stream.strip())
+
+    def _start_container(self) -> None:
+        volumes, environment = self._create_runner_environment()
         try:
             if self.docker_client.containers.list(all=True, filters={'name': self.name}):
-                _docker_info('Starting the docker container')
+                self.logger.status('Starting the docker container', fg=Color.BRIGHT_MAGENTA)
                 self.container = self.docker_client.containers.get(self.name)
                 self.container.start()
             else:
@@ -133,7 +194,7 @@ class DockerRunner:
                     environment.update(self.configfile.run.environment)
                 if self.configfile.run.volumes:
                     volumes.update(self.configfile.run.volumes)
-                _docker_info('Creating the docker container')
+                self.logger.status('Creating the docker container', fg=Color.BRIGHT_MAGENTA)
                 self.container = self.docker_client.containers.run(
                     self.image,
                     "/bin/bash -c 'while true; do sleep 30; done;'",
@@ -147,69 +208,28 @@ class DockerRunner:
                     detach=True,
                     auto_remove=False,
                     network=self.configfile.run.network)
-            _docker_info(f'Container {self.name} running')
+            self.logger.status(f'Container {self.name} running', fg=Color.BRIGHT_MAGENTA)
         except Exception as e:
             raise ConanException(f'Imposible to run the container "{self.name}" with image "{self.image}"'
                                  f'\n\n{str(e)}')
-        try:
-            self.init_container()
-            self.run_command(self.command)
-            self.update_local_cache()
-        except ConanException as e:
-            error = True
-            raise e
-        except RunnerException as e:
-            error = True
-            raise ConanException(f'"{e.command}" inside docker fail'
-                                 f'\n\nLast command output: {str(e.stdout_log)}')
-        finally:
-            if self.container:
-                error_prefix = 'ERROR: ' if error else ''
-                _docker_info(f'{error_prefix}Stopping container', error)
-                self.container.stop()
-                if self.remove:
-                    _docker_info(f'{error_prefix}Removing container', error)
-                    self.container.remove()
-
-    def build_image(self):
-        dockerfile_file_path = self.dockerfile
-        if os.path.isdir(self.dockerfile):
-            dockerfile_file_path = os.path.join(self.dockerfile, 'Dockerfile')
-        with open(dockerfile_file_path) as f:
-            build_path = self.docker_build_context or os.path.dirname(dockerfile_file_path)
-            ConanOutput().highlight(f"Dockerfile path: '{dockerfile_file_path}'")
-            ConanOutput().highlight(f"Docker build context: '{build_path}'\n")
-            docker_build_logs = self.docker_api.build(
-                path=build_path,
-                dockerfile=f.read(),
-                tag=self.image,
-                buildargs=self.configfile.build.build_args,
-                cache_from=self.configfile.build.cache_from,
-            )
-        for chunk in docker_build_logs:
-                for line in chunk.decode("utf-8").split('\r\n'):
-                    if line:
-                        stream = json.loads(line).get('stream')
-                        if stream:
-                            ConanOutput().status(stream.strip())
 
-    def run_command(self, command, workdir=None, log=True):
+    def _run_command(self, command: str, workdir: Optional[str] = None, verbose: bool = True) -> tuple[str, str]:
         workdir = workdir or self.abs_docker_path
-        if log:
-            _docker_info(f'Running in container: "{command}"')
+        log = self.runner_logger.status if verbose else self.runner_logger.verbose
+        log(f'$ {command}', fg=Color.BLUE)
         exec_instance = self.docker_api.exec_create(self.container.id, f"/bin/bash -c '{command}'", workdir=workdir, tty=True)
         exec_output = self.docker_api.exec_start(exec_instance['Id'], tty=True, stream=True, demux=True,)
         stderr_log, stdout_log = '', ''
         try:
             for (stdout_out, stderr_out) in exec_output:
                 if stdout_out is not None:
-                    stdout_log += stdout_out.decode('utf-8', errors='ignore').strip()
-                    if log:
-                        ConanOutput().status(stdout_out.decode('utf-8', errors='ignore').strip())
+                    decoded = stdout_out.decode('utf-8', errors='ignore').strip()
+                    stdout_log += decoded
+                    log(decoded)
                 if stderr_out is not None:
-                    stderr_log += stderr_out.decode('utf-8', errors='ignore').strip()
-                    if log:
-                        ConanOutput().status(stderr_out.decode('utf-8', errors='ignore').strip())
+                    decoded = stderr_out.decode('utf-8', errors='ignore').strip()
+                    stderr_log += decoded
+                    log(decoded)
         except Exception as e:
             if platform.system() == 'Windows':
                 import pywintypes
@@ -222,52 +242,92 @@ class DockerRunner:
             raise RunnerException(command=command, stdout_log=stdout_log, stderr_log=stderr_log)
         return stdout_log, stderr_log
 
-    def create_runner_environment(self):
+    def _get_volumes_and_docker_path(self) -> tuple[dict,str]:
+        app = ConanApp(self.conan_api)
+        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)
+        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
+        # new relative path
+        if hasattr(conanfile, "layout"):
+            try:
+                conanfile.layout()
+                if conanfile.folders.root:
+                    abs_path = self._get_abs_host_path(conanfile.folders.root)
+                    if self.abs_host_path.is_relative_to(abs_path):
+                        abs_docker_base_path /= abs_path.name
+                        volumes = {abs_path: {'bind': abs_docker_base_path.as_posix(), 'mode': 'rw'}}
+                        abs_docker_path = abs_docker_base_path / self.abs_host_path.relative_to(abs_path)
+                        return volumes, abs_docker_path.as_posix()
+            except:
+                pass
+        abs_docker_path = (abs_docker_base_path / self.abs_host_path.name).as_posix()
+        volumes = {self.abs_host_path: {'bind': abs_docker_path, 'mode': 'rw'}}
+        return volumes, abs_docker_path
+
+    def _create_runner_environment(self) -> tuple[dict, dict]:
+        # Runner configuration
+        self.abs_runner_home_path = self.abs_host_path / '.conanrunner'
+        self.docker_user_name = self.configfile.run.user or 'root'
+        volumes, self.abs_docker_path = self._get_volumes_and_docker_path()
         shutil.rmtree(self.abs_runner_home_path, ignore_errors=True)
-        volumes = {self.abs_host_path: {'bind': self.abs_docker_path, 'mode': 'rw'}}
         environment = {'CONAN_RUNNER_ENVIRONMENT': '1'}
 
+        # Update conan command and some paths to run inside the container
+        self.raw_args[self.raw_args.index(self.args.path)] = self.abs_docker_path
+        self.command = ' '.join([f'conan {self.command}'] + [f'"{raw_arg}"' if ' ' in raw_arg else raw_arg for raw_arg in self.raw_args] + ['-f json > create.json'])
+
         if self.cache == 'shared':
             volumes[self.conan_api.home_folder] = {'bind': f'/{self.docker_user_name}/.conan2', 'mode': 'rw'}
 
         if self.cache in ['clean', 'copy']:
             # Copy all conan profiles and config files to docker workspace
-            os.mkdir(self.abs_runner_home_path)
-            shutil.copytree(
-                os.path.join(self.conan_api.home_folder, 'profiles'),
-                os.path.join(self.abs_runner_home_path, 'profiles')
-            )
+            for profile in set(self.args.profile_host + (self.args.profile_build or [])):
+                profile_path = self.conan_api.profiles.get_path(profile)
+                dest_filename = self.abs_runner_home_path / 'profiles' / Path(profile)
+                if not dest_filename.exists():
+                    dest_filename.parent.mkdir(parents=True, exist_ok=True)
+                self.logger.verbose(f"Copying profile '{profile}': {profile_path} -> {dest_filename}")
+                shutil.copy(profile_path, dest_filename)
+            if not self.args.profile_build:
+                dest_filename = self.abs_runner_home_path / 'profiles' / "default"
+                default_build_profile = self.conan_api.profiles.get_default_build()
+                self.logger.verbose(f"Copying default profile: {default_build_profile} -> {dest_filename}")
+                shutil.copy(default_build_profile, dest_filename.as_posix())
+
             for file_name in ['global.conf', 'settings.yml', 'remotes.json']:
-                src_file = os.path.join(self.conan_api.home_folder, file_name)
-                if os.path.exists(src_file):
-                    shutil.copy(src_file, os.path.join(self.abs_runner_home_path, file_name))
+                src_file = Path(self.conan_api.home_folder) / file_name
+                if src_file.exists():
+                    self.logger.verbose(f"Copying {src_file} -> {self.abs_runner_home_path / file_name}")
+                    shutil.copy(src_file, self.abs_runner_home_path / file_name)
 
             if self.cache == 'copy':
-                tgz_path = os.path.join(self.abs_runner_home_path, 'local_cache_save.tgz')
-                _docker_info(f'Save host cache in: {tgz_path}')
+                tgz_path = self.abs_runner_home_path / 'local_cache_save.tgz'
+                self.logger.status(f'Save host cache in: {tgz_path}')
                 self.conan_api.cache.save(self.conan_api.list.select(ListPattern("*:*")), tgz_path)
         return volumes, environment
 
-    def init_container(self):
+    def _init_container(self) -> None:
         min_conan_version = '2.1'
-        stdout, _ = self.run_command('conan --version', log=True)
+        stdout, _ = self._run_command('conan --version', verbose=True)
         docker_conan_version = str(stdout.split('Conan version ')[1].replace('\n', '').replace('\r', '')) # Remove all characters and color
         if Version(docker_conan_version) <= Version(min_conan_version):
-            ConanOutput().status(f'ERROR: conan version inside the container must be greater than {min_conan_version}', fg=Color.BRIGHT_RED)
-            raise ConanException( f'conan version inside the container must be greater than {min_conan_version}')
+            raise ConanException(f'conan version inside the container must be greater than {min_conan_version}')
         if self.cache != 'shared':
-            self.run_command('mkdir -p ${HOME}/.conan2/profiles', log=False)
-            self.run_command('cp -r "'+self.abs_docker_path+'/.conanrunner/profiles/." ${HOME}/.conan2/profiles/.', log=False)
+            self._run_command('mkdir -p ${HOME}/.conan2/profiles', verbose=False)
+            self._run_command('cp -r "'+self.abs_docker_path+'/.conanrunner/profiles/." ${HOME}/.conan2/profiles/.', verbose=False)
+
             for file_name in ['global.conf', 'settings.yml', 'remotes.json']:
-                if os.path.exists( os.path.join(self.abs_runner_home_path, file_name)):
-                    self.run_command('cp "'+self.abs_docker_path+'/.conanrunner/'+file_name+'" ${HOME}/.conan2/'+file_name, log=False)
+                if (self.abs_runner_home_path / file_name).exists():
+                    self._run_command('cp "'+self.abs_docker_path+'/.conanrunner/'+file_name+'" ${HOME}/.conan2/'+file_name, verbose=False)
             if self.cache in ['copy']:
-                self.run_command('conan cache restore "'+self.abs_docker_path+'/.conanrunner/local_cache_save.tgz"')
+                self._run_command('conan cache restore "'+self.abs_docker_path+'/.conanrunner/local_cache_save.tgz"')
 
-    def update_local_cache(self):
+    def _update_local_cache(self) -> None:
         if self.cache != 'shared':
-            self.run_command('conan list --graph=create.json --graph-binaries=build --format=json > pkglist.json', log=False)
-            self.run_command('conan cache save --list=pkglist.json --file "'+self.abs_docker_path+'"/.conanrunner/docker_cache_save.tgz')
-            tgz_path = os.path.join(self.abs_runner_home_path, 'docker_cache_save.tgz')
-            _docker_info(f'Restore host cache from: {tgz_path}')
-            package_list = self.conan_api.cache.restore(tgz_path)
+            self._run_command('conan list --graph=create.json --graph-binaries=build --format=json > pkglist.json', verbose=False)
+            self._run_command('conan cache save --list=pkglist.json --file "'+self.abs_docker_path+'"/.conanrunner/docker_cache_save.tgz')
+            tgz_path = self.abs_runner_home_path / 'docker_cache_save.tgz'
+            self.logger.status(f'Restore host cache from: {tgz_path}')
+            self.conan_api.cache.restore(tgz_path)
diff --git a/conan/internal/runner/output.py b/conan/internal/runner/output.py
new file mode 100644
index 000000000..a1732aa9e
--- /dev/null
+++ b/conan/internal/runner/output.py
@@ -0,0 +1,12 @@
+from conan.api.output import Color, ConanOutput
+
+class RunnerOutput(ConanOutput):
+    def __init__(self, runner_info: str):
+        super().__init__()
+        self.set_warnings_as_errors(True) # Make log errors blocker
+        self._prefix = f"{runner_info} | "
+
+    def _write_message(self, msg, fg=None, bg=None, newline=True):
+        for line in msg.splitlines():
+            super()._write_message(self._prefix, Color.BLACK, Color.BRIGHT_YELLOW, newline=False)
+            super()._write_message(line, fg, bg, newline)
diff --git a/conan/tools/cmake/__init__.py b/conan/tools/cmake/__init__.py
index e57f14a65..d6a1c431f 100644
--- a/conan/tools/cmake/__init__.py
+++ b/conan/tools/cmake/__init__.py
@@ -1,14 +1,31 @@
+from conan.errors import ConanException
 from conan.tools.cmake.toolchain.toolchain import CMakeToolchain
 from conan.tools.cmake.cmake import CMake
 from conan.tools.cmake.layout import cmake_layout
 
 
 def CMakeDeps(conanfile):  # noqa
-    if conanfile.conf.get("tools.cmake.cmakedeps:new", choices=["will_break_next"]):
+    if conanfile.conf.get("tools.cmake.cmakedeps:new",
+                          choices=["will_break_next", "recipe_will_break"]) == "will_break_next":
         from conan.tools.cmake.cmakedeps2.cmakedeps import CMakeDeps2
-        conanfile.output.warning("Using the new CMakeDeps generator, behind the "
+        conanfile.output.warning("Using the new CMakeConfigDeps generator, behind the "
                                  "'tools.cmake.cmakedeps:new' gate conf. This conf will change "
                                  "next release, breaking, so use it only for testing and dev")
         return CMakeDeps2(conanfile)
     from conan.tools.cmake.cmakedeps.cmakedeps import CMakeDeps as _CMakeDeps
     return _CMakeDeps(conanfile)
+
+
+
+def CMakeConfigDeps(conanfile):  # noqa
+    if conanfile.conf.get("tools.cmake.cmakedeps:new",
+                          choices=["will_break_next", "recipe_will_break"]):
+        from conan.tools.cmake.cmakedeps2.cmakedeps import CMakeDeps2
+        conanfile.output.warning("Using the new CMakeConfigDeps generator, behind the "
+                                 "'tools.cmake.cmakedeps:new' gate conf. This conf will change "
+                                 "next release, breaking, so use it only for testing and dev")
+        return CMakeDeps2(conanfile)
+    else:
+        raise ConanException("CMakeConfigDeps is being used in conanfile, but the conf "
+                             "'tools.cmake.cmakedeps:new' is not enabled. This is an incubating"
+                             "feature, please define it to enable CMakeConfigDeps usage")
diff --git a/conan/tools/cmake/cmake.py b/conan/tools/cmake/cmake.py
index ae3acd5ca..4a6a5eca7 100644
--- a/conan/tools/cmake/cmake.py
+++ b/conan/tools/cmake/cmake.py
@@ -185,7 +185,7 @@ class CMake:
 
     def install(self, build_type=None, component=None, cli_args=None, stdout=None, stderr=None):
         """
-        Equivalent to run ``cmake --build . --target=install``
+        Equivalent to running ``cmake --install``
 
         :param component: The specific component to install, if any
         :param build_type: Use it only to override the value defined in the settings.build_type.
diff --git a/conan/tools/cmake/cmakedeps/templates/config.py b/conan/tools/cmake/cmakedeps/templates/config.py
index 1fc1d61d1..f7e9fe1ba 100644
--- a/conan/tools/cmake/cmakedeps/templates/config.py
+++ b/conan/tools/cmake/cmakedeps/templates/config.py
@@ -112,5 +112,12 @@ class ConfigTemplate(CMakeDepsFileTemplate):
                                           REQUIRED_VARS {{ file_name }}_VERSION
                                           VERSION_VAR {{ file_name }}_VERSION)
         mark_as_advanced({{ file_name }}_FOUND {{ file_name }}_VERSION)
+
+        {% for prefix in additional_variables_prefixes %}
+        set({{ prefix }}_FOUND 1)
+        set({{ prefix }}_VERSION "{{ version }}")
+        mark_as_advanced({{ prefix }}_FOUND {{ prefix }}_VERSION)
+        {% endfor %}
+
         {% endif %}
         """)
diff --git a/conan/tools/cmake/cmakedeps2/cmakedeps.py b/conan/tools/cmake/cmakedeps2/cmakedeps.py
index e190c268a..cae3326c0 100644
--- a/conan/tools/cmake/cmakedeps2/cmakedeps.py
+++ b/conan/tools/cmake/cmakedeps2/cmakedeps.py
@@ -61,7 +61,7 @@ class CMakeDeps2:
                 continue
 
             if require.direct:
-                direct_deps.append(dep)
+                direct_deps.append((require, dep))
             config = ConfigTemplate2(self, dep)
             ret[config.filename] = config.content()
             config_version = ConfigVersionTemplate2(self, dep)
@@ -78,14 +78,16 @@ class CMakeDeps2:
     def _print_help(self, direct_deps):
         if direct_deps:
             msg = ["CMakeDeps necessary find_package() and targets for your CMakeLists.txt"]
-            targets = []
-            for dep in direct_deps:
-                msg.append(f"    find_package({self.get_cmake_filename(dep)})")
-                if not dep.cpp_info.exe:
+            link_targets = []
+            for (require, dep) in direct_deps:
+                note = " # Optional. This is a tool-require, can't link its targets" \
+                    if require.build else ""
+                msg.append(f"    find_package({self.get_cmake_filename(dep)}){note}")
+                if not require.build and not dep.cpp_info.exe:
                     target_name = self.get_property("cmake_target_name", dep)
-                    targets.append(target_name or f"{dep.ref.name}::{dep.ref.name}")
-            if targets:
-                msg.append(f"    target_link_libraries(... {' '.join(targets)})")
+                    link_targets.append(target_name or f"{dep.ref.name}::{dep.ref.name}")
+            if link_targets:
+                msg.append(f"    target_link_libraries(... {' '.join(link_targets)})")
             self._conanfile.output.info("\n".join(msg), fg=Color.CYAN)
 
     def set_property(self, dep, prop, value, build_context=False):
diff --git a/conan/tools/cps/cps_deps.py b/conan/tools/cps/cps_deps.py
index 578a87c01..d416d4ad1 100644
--- a/conan/tools/cps/cps_deps.py
+++ b/conan/tools/cps/cps_deps.py
@@ -32,7 +32,7 @@ class CPSDeps:
     def generate(self):
         cps_folder = os.path.join(self._conanfile.folders.base_build, "build", "cps")
         config_name = self._config_name()
-        folder = os.path.join(cps_folder, config_name)
+        folder = os.path.join(cps_folder, config_name, "cps")  # CMake wants a "/cps/" folder
         self._conanfile.output.info(f"[CPSDeps] folder {cps_folder}")
         deps = self._conanfile.dependencies.host.items()
         mapping = {}
diff --git a/conans/client/graph/proxy.py b/conans/client/graph/proxy.py
index d7a364f97..66a56c283 100644
--- a/conans/client/graph/proxy.py
+++ b/conans/client/graph/proxy.py
@@ -14,6 +14,7 @@ class ConanProxy:
         self._cache = conan_app.cache
         self._remote_manager = conan_app.remote_manager
         self._resolved = {}  # Cache of the requested recipes to optimize calls
+        self._legacy_update = conan_app.global_conf.get("core:update_policy", choices=["legacy"])
 
     def get_recipe(self, ref, remotes, update, check_update):
         """
@@ -97,6 +98,7 @@ class ConanProxy:
         output = ConanOutput(scope=str(reference))
 
         results = []
+        need_update = should_update_reference(reference, update) or check_update
         for remote in remotes:
             if remote.allowed_packages and not any(reference.matches(f, is_consumer=False)
                                                    for f in remote.allowed_packages):
@@ -104,11 +106,19 @@ class ConanProxy:
                 continue
             output.info(f"Checking remote: {remote.name}")
             try:
+                if self._legacy_update and need_update:
+                    if not getattr(ConanProxy, "update_policy_legacy_warning", None):
+                        ConanProxy.update_policy_legacy_warning = True
+                        ConanOutput().warning("The 'core:update_policy' conf is deprecated and will "
+                                              "be removed in future versions", warn_tag="deprecated")
+                    refs = self._remote_manager.get_recipe_revisions_references(reference, remote)
+                    results.extend([{'remote': remote, 'ref': ref} for ref in refs])
+                    continue
                 if not reference.revision:
                     ref = self._remote_manager.get_latest_recipe_reference(reference, remote)
                 else:
                     ref = self._remote_manager.get_recipe_revision_reference(reference, remote)
-                if not should_update_reference(reference, update) and not check_update:
+                if not need_update:
                     return remote, ref
                 results.append({'remote': remote, 'ref': ref})
             except NotFoundException:
@@ -117,6 +127,17 @@ class ConanProxy:
         if len(results) == 0:
             return None, None
 
+        if self._legacy_update and need_update:
+            # Use only the first occurence of each revision in the remotes
+            filtered_results = []
+            revisions = set()
+            for r in results:
+                ref = r["ref"]
+                if ref.revision not in revisions:
+                    revisions.add(ref.revision)
+                    filtered_results.append(r)
+            results = filtered_results
+
         remotes_results = sorted(results, key=lambda k: k['ref'].timestamp, reverse=True)
         # get the latest revision from all remotes
         found_rrev = remotes_results[0]
diff --git a/conans/client/rest_client_local_recipe_index.py b/conans/client/rest_client_local_recipe_index.py
index 26b398383..7ab87848a 100644
--- a/conans/client/rest_client_local_recipe_index.py
+++ b/conans/client/rest_client_local_recipe_index.py
@@ -138,6 +138,9 @@ class RestApiClientLocalRecipesIndex:
     def get_recipe_revision_reference(self, ref):
         new_ref = self._export_recipe(ref)
         if new_ref != ref:
+            ConanOutput().warning(f"A specific revision '{ref.repr_notime()}' was requested, but it "
+                                  "doesn't match the current available revision in source. The "
+                                  "local-recipes-index can't provide a specific revision")
             raise RecipeNotFoundException(ref)
         return new_ref
 
diff --git a/pyinstaller.py b/pyinstaller.py
index 75323736c..d6c651499 100644
--- a/pyinstaller.py
+++ b/pyinstaller.py
@@ -105,15 +105,26 @@ def pyinstall(source_folder, onefile=False):
               # Modules that can be imported in ConanFile conan.tools and errors
               "--collect-submodules=conan.cli.commands "
               "--hidden-import=conan.errors "
-              "--hidden-import=conan.tools.microsoft "
-              "--hidden-import=conan.tools.gnu --hidden-import=conan.tools.cmake "
-              "--hidden-import=conan.tools.meson --hidden-import=conan.tools.apple "
-              "--hidden-import=conan.tools.build --hidden-import=conan.tools.env "
+              "--hidden-import=conan.tools.android "
+              "--hidden-import=conan.tools.apple "
+              "--hidden-import=conan.tools.build "
+              "--hidden-import=conan.tools.cmake "
+              "--hidden-import=conan.tools.env "
               "--hidden-import=conan.tools.files "
-              "--hidden-import=conan.tools.google --hidden-import=conan.tools.intel "
-              "--hidden-import=conan.tools.layout --hidden-import=conan.tools.premake "
-              "--hidden-import=conan.tools.qbs --hidden-import=conan.tools.scm "
-              "--hidden-import=conan.tools.system --hidden-import=conan.tools.system.package_manager")
+              "--hidden-import=conan.tools.gnu "
+              "--hidden-import=conan.tools.google "
+              "--hidden-import=conan.tools.intel "
+              "--hidden-import=conan.tools.layout "
+              "--hidden-import=conan.tools.meson "
+              "--hidden-import=conan.tools.microsoft "
+              "--hidden-import=conan.tools.premake "
+              "--hidden-import=conan.tools.qbs "
+              "--hidden-import=conan.tools.ros "
+              "--hidden-import=conan.tools.sbom "
+              "--hidden-import=conan.tools.scm "
+              "--hidden-import=conan.tools.scons "
+              "--hidden-import=conan.tools.system "
+              "--hidden-import=conan.tools.system.package_manager")
 
     if not os.path.exists(pyinstaller_path):
         os.mkdir(pyinstaller_path)
@@ -128,7 +139,6 @@ def pyinstall(source_folder, onefile=False):
             file.write(content)
         win_ver = ["--version-file", win_ver_file]
 
-
     if onefile:
         distpath = os.path.join(pyinstaller_path, "dist", "conan")
     else:


Test patch:
diff --git a/.ci/docker/conan-tests b/.ci/docker/conan-tests
index 17a7cedb4..1ee57ab22 100644
--- a/.ci/docker/conan-tests
+++ b/.ci/docker/conan-tests
@@ -7,6 +7,7 @@ ENV DEBIAN_FRONTEND=noninteractive
 ENV PY36=3.6.15 \
     PY38=3.8.6 \
     PY39=3.9.2 \
+    PY310=3.10.16 \
     PY312=3.12.3 \
     PY313=3.13.0 \
     CMAKE_3_15=/usr/share/cmake-3.15.7/bin/cmake \
@@ -95,6 +96,7 @@ RUN curl https://pyenv.run | bash && \
     pyenv install $PY36 && \
     pyenv install $PY38 && \
     pyenv install $PY39 && \
+    pyenv install $PY310 && \
     pyenv install $PY312 && \
     pyenv install $PY313 && \
     pyenv global $PY39 && \
diff --git a/.github/workflows/linux-tests.yml b/.github/workflows/linux-tests.yml
index 2cbfeaf15..b5af5db16 100644
--- a/.github/workflows/linux-tests.yml
+++ b/.github/workflows/linux-tests.yml
@@ -2,6 +2,10 @@ name: Linux tests
 
 on:
   workflow_call:
+    inputs:
+      python-versions:
+        required: true
+        type: string
 
 jobs:
   build_container:
@@ -49,7 +53,7 @@ jobs:
       options: --user conan
     strategy:
       matrix:
-        python-version: [3.13, 3.9, 3.8, 3.6]
+        python-version: ${{ fromJson(inputs.python-versions) }}
         test-type: [unittests, integration, functional]
         include:
         - test-type: unittests
@@ -92,19 +96,30 @@ jobs:
   linux_docker_tests:
     needs: build_container
     runs-on: ubuntu-latest
+    container:
+      image: ghcr.io/${{ github.repository_owner }}/conan-tests:${{ needs.build_container.outputs.image_tag }}
+      options: --user conan
     strategy:
       matrix:
-        python-version: [3.13, 3.9]
+        # Use modern versions due to docker incompatibility with python <3.8
+        python-version: ${{ github.event_name != 'pull_request' && fromJson('["3.13", "3.9"]') || fromJson('["3.10"]') }}
+
     name: Docker Runner Tests (${{ matrix.python-version }})
     steps:
-      - name: Set up Python ${{ matrix.python-version }}
-        uses: actions/setup-python@v5
-        with:
-          python-version: ${{ matrix.python-version }}
-
       - name: Checkout code
         uses: actions/checkout@v4
 
+      - name: Set up Python ${{ matrix.python-version }}
+        run: |
+          pyenv global ${{ matrix.python-version }}
+          python --version
+
+      - name: Cache pip
+        uses: actions/cache@v4
+        with:
+          path: ~/.cache/pip
+          key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('**/requirements*.txt') }}
+
       - name: Install dependencies
         run: |
           pip install --upgrade pip
diff --git a/.github/workflows/osx-tests.yml b/.github/workflows/osx-tests.yml
index 767132ca8..2162ac28b 100644
--- a/.github/workflows/osx-tests.yml
+++ b/.github/workflows/osx-tests.yml
@@ -2,6 +2,10 @@ name: OSX Tests
 
 on:
   workflow_call:
+    inputs:
+      python-versions:
+        required: true
+        type: string
 
 jobs:
   osx_setup:
@@ -11,10 +15,10 @@ jobs:
       - name: Checkout code
         uses: actions/checkout@v4
 
-      - name: Set up Python 3.8
+      - name: Set up Python
         uses: actions/setup-python@v5
         with:
-          python-version: 3.8
+          python-version: '3.10'
 
       - name: Cache pip packages
         id: cache-pip
@@ -114,7 +118,7 @@ jobs:
     strategy:
       fail-fast: true
       matrix:
-        python-version: [3.8, 3.12, 3.13]
+        python-version: ${{ fromJson(inputs.python-versions) }}
         test-type: [unittests, integration, functional]
         include:
         - test-type: unittests
diff --git a/.github/workflows/win-tests.yml b/.github/workflows/win-tests.yml
index c67b62eab..75e21a273 100644
--- a/.github/workflows/win-tests.yml
+++ b/.github/workflows/win-tests.yml
@@ -2,13 +2,17 @@ name: Windows Tests
 
 on:
   workflow_call:
+    inputs:
+      python-versions:
+        required: true
+        type: string
 
 jobs:
   unit_integration_tests:
     runs-on: windows-2022
     strategy:
       matrix:
-        python-version: [3.13, 3.8, 3.6]
+        python-version: ${{ fromJson(inputs.python-versions) }}
     name: Unit & Integration Tests (${{ matrix.python-version }})
     steps:
       - name: Checkout code
@@ -59,8 +63,7 @@ jobs:
     runs-on: windows-2022
     strategy:
       matrix:
-        python-version: [3.13, 3.8, 3.6]
-
+        python-version: ${{ fromJson(inputs.python-versions) }}
     name: Functional Tests (${{ matrix.python-version }})
     steps:
       - name: Checkout code
diff --git a/conan/test/utils/profiles.py b/conan/test/utils/profiles.py
index aa005ce15..9f2c16381 100644
--- a/conan/test/utils/profiles.py
+++ b/conan/test/utils/profiles.py
@@ -5,11 +5,7 @@ from conan.internal.model.profile import Profile
 from conans.util.files import save
 
 
-def create_profile(folder, name, settings=None, package_settings=None, env=None,
-                   package_env=None, options=None, conf=None):
-
-    package_env = package_env or {}
-
+def create_profile(folder, name, settings=None, package_settings=None, options=None):
     profile = Profile()
     profile.settings = settings or {}
 
@@ -19,18 +15,4 @@ def create_profile(folder, name, settings=None, package_settings=None, env=None,
     if options:
         profile.options = Options(options_values=options)
 
-    if conf:
-        _conf = "\n".join(conf) if isinstance(conf, list) else conf
-        profile.conf.loads(_conf)
-
-    """for package_name, envs in package_env.items():
-        for var_name, value in envs:
-            # Initialize Environment without Conanfile, what else can we do
-            profile._environments.set_default(package_name, Environment(conanfile=None))\
-                .define(var_name, value)
-
-    for var_name, value in env or {}:
-        profile._environments.set_default(None, Environment(conanfile=None)) \
-            .define(var_name, value)"""
-
     save(os.path.join(folder, name), profile.dumps())
diff --git a/conan/test/utils/tools.py b/conan/test/utils/tools.py
index 98de894d4..f195cdfe9 100644
--- a/conan/test/utils/tools.py
+++ b/conan/test/utils/tools.py
@@ -8,17 +8,15 @@ import shutil
 import socket
 import sys
 import textwrap
-import threading
-import time
 import traceback
 import uuid
 import zipfile
-from collections import OrderedDict
 from contextlib import contextmanager
+from inspect import getframeinfo, stack
 from urllib.parse import urlsplit, urlunsplit
 
-import bottle
 import mock
+import pytest
 import requests
 from mock import Mock
 from requests.exceptions import HTTPError
@@ -35,11 +33,8 @@ from conan.api.model import Remote
 from conan.cli.cli import Cli, _CONAN_INTERNAL_CUSTOM_COMMANDS_PATH
 from conan.test.utils.env import environment_update
 from conan.internal.errors import NotFoundException
-from conan.internal.model.manifest import FileTreeManifest
 from conan.api.model import PkgReference
-from conan.internal.model.profile import Profile
 from conan.api.model import RecipeReference
-from conan.internal.model.settings import Settings
 from conan.test.assets.genconanfile import GenConanfile
 from conan.test.utils.artifactory import ArtifactoryServer
 from conan.test.utils.mocks import RedirectedInputStream
@@ -83,28 +78,6 @@ default_profiles = {
         """)
 }
 
-def inc_recipe_manifest_timestamp(cache, reference, inc_time):
-    ref = RecipeReference.loads(reference)
-    path = cache.get_latest_recipe_reference(ref).export()
-    manifest = FileTreeManifest.load(path)
-    manifest.time += inc_time
-    manifest.save(path)
-
-
-def inc_package_manifest_timestamp(cache, package_reference, inc_time):
-    path = cache.get_latest_package_reference(package_reference).package()
-    manifest = FileTreeManifest.load(path)
-    manifest.time += inc_time
-    manifest.save(path)
-
-
-def create_profile(profile=None, settings=None):
-    if profile is None:
-        profile = Profile()
-    if profile.processed_settings is None:
-        profile.processed_settings = settings or Settings()
-    return profile
-
 
 class TestingResponse(object):
     """Wraps a response from TestApp external tool
@@ -442,15 +415,6 @@ class TestClient:
         self.cache_folder = cache_folder or os.path.join(temp_folder(path_with_spaces), ".conan2")
 
         self.requester_class = requester_class
-
-        if servers and len(servers) > 1 and not isinstance(servers, OrderedDict):
-            raise Exception(textwrap.dedent("""
-                Testing framework error: Servers should be an OrderedDict. e.g:
-                    servers = OrderedDict()
-                    servers["r1"] = server
-                    servers["r2"] = TestServer()
-            """))
-
         self.servers = servers or {}
         if servers is not False:  # Do not mess with registry remotes
             self.update_servers()
@@ -629,15 +593,15 @@ class TestClient:
                 msg = " Command succeeded (failure expected): "
             else:
                 msg = " Command failed (unexpectedly): "
-            exc_message = "\n{header}\n{cmd}\n{output_header}\n{output}\n".format(
-                header='{:=^80}'.format(msg),
-                output_header='{:=^80}'.format(" Output: "),
-                cmd=command,
-                output=str(self.stderr) + str(self.stdout) + "\n" + str(self.out)
-            )
+
+            output = str(self.stderr) + str(self.stdout) + "\n"
+            exc_message = f"\n{msg:=^80}\n{command}\n{' Output: ':=^80}\n{output}\n"
             if trace:
-                exc_message += '{:=^80}'.format(" Traceback: ") + f"\n{trace}"
-            raise Exception(exc_message)
+                exc_message += f'{" Traceback: ":=^80}\n{trace}'
+
+            caller = getframeinfo(stack()[3][0])
+            exc_message = f"{caller.filename}:{caller.lineno}" + exc_message
+            pytest.fail(exc_message, pytrace=False)
 
     def save(self, files, path=None, clean_first=False):
         """ helper metod, will store files in the current folder
@@ -853,28 +817,6 @@ def get_free_port():
     return ret
 
 
-class StoppableThreadBottle(threading.Thread):
-    """
-    Real server to test download endpoints
-    """
-
-    def __init__(self, host=None, port=None):
-        self.host = host or "127.0.0.1"
-        self.server = bottle.Bottle()
-        self.port = port or get_free_port()
-        super(StoppableThreadBottle, self).__init__(target=self.server.run,
-                                                    kwargs={"host": self.host, "port": self.port})
-        self.daemon = True
-        self._stop = threading.Event()
-
-    def stop(self):
-        self._stop.set()
-
-    def run_server(self):
-        self.start()
-        time.sleep(1)
-
-
 def zipdir(path, zipfilename):
     with zipfile.ZipFile(zipfilename, 'w', zipfile.ZIP_DEFLATED) as z:
         for root, _, files in os.walk(path):
diff --git a/test/functional/command/runner_test.py b/test/functional/command/runner_test.py
index 9b1e16d87..5eae54cb5 100644
--- a/test/functional/command/runner_test.py
+++ b/test/functional/command/runner_test.py
@@ -542,9 +542,104 @@ def test_create_docker_runner_default_build_profile():
 
     client.save({"host_clean": profile_host})
     client.run("new cmake_lib -d name=pkg -d version=0.2")
-    client.run("create . -pr:h host_clean")
+    client.run("create . -pr:h host_clean -vverbose")
 
+    assert "Copying default profile" in client.out
     assert "Restore: pkg/0.2" in client.out
     assert "Restore: pkg/0.2:8631cf963dbbb4d7a378a64a6fd1dc57558bc2fe" in client.out
     assert "Restore: pkg/0.2:8631cf963dbbb4d7a378a64a6fd1dc57558bc2fe metadata" in client.out
     assert "Removing container" in client.out
+
+
+@pytest.mark.docker_runner
+@pytest.mark.skipif(docker_skip('ubuntu:22.04'), reason="Only docker running")
+def test_create_docker_runner_in_subfolder():
+    client = TestClient()
+    conanfile = textwrap.dedent("""
+        import os
+        from conan import ConanFile
+        from conan.tools.files import load, copy
+        from conan.tools.cmake import CMake
+
+        class Pkg(ConanFile):
+            name = "pkg"
+            version = "1.0"
+            settings = "os", "compiler", "build_type", "arch"
+            generators = "CMakeToolchain"
+
+            def layout(self):
+                self.folders.root = ".."
+                self.folders.source = "."
+                self.folders.build = "build"
+
+            def export_sources(self):
+                folder = os.path.join(self.recipe_folder, "..")
+                copy(self, "*.txt", folder, self.export_sources_folder)
+                copy(self, "src/*.cpp", folder, self.export_sources_folder)
+                copy(self, "include/*.h", folder, self.export_sources_folder)
+
+            def source(self):
+                cmake_file = load(self, "CMakeLists.txt")
+
+            def build(self):
+                path = os.path.join(self.source_folder, "CMakeLists.txt")
+                cmake_file = load(self, path)
+                cmake = CMake(self)
+                cmake.configure()
+                cmake.build()
+
+            def package(self):
+                cmake = CMake(self)
+                cmake.install()
+            """)
+
+    header = textwrap.dedent("""
+        #pragma once
+        void hello();
+        """)
+    source = textwrap.dedent("""
+        #include <iostream>
+        void hello() {
+            std::cout << "Hello!" << std::endl;
+        }
+        """)
+
+    cmakelist = textwrap.dedent("""
+        cmake_minimum_required(VERSION 3.15)
+        project(pkg CXX)
+        add_library(pkg src/hello.cpp)
+        target_include_directories(pkg PUBLIC include)
+        set_target_properties(pkg PROPERTIES PUBLIC_HEADER "include/hello.h")
+        install(TARGETS pkg)
+
+        """)
+
+    profile_host = textwrap.dedent(f"""\
+        [settings]
+        arch={{{{ detect_api.detect_arch() }}}}
+        build_type=Release
+        compiler=gcc
+        compiler.cppstd=gnu17
+        compiler.libcxx=libstdc++11
+        compiler.version=11
+        os=Linux
+        [runner]
+        type=docker
+        dockerfile={dockerfile_path()}
+        build_context={conan_base_path()}
+        image=conan-runner-default-test
+        cache=clean
+        remove=True
+        """)
+
+    client.save({"conan/conanfile.py": conanfile,
+                "conan/host": profile_host,
+                "include/hello.h": header,
+                "src/hello.cpp": source,
+                "CMakeLists.txt": cmakelist})
+
+    with client.chdir("conan"):
+        client.run("create . -pr:h host -vverbose")
+
+    assert "Restore: pkg/1.0" in client.out
+    assert "Removing container" in client.out
diff --git a/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_new.py b/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_new.py
index 64a993e5e..27a7184cb 100644
--- a/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_new.py
+++ b/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_new.py
@@ -868,6 +868,9 @@ class TestToolRequires:
                                                               .with_tool_requires("tool/0.1")})
         c.run("create tool")
         c.run(f"install pkg -g CMakeDeps -c tools.cmake.cmakedeps:new={new_value}")
+        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
         tool_config = c.load("pkg/tool-config.cmake")
         assert 'set(tool_INCLUDE_DIRS' not in tool_config
         assert 'set(tool_INCLUDE_DIR' not in tool_config
diff --git a/test/functional/toolchains/cmake/test_cmake_and_no_soname_flag.py b/test/functional/toolchains/cmake/test_cmake_and_no_soname_flag.py
index 11d61b95f..d2dc005df 100644
--- a/test/functional/toolchains/cmake/test_cmake_and_no_soname_flag.py
+++ b/test/functional/toolchains/cmake/test_cmake_and_no_soname_flag.py
@@ -64,9 +64,9 @@ def test_no_soname_flag(nosoname_property):
     # If `nosoname_property` is False, and we have a library without the SONAME flag,
     # then it should fail
     if nosoname_property is False:
-        with pytest.raises(Exception, match=r"libnosoname.so: cannot open shared object "
-                                            r"file: No such file or directory"):
-            client.run_command(command)
+        client.run_command(command, assert_error=True)
+        assert "libnosoname.so: cannot open shared object file: " \
+               "No such file or directory" in client.out
     else:
         client.run_command(command)
         assert "nosoname/0.1: Hello World Release!" in client.out
diff --git a/test/functional/tools/scm/test_git.py b/test/functional/tools/scm/test_git.py
index d4211ec68..e33194333 100644
--- a/test/functional/tools/scm/test_git.py
+++ b/test/functional/tools/scm/test_git.py
@@ -596,9 +596,8 @@ class TestGitCloneWithArgs:
         c = TestClient()
         git_args = ['--branch', 'foobar']
         c.save({"conanfile.py": self.conanfile.format(url=url, commit=commit, args=str(git_args))})
-        with pytest.raises(Exception):
-            c.run("create .")
-            assert "Remote branch foobar not found" in c.out
+        c.run("create .", assert_error=True)
+        assert "Remote branch foobar not found" in c.out
 
 
 @pytest.mark.tool("git")
diff --git a/test/integration/cache/backup_sources_test.py b/test/integration/cache/backup_sources_test.py
index e992e2e85..2fdb4be52 100644
--- a/test/integration/cache/backup_sources_test.py
+++ b/test/integration/cache/backup_sources_test.py
@@ -1,16 +1,19 @@
 import json
 import os
 import textwrap
+import uuid
 from unittest import mock
 
+import bottle
 import pytest
 from bottle import static_file, HTTPError, request
+from webtest import TestApp
 
 from conan.internal.errors import NotFoundException
 from conan.errors import ConanException
 from conan.test.utils.file_server import TestFileServer
 from conan.test.utils.test_files import temp_folder
-from conan.test.utils.tools import TestClient, StoppableThreadBottle
+from conan.test.utils.tools import TestClient
 from conans.util.files import save, load, rmdir, mkdir, remove
 
 
@@ -290,20 +293,20 @@ class TestDownloadCacheBackupSources:
 
     def test_sources_backup_server_not_found(self):
         conanfile = textwrap.dedent(f"""
-                   from conan import ConanFile
-                   from conan.tools.files import download
-                   class Pkg2(ConanFile):
-                       name = "pkg"
-                       version = "1.0"
-                       def source(self):
-                           download(self, "{self.file_server.fake_url}/internet/myfile.txt", "myfile.txt",
-                                    sha256="unused")
-                   """)
+           from conan import ConanFile
+           from conan.tools.files import download
+           class Pkg2(ConanFile):
+               name = "pkg"
+               version = "1.0"
+               def source(self):
+                   download(self, "{self.file_server.fake_url}/internet/myfile.txt", "myfile.txt",
+                            sha256="unused")
+           """)
 
         self.client.save_home(
             {"global.conf": f"core.sources:download_cache={self.download_cache_folder}\n"
                             f"core.download:retry=0\n"
-                            f"core.sources:download_urls=['http://locahost/nothing-here', 'origin']\n"})
+                            f"core.sources:download_urls=['error_server', 'origin']\n"})
 
         self.client.save({"conanfile.py": conanfile})
         self.client.run("create .", assert_error=True)
@@ -312,37 +315,47 @@ class TestDownloadCacheBackupSources:
     def test_upload_sources_backup_creds_needed(self):
         client = TestClient(default_server_user=True, light=True)
         download_cache_folder = temp_folder()
-        http_server = StoppableThreadBottle()
         http_server_base_folder_backups = temp_folder()
         http_server_base_folder_internet = temp_folder()
 
         save(os.path.join(http_server_base_folder_internet, "myfile.txt"), "Hello, world!")
 
-        def valid_auth(token):
-            auth = request.headers.get("Authorization")
-            if auth == f"Bearer {token}":
-                return
-            return HTTPError(401, "Authentication required")
-
-        @http_server.server.get("/internet/<file>")
-        def get_internet_file(file):
-            return static_file(file, http_server_base_folder_internet)
-
-        @http_server.server.get("/downloader/<file>")
-        def get_file(file):
-            ret = valid_auth("mytoken")
-            return ret or static_file(file, http_server_base_folder_backups)
-
-        @http_server.server.put("/uploader/<file>")
-        def put_file(file):
-            ret = valid_auth("myuploadtoken")
-            if ret:
-                return ret
-            dest = os.path.join(http_server_base_folder_backups, file)
-            with open(dest, 'wb') as f:
-                f.write(request.body.read())
-
-        http_server.run_server()
+        class TestFileServer2:
+
+            def __init__(self):
+                self.fake_url = "http://fake%s.com" % str(uuid.uuid4()).replace("-", "")
+                self.root_app = bottle.Bottle()
+                self.app = TestApp(self.root_app)
+                self._attach_to(self.root_app)
+
+            @staticmethod
+            def _attach_to(app):
+                def valid_auth(token):
+                    auth = request.headers.get("Authorization")
+                    if auth == f"Bearer {token}":
+                        return
+                    return HTTPError(401, "Authentication required")
+
+                @app.route("/internet/<file>", method=["GET"])
+                def get_internet_file(file):
+                    return static_file(file, http_server_base_folder_internet)
+
+                @app.route("/downloader/<file>", method=["GET"])
+                def get_file(file):
+                    ret = valid_auth("mytoken")
+                    return ret or static_file(file, http_server_base_folder_backups)
+
+                @app.route("/uploader/<file>", method=["PUT"])
+                def put_file(file):
+                    ret = valid_auth("myuploadtoken")
+                    if ret:
+                        return ret
+                    dest = os.path.join(http_server_base_folder_backups, file)
+                    with open(dest, 'wb') as f:
+                        f.write(request.body.read())
+
+        http_server = TestFileServer2()
+        client.servers["file_server"] = http_server
 
         sha256 = "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3"
         conanfile = textwrap.dedent(f"""
@@ -352,32 +365,32 @@ class TestDownloadCacheBackupSources:
                 name = "pkg"
                 version = "1.0"
                 def source(self):
-                    download(self, "http://localhost:{http_server.port}/internet/myfile.txt", "myfile.txt",
+                    download(self, "{http_server.fake_url}/internet/myfile.txt", "myfile.txt",
                              sha256="{sha256}")
                     self.output.info(f"CONTENT: {{load(self, 'myfile.txt')}}")
             """)
 
         client.save_home(
             {"global.conf": f"core.sources:download_cache={download_cache_folder}\n"
-                            f"core.sources:download_urls=['http://localhost:{http_server.port}/downloader/', 'origin']\n"
-                            f"core.sources:upload_url=http://localhost:{http_server.port}/uploader/"})
+                            f"core.sources:download_urls=['{http_server.fake_url}/downloader/', 'origin']\n"
+                            f"core.sources:upload_url={http_server.fake_url}/uploader/"})
 
         client.save({"conanfile.py": conanfile})
         client.run("create .", assert_error=True)
-        assert f"ConanException: The source backup server 'http://localhost:{http_server.port}" \
+        assert f"ConanException: The source backup server '{http_server.fake_url}" \
                f"/downloader/' needs authentication" in client.out
         content = {"credentials": [
-            {"url": f"http://localhost:{http_server.port}", "token": "mytoken"}
+            {"url": f"{http_server.fake_url}", "token": "mytoken"}
         ]}
         save(os.path.join(client.cache_folder, "source_credentials.json"), json.dumps(content))
 
         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://localhost:{http_server.port}" \
+        assert f"The source backup server '{http_server.fake_url}" \
                f"/uploader/' needs authentication" in client.out
         content = {"credentials": [
-            {"url": f"http://localhost:{http_server.port}", "token": "myuploadtoken"}
+            {"url": f"{http_server.fake_url}", "token": "myuploadtoken"}
         ]}
         # Now use the correct UPLOAD token
         save(os.path.join(client.cache_folder, "source_credentials.json"), json.dumps(content))
@@ -391,7 +404,7 @@ class TestDownloadCacheBackupSources:
         assert "already in server, skipping upload" in client.out
 
         content = {"credentials": [
-            {"url": f"http://localhost:{http_server.port}", "token": "mytoken"}
+            {"url": f"{http_server.fake_url}", "token": "mytoken"}
         ]}
 
         save(os.path.join(client.cache_folder, "source_credentials.json"), json.dumps(content))
@@ -402,10 +415,10 @@ class TestDownloadCacheBackupSources:
         os.remove(os.path.join(http_server_base_folder_internet, "myfile.txt"))
 
         client.run("source .")
-        assert f"Sources for http://localhost:{http_server.port}/internet/myfile.txt found in remote backup" in client.out
+        assert f"Sources for {http_server.fake_url}/internet/myfile.txt found in remote backup" in client.out
         assert "CONTENT: Hello, world!" in client.out
         client.run("source .")
-        assert f"Source http://localhost:{http_server.port}/internet/myfile.txt retrieved from local download cache" in client.out
+        assert f"Source {http_server.fake_url}/internet/myfile.txt retrieved from local download cache" in client.out
         assert "CONTENT: Hello, world!" in client.out
 
     def test_download_sources_multiurl(self):
@@ -521,8 +534,6 @@ class TestDownloadCacheBackupSources:
     def test_ok_when_origin_authorization_error(self):
         client = TestClient(default_server_user=True, light=True)
         download_cache_folder = temp_folder()
-        http_server = StoppableThreadBottle()
-
         http_server_base_folder_backup1 = temp_folder()
         http_server_base_folder_backup2 = temp_folder()
 
@@ -530,27 +541,40 @@ class TestDownloadCacheBackupSources:
         save(os.path.join(http_server_base_folder_backup2, sha256), "Hello, world!")
         save(os.path.join(http_server_base_folder_backup2, sha256 + ".json"), '{"references": {}}')
 
-        @http_server.server.get("/internet/<file>")
-        def get_internet_file(file):
-            return HTTPError(401, "You Are Not Allowed Here")
-
-        @http_server.server.get("/downloader1/<file>")
-        def get_file(file):
-            return static_file(file, http_server_base_folder_backup1)
-
-        @http_server.server.put("/downloader1/<file>")
-        def put_file(file):
-            if file in os.listdir(http_server_base_folder_backup1):
-                return HTTPError(401, "You Are Not Allowed Here")
-            dest = os.path.join(http_server_base_folder_backup1, file)
-            with open(dest, 'wb') as f:
-                f.write(request.body.read())
-
-        @http_server.server.get("/downloader2/<file>")
-        def get_file_2(file):
-            return static_file(file, http_server_base_folder_backup2)
-
-        http_server.run_server()
+        class TestFileServer2:
+
+            def __init__(self, store=None):
+                self.store = store or temp_folder(path_with_spaces=False)
+                mkdir(self.store)
+                self.fake_url = "http://fake%s.com" % str(uuid.uuid4()).replace("-", "")
+                self.root_app = bottle.Bottle()
+                self.app = TestApp(self.root_app)
+                self._attach_to(self.root_app, self.store)
+
+            @staticmethod
+            def _attach_to(app, store):
+                @app.route("/internet/<file>", method=["GET"])
+                def get_internet_file(file):
+                    return HTTPError(401, "You Are Not Allowed Here")
+
+                @app.route("/downloader1/<file>", method=["GET"])
+                def get_file(file):
+                    return static_file(file, http_server_base_folder_backup1)
+
+                @app.route("/downloader1/<file>", method=["PUT"])
+                def put_file(file):
+                    if file in os.listdir(http_server_base_folder_backup1):
+                        return HTTPError(401, "You Are Not Allowed Here")
+                    dest = os.path.join(http_server_base_folder_backup1, file)
+                    with open(dest, 'wb') as f:
+                        f.write(request.body.read())
+
+                @app.route("/downloader2/<file>", method=["GET"])
+                def get_file_2(file):
+                    return static_file(file, http_server_base_folder_backup2)
+
+        http_server = TestFileServer2()
+        client.servers["file_server"] = http_server
 
         conanfile = textwrap.dedent(f"""
            from conan import ConanFile
@@ -559,18 +583,18 @@ class TestDownloadCacheBackupSources:
                name = "pkg"
                version = "1.0"
                def source(self):
-                   download(self, "http://localhost:{http_server.port}/internet/myfile.txt", "myfile.txt",
+                   download(self, "{http_server.fake_url}/internet/myfile.txt", "myfile.txt",
                             sha256="{sha256}")
            """)
         client.save_home({"global.conf": f"core.sources:download_cache={download_cache_folder}\n"
-                          f"core.sources:download_urls=['http://localhost:{http_server.port}/downloader1/', "
-                          f"'origin', 'http://localhost:{http_server.port}/downloader2/']\n"
-                          f"core.sources:upload_url=http://localhost:{http_server.port}/downloader1/\n"
+                          f"core.sources:download_urls=['{http_server.fake_url}/downloader1/', "
+                          f"'origin', '{http_server.fake_url}/downloader2/']\n"
+                          f"core.sources:upload_url={http_server.fake_url}/downloader1/\n"
                           "core.upload:retry=0\ncore.download:retry=0"})
 
         client.save({"conanfile.py": conanfile})
         client.run("create .")
-        assert f"Sources for http://localhost:{http_server.port}/internet/myfile.txt found in remote backup http://localhost:{http_server.port}/downloader2/" in client.out
+        assert f"Sources for {http_server.fake_url}/internet/myfile.txt found in remote backup {http_server.fake_url}/downloader2/" in client.out
         # TODO: Check better message with Authentication error message
         assert "failed in 'origin'" in client.out
 
@@ -753,13 +777,13 @@ class TestDownloadCacheBackupSources:
         save(os.path.join(http_server_base_folder_internet, "myfile.txt"), "Hello, world!")
 
         conanfile = textwrap.dedent(f"""
-                               from conan import ConanFile
-                               from conan.tools.files import download
-                               class Pkg2(ConanFile):
-                                   def source(self):
-                                       download(self, "{self.file_server.fake_url}/internet/myfile.txt", "myfile.txt",
-                                                sha256="{sha256}")
-                               """)
+           from conan import ConanFile
+           from conan.tools.files import download
+           class Pkg2(ConanFile):
+               def source(self):
+                   download(self, "{self.file_server.fake_url}/internet/myfile.txt", "myfile.txt",
+                            sha256="{sha256}")
+           """)
 
         self.client.save_home(
             {"global.conf": f"core.sources:download_cache={self.download_cache_folder}\n"
@@ -795,13 +819,13 @@ class TestDownloadCacheBackupSources:
         save(os.path.join(http_server_base_folder_internet, "myfile.txt"), "Hello, world!")
 
         conanfile = textwrap.dedent(f"""
-                   from conan import ConanFile
-                   from conan.tools.files import download
-                   class Pkg2(ConanFile):
-                       def source(self):
-                           download(self, "{self.file_server.fake_url}/internet/myfile.txt", "myfile.txt",
-                                    sha256="{sha256}")
-                   """)
+           from conan import ConanFile
+           from conan.tools.files import download
+           class Pkg2(ConanFile):
+               def source(self):
+                   download(self, "{self.file_server.fake_url}/internet/myfile.txt", "myfile.txt",
+                            sha256="{sha256}")
+           """)
 
         self.client.save_home(
             {"global.conf": f"core.sources:download_cache={self.download_cache_folder}\n"
diff --git a/test/integration/command/install/install_update_test.py b/test/integration/command/install/install_update_test.py
index 5a5ab4b77..450d771be 100644
--- a/test/integration/command/install/install_update_test.py
+++ b/test/integration/command/install/install_update_test.py
@@ -3,7 +3,7 @@ import textwrap
 from time import sleep
 
 from conan.api.model import RecipeReference
-from conan.test.utils.tools import TestClient, GenConanfile
+from conan.test.utils.tools import TestClient, GenConanfile, TestServer
 from conans.util.files import load
 
 
@@ -165,3 +165,71 @@ def test_install_update_repeated_tool_requires():
     c.run("create libc")
     c.run("install libc --update -pr=profile")
     assert 1 == str(c.out).count("tool/0.1: Checking remote")
+
+
+class TestUpdateOldPolicy:
+    def test_multi_remote_update_resolution(self):
+        c = TestClient(servers={"r1": TestServer(), "r2": TestServer(), "r3": TestServer()},
+                       inputs=["admin", "password"] * 3, light=True)
+        c.save({"conanfile.py": GenConanfile("pkg", "0.1")})
+        c.run("export .")
+        rev1 = c.exported_recipe_revision()
+        c.run("upload * -r=r1 -c")
+        # second revision
+        c.save({"conanfile.py": GenConanfile("pkg", "0.1").with_class_attribute("auther = 'me'")})
+        c.run("export .")
+        rev2 = c.exported_recipe_revision()
+        assert rev1 != rev2
+        c.run("upload * -r=r2 -c")  # By default uploads latest revisions only
+        assert rev1 not in c.out
+        assert rev2 in c.out
+        # going back to the previous revision
+        c.save({"conanfile.py": GenConanfile("pkg", "0.1")})
+        c.run("export .")  # Makes it the latest
+        rev3 = c.exported_recipe_revision()
+        assert rev1 == rev3
+        c.run("upload * -r=r3 -c")  # By default uploads latest revisions only
+        assert rev3 in c.out
+        assert rev2 not in c.out
+
+        # now test the --update, it will pick up the latest revision, which is r3
+        c.run("remove * -c")
+        c.run("graph info --requires=pkg/0.1 --update")
+        assert f"pkg/0.1#{rev3} - Downloaded (r3)" in c.out
+
+        # But if we enable order-based first found timestamp, it will pick up r2
+        c.run("remove * -c")
+        c.run("graph info --requires=pkg/0.1 --update -cc core:update_policy=legacy")
+        assert "The 'core:update_policy' conf is deprecated and will be removed" in c.out
+        assert f"pkg/0.1#{rev2} - Downloaded (r2)" in c.out
+
+    def test_multi_remote_update_resolution_2_remotes(self):
+        c = TestClient(servers={"r1": TestServer(), "r2": TestServer()},
+                       inputs=["admin", "password"] * 2, light=True)
+        c.save({"conanfile.py": GenConanfile("pkg", "0.1")})
+        c.run("export .")
+        rev1 = c.exported_recipe_revision()
+        c.run("upload * -r=r1 -c")
+        # second revision
+        c.save({"conanfile.py": GenConanfile("pkg", "0.1").with_class_attribute("auther = 'me'")})
+        c.run("export .")
+        rev2 = c.exported_recipe_revision()
+        assert rev1 != rev2
+        c.run("upload * -r=r1 -c")
+        c.run("list *#* -r=r1")
+
+        # Upload the first, old revision to the other remote
+        c.run(f"upload pkg/0.1#{rev1} -r=r2 -c")
+        assert rev1 in c.out
+        assert rev2 not in c.out
+        c.run("list *#* -r=r2")
+
+        # now test the --update, it will pick up the latest revision, which is r3
+        c.run("remove * -c")
+        c.run("graph info --requires=pkg/0.1 --update")
+        assert f"pkg/0.1#{rev1} - Downloaded (r2)" in c.out
+
+        # But if we enable order-based first found timestamp, it will pick up r2
+        c.run("remove * -c")
+        c.run("graph info --requires=pkg/0.1 --update -cc core:update_policy=legacy")
+        assert f"pkg/0.1#{rev2} - Downloaded (r1)" in c.out
diff --git a/test/integration/command/test_remote_users.py b/test/integration/command/test_remote_users.py
index aed12fa5f..46b21945c 100644
--- a/test/integration/command/test_remote_users.py
+++ b/test/integration/command/test_remote_users.py
@@ -4,7 +4,6 @@ import time
 from collections import OrderedDict
 from datetime import timedelta
 
-import pytest
 from mock import patch
 
 from conan.internal.api.remotes.localdb import LocalDB
@@ -18,12 +17,10 @@ class TestUser:
         """ Test that proper error is reported when no remotes are defined and conan user is executed
         """
         client = TestClient()
-        with pytest.raises(Exception):
-            client.run("remote list-users")
+        client.run("remote list-users", assert_error=True)
         assert "ERROR: No remotes defined" in client.out
 
-        with pytest.raises(Exception):
-            client.run("remote login wrong_remote foo -p bar")
+        client.run("remote login wrong_remote foo -p bar", assert_error=True)
         assert "ERROR: Remote 'wrong_remote' can't be found or is disabled" in client.out
 
     def test_command_user_list(self):
@@ -35,8 +32,7 @@ class TestUser:
         client = TestClient(servers=servers)
 
         # Test with wrong remote right error is reported
-        with pytest.raises(Exception):
-            client.run("remote login Test_Wrong_Remote foo")
+        client.run("remote login Test_Wrong_Remote foo", assert_error=True)
         assert "ERROR: Remote 'Test_Wrong_Remote' can't be found or is disabled" in client.out
 
         # Test user list for all remotes is reported
diff --git a/test/integration/conanfile/conan_data_test.py b/test/integration/conanfile/conan_data_test.py
index 69f04b557..2cd13182c 100644
--- a/test/integration/conanfile/conan_data_test.py
+++ b/test/integration/conanfile/conan_data_test.py
@@ -437,8 +437,7 @@ def test_trim_conandata_as_hook_without_conandata(raise_if_missing):
 
     c.save({"conanfile.py": GenConanfile("pkg")})
     if raise_if_missing:
-        with pytest.raises(Exception, match="conandata.yml file doesn't exist") as exception:
-            c.run("export . --version=1.0")
+        c.run("export . --version=1.0", assert_error=True)
     else:
         c.run("export . --version=1.0")
         assert c.exported_recipe_revision() == "a9ec2e5fbb166568d4670a9cd1ef4b26"
diff --git a/test/integration/configuration/profile_test.py b/test/integration/configuration/profile_test.py
index b8a6f3396..ebf563b17 100644
--- a/test/integration/configuration/profile_test.py
+++ b/test/integration/configuration/profile_test.py
@@ -33,9 +33,8 @@ class AConan(ConanFile):
 """
 
 
-def create_profile(folder, name, settings=None, package_settings=None, env=None,
-                   package_env=None, options=None):
-    _create_profile(folder, name, settings, package_settings, env, package_env, options)
+def create_profile(folder, name, settings=None, package_settings=None, options=None):
+    _create_profile(folder, name, settings, package_settings, options)
     content = load(os.path.join(folder, name))
     content = "include(default)\n    \n" + content
     save(os.path.join(folder, name), content)
diff --git a/test/integration/cps/test_cps.py b/test/integration/cps/test_cps.py
index e82527398..95280638e 100644
--- a/test/integration/cps/test_cps.py
+++ b/test/integration/cps/test_cps.py
@@ -17,7 +17,7 @@ def test_cps():
 
     settings = "-s os=Windows -s compiler=msvc -s compiler.version=191 -s arch=x86_64"
     c.run(f"install --requires=pkg/0.1 {settings} -g CPSDeps")
-    pkg = json.loads(c.load("build/cps/msvc-191-x86_64-release/pkg.cps"))
+    pkg = json.loads(c.load("build/cps/msvc-191-x86_64-release/cps/pkg.cps"))
     assert pkg["name"] == "pkg"
     assert pkg["version"] == "0.1"
     assert pkg["license"] == "MIT"
@@ -39,7 +39,7 @@ def test_cps_static_lib():
 
     settings = "-s os=Windows -s compiler=msvc -s compiler.version=191 -s arch=x86_64"
     c.run(f"install --requires=pkg/0.1 {settings} -g CPSDeps")
-    pkg = json.loads(c.load("build/cps/msvc-191-x86_64-release/pkg.cps"))
+    pkg = json.loads(c.load("build/cps/msvc-191-x86_64-release/cps/pkg.cps"))
     assert pkg["name"] == "pkg"
     assert pkg["version"] == "0.1"
     assert pkg["configurations"] == ["release"]
@@ -56,7 +56,7 @@ def test_cps_header():
 
     settings = "-s os=Windows -s compiler=msvc -s compiler.version=191 -s arch=x86_64"
     c.run(f"install --requires=pkg/0.1 {settings} -g CPSDeps")
-    pkg = json.loads(c.load("build/cps/msvc-191-x86_64-release/pkg.cps"))
+    pkg = json.loads(c.load("build/cps/msvc-191-x86_64-release/cps/pkg.cps"))
     assert pkg["name"] == "pkg"
     assert pkg["version"] == "0.1"
     assert "configurations" not in "pkg"
@@ -173,7 +173,7 @@ def test_extended_cpp_info():
 
     settings = "-s os=Windows -s compiler=msvc -s compiler.version=191 -s arch=x86_64"
     c.run(f"install --requires=pkg/0.1 {settings} -g CPSDeps")
-    pkg = json.loads(c.load("build/cps/msvc-191-x86_64-release/pkg.cps"))
+    pkg = json.loads(c.load("build/cps/msvc-191-x86_64-release/cps/pkg.cps"))
     assert pkg["name"] == "pkg"
     assert pkg["version"] == "0.1"
     assert pkg["default_components"] == ["pkg"]
diff --git a/test/integration/remote/auth_test.py b/test/integration/remote/auth_test.py
index af9b14d86..6e5019a07 100644
--- a/test/integration/remote/auth_test.py
+++ b/test/integration/remote/auth_test.py
@@ -62,12 +62,13 @@ class AuthorizeTest(unittest.TestCase):
 
     def test_auth_with_env(self):
 
-        def _upload_with_credentials(credentials):
+        def _upload_with_credentials(credentials, assert_error=False):
             cli = TestClient(servers=self.servers)
             save(os.path.join(cli.current_folder, CONANFILE), conan_content)
             cli.run("export . --user=lasote --channel=testing")
             with environment_update(credentials):
-                cli.run("upload %s -r default --only-recipe" % str(self.ref))
+                cli.run("upload %s -r default --only-recipe" % str(self.ref),
+                        assert_error=assert_error)
             return cli
 
         # Try with remote name in credentials
@@ -89,10 +90,9 @@ class AuthorizeTest(unittest.TestCase):
         self.assertIn("Got password '******' from environment", client.out)
 
         # Bad pass raise
-        with self.assertRaises(Exception):
-            client = _upload_with_credentials({"CONAN_PASSWORD": "bad",
-                                               "CONAN_LOGIN_USERNAME": "pepe"})
-            self.assertIn("Too many failed login attempts, bye!", client.out)
+        client = _upload_with_credentials({"CONAN_PASSWORD": "bad",
+                                           "CONAN_LOGIN_USERNAME": "pepe"}, assert_error=True)
+        self.assertIn("Authentication error in remote 'default'", client.out)
 
     def test_max_retries(self):
         """Bad login 3 times"""
diff --git a/test/integration/remote/test_local_recipes_index.py b/test/integration/remote/test_local_recipes_index.py
index 12123742c..bbff7cebe 100644
--- a/test/integration/remote/test_local_recipes_index.py
+++ b/test/integration/remote/test_local_recipes_index.py
@@ -332,6 +332,23 @@ class TestErrorsUx:
         c.run("install --requires=zlib/[*] --build missing", assert_error=True)
         assert "NameError: name 'ConanFile' is not defined" in c.out
 
+    def test_require_revision(self):
+        # https://github.com/conan-io/conan/issues/17814
+        folder = temp_folder()
+        recipes_folder = os.path.join(folder, "recipes")
+        zlib_config = textwrap.dedent("""
+           versions:
+             "1.2.11":
+               folder: all
+           """)
+        save_files(recipes_folder,
+                   {"zlib/config.yml": zlib_config,
+                    "zlib/all/conanfile.py": str(GenConanfile("zlib"))})
+        c = TestClient(light=True)
+        c.run(f"remote add local '{folder}'")
+        c.run("install --requires=zlib/1.2.11#rev1", assert_error=True)
+        assert "A specific revision 'zlib/1.2.11#rev1' was requested" in c.out
+
 
 class TestPythonRequires:
     @pytest.fixture(scope="class")
@@ -361,6 +378,7 @@ class TestPythonRequires:
         assert "pyreq/1.0#a0d63ca853edefa33582a24a1bb3c75f - Downloaded (local)" in c.out
         assert "pkg/1.0: Created package" in c.out
 
+
 class TestUserChannel:
     @pytest.fixture(scope="class")
     def c3i_user_channel_folder(self):
@@ -373,8 +391,8 @@ class TestUserChannel:
                   "2.0":
                     folder: other
                 """)
-        pkg = str(GenConanfile("pkg").with_class_attribute("user='myuser'")\
-                                    .with_class_attribute("channel='mychannel'"))
+        pkg = str(GenConanfile("pkg").with_class_attribute("user='myuser'")
+                                     .with_class_attribute("channel='mychannel'"))
         save_files(recipes_folder,
                    {"pkg/config.yml": config,
                     "pkg/all/conanfile.py": pkg,
diff --git a/test/integration/toolchains/cmake/cmakedeps/test_cmakedeps.py b/test/integration/toolchains/cmake/cmakedeps/test_cmakedeps.py
index 43e203488..457300b91 100644
--- a/test/integration/toolchains/cmake/cmakedeps/test_cmakedeps.py
+++ b/test/integration/toolchains/cmake/cmakedeps/test_cmakedeps.py
@@ -715,6 +715,7 @@ def test_cmakedeps_set_legacy_variable_name():
     conanfile = base_conanfile + """
     def package_info(self):
         self.cpp_info.set_property("cmake_file_name", "CMakeFileName")
+        self.cpp_info.set_property("cmake_find_mode", "both")
     """
     client.save({"dep/conanfile.py": conanfile})
     client.run("create dep")
@@ -725,6 +726,9 @@ def test_cmakedeps_set_legacy_variable_name():
     cmake_variables = ["VERSION_STRING", "INCLUDE_DIRS", "INCLUDE_DIR", "LIBRARIES", "DEFINITIONS"]
     for variable in cmake_variables:
         assert f"CMakeFileName_{variable}" in dep_config
+    dep_find = client.load("FindCMakeFileName.cmake")
+    for variable in cmake_variables:
+        assert f"CMakeFileName_{variable}" in dep_find
 
     consumer_conanfile = textwrap.dedent("""
         from conan import ConanFile
@@ -752,6 +756,16 @@ def test_cmakedeps_set_legacy_variable_name():
         assert f"PREFIX_{variable}" in dep_config
         assert f"prefix_{variable}" in dep_config
 
+    dep_find = client.load(os.path.join("consumer", "FindCMakeFileName.cmake"))
+    for variable in cmake_variables:
+        assert f"CMakeFileName_{variable}" in dep_find
+        assert f"PREFIX_{variable}" in dep_find
+        assert f"prefix_{variable}" in dep_find
+    assert "set(prefix_FOUND 1)" in dep_find
+    assert "set(PREFIX_FOUND 1)" in dep_find
+    assert 'set(prefix_VERSION "1.0")' in dep_find
+    assert 'set(PREFIX_VERSION "1.0")' in dep_find
+
     conanfile = base_conanfile + """
     def package_info(self):
         self.cpp_info.set_property("cmake_file_name", "NewCMakeFileName")
@@ -901,10 +915,10 @@ def test_alias_cmakedeps_set_property():
              """)})
     tc.run("create dep")
     tc.run("install .")
-    targetsData = tc.load("depTargets.cmake")
-    assert "add_library(dep::dep" in targetsData
-    assert "add_library(alias" in targetsData
-    assert "add_library(dep::other_name" in targetsData
+    targets_data = tc.load("depTargets.cmake")
+    assert "add_library(dep::dep" in targets_data
+    assert "add_library(alias" in targets_data
+    assert "add_library(dep::other_name" in targets_data
 
-    assert "add_library(component_alias" in targetsData
-    assert "add_library(dep::my_aliased_component" in targetsData
+    assert "add_library(component_alias" in targets_data
+    assert "add_library(dep::my_aliased_component" in targets_data
diff --git a/test/integration/toolchains/cmake/cmakedeps2/__init__.py b/test/integration/toolchains/cmake/cmakedeps2/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/integration/toolchains/cmake/cmakedeps2/test_cmakedeps.py b/test/integration/toolchains/cmake/cmakedeps2/test_cmakedeps.py
index 4b6001749..83420d6d6 100644
--- a/test/integration/toolchains/cmake/cmakedeps2/test_cmakedeps.py
+++ b/test/integration/toolchains/cmake/cmakedeps2/test_cmakedeps.py
@@ -1,6 +1,7 @@
 import re
 import textwrap
 
+from conan.test.assets.genconanfile import GenConanfile
 from conan.test.utils.tools import TestClient
 
 new_value = "will_break_next"
@@ -90,3 +91,64 @@ def test_cmakedeps_transitive_paths():
     assert not re.search(r"list\(PREPEND CMAKE_PROGRAM_PATH /bina\"", cmake_paths)
     assert re.search(r"list\(PREPEND CMAKE_LIBRARY_PATH \".*/libb.*/p/libb\" \".*/liba.*/p/liba\"\)", cmake_paths)
     assert re.search(r"list\(PREPEND CMAKE_INCLUDE_PATH \".*/libb.*/p/includeb\" \".*/liba.*/p/includea\"\)", cmake_paths)
+
+
+def test_cmakeconfigdeps_recipe():
+    c = TestClient()
+    conanfile = textwrap.dedent("""
+        from conan.tools.cmake import CMakeConfigDeps
+        from conan import ConanFile
+        class TestConan(ConanFile):
+            settings = "build_type"
+            requires = "dep/0.1"
+            def generate(self):
+                deps = CMakeConfigDeps(self)
+                deps.generate()
+    """)
+    c.save({"dep/conanfile.py": GenConanfile("dep", "0.1"),
+            "app/conanfile.py": conanfile})
+    c.run("create dep")
+    c.run("install app", assert_error=True)
+    assert "CMakeConfigDeps is being used in conanfile, but the conf " \
+           "'tools.cmake.cmakedeps:new' is not enabled" in c.out
+    c.run("install app -c tools.cmake.cmakedeps:new=will_break_next")
+    # will not fail, still warn
+    assert "WARN: Using the new CMakeConfigDeps generator" in c.out
+    # The only-recipe also not fails
+    c.run("install app -c tools.cmake.cmakedeps:new=recipe_will_break")
+    # will not fail
+    assert "WARN: Using the new CMakeConfigDeps generator" in c.out
+
+    # attribute generator
+    conanfile = textwrap.dedent("""
+        from conan.tools.cmake import CMakeConfigDeps
+        from conan import ConanFile
+        class TestConan(ConanFile):
+            settings = "build_type"
+            requires = "dep/0.1"
+            generators = "CMakeConfigDeps"
+        """)
+    c.save({"app/conanfile.py": conanfile}, clean_first=True)
+    c.run("install app", assert_error=True)
+    assert "CMakeConfigDeps is being used in conanfile, but the conf " \
+           "'tools.cmake.cmakedeps:new' is not enabled" in c.out
+    c.run("install app -c tools.cmake.cmakedeps:new=will_break_next")
+    assert "WARN: Using the new CMakeConfigDeps generator" in c.out
+    c.run("install app -c tools.cmake.cmakedeps:new=recipe_will_break")
+    assert "WARN: Using the new CMakeConfigDeps generator" in c.out
+
+    # conanfile.txt
+    conanfile = textwrap.dedent("""
+        [requires]
+        dep/0.1
+        [generators]
+        CMakeConfigDeps
+        """)
+    c.save({"app/conanfile.txt": conanfile}, clean_first=True)
+    c.run("install app", assert_error=True)
+    assert "CMakeConfigDeps is being used in conanfile, but the conf " \
+           "'tools.cmake.cmakedeps:new' is not enabled" in c.out
+    c.run("install app -c tools.cmake.cmakedeps:new=will_break_next")
+    assert "WARN: Using the new CMakeConfigDeps generator" in c.out
+    c.run("install app -c tools.cmake.cmakedeps:new=recipe_will_break")
+    assert "WARN: Using the new CMakeConfigDeps generator" in c.out