Repository: conan-io/conan
Pull request #16511 (2024-06-20 19:06:58)

new lock update subcommand
Changelog: Feature: New ``conan lock update`` subcommand to remove + add a reference in the same command
Docs: https://github.com/conan-io/docs/pull/XXXX

Close https://github.com/conan-io/conan/issues/16377


Patch:
diff --git a/README.md b/README.md
index 8c76da2cd..7c298f615 100644
--- a/README.md
+++ b/README.md
@@ -128,7 +128,7 @@ OK
 To run specific tests, you can specify the test name too, something like:
 
 ```bash
-$ python -m pytest conans/test/functional/command/export_test.py::TestRevisionModeSCM::test_revision_mode_scm -s
+$ python -m pytest test/functional/command/export_test.py::TestRevisionModeSCM::test_revision_mode_scm -s
 ```
 
 The `-s` argument can be useful to see some output that otherwise is captured by *pytest*.
diff --git a/conan/api/subapi/lockfile.py b/conan/api/subapi/lockfile.py
index e2d51c1f9..1d193bbcd 100644
--- a/conan/api/subapi/lockfile.py
+++ b/conan/api/subapi/lockfile.py
@@ -64,11 +64,13 @@ class LockfileAPI:
         else:
             python_requires = []
         python_requires = python_requires + ([ref] if is_python_require else [])
-        lockfile = self.add_lockfile(lockfile,
+        new_lock = self.add_lockfile(lockfile,
                                      requires=[ref] if is_require else None,
                                      python_requires=python_requires,
                                      build_requires=[ref] if is_build_require else None)
-        return lockfile
+        if lockfile is None:  # If there was no lockfile, it is a partial one to lock export
+            new_lock.partial = True
+        return new_lock
 
     @staticmethod
     def update_lockfile(lockfile, graph, lock_packages=False, clean=False):
@@ -83,7 +85,6 @@ class LockfileAPI:
                      config_requires=None):
         if lockfile is None:
             lockfile = Lockfile()  # create a new lockfile
-            lockfile.partial = True
 
         lockfile.add(requires=requires, build_requires=build_requires,
                      python_requires=python_requires, config_requires=config_requires)
diff --git a/conan/cli/commands/lock.py b/conan/cli/commands/lock.py
index 984661a98..0d7c3f395 100644
--- a/conan/cli/commands/lock.py
+++ b/conan/cli/commands/lock.py
@@ -11,7 +11,7 @@ from conans.model.recipe_ref import RecipeReference
 
 
 @conan_command(group="Consumer")
-def lock(conan_api, parser, *args):
+def lock(conan_api, parser, *args):  # noqa
     """
     Create or manage lockfiles.
     """
@@ -63,7 +63,7 @@ def lock_create(conan_api, parser, subparser, *args):
 
 
 @conan_subcommand()
-def lock_merge(conan_api, parser, subparser, *args):
+def lock_merge(conan_api, parser, subparser, *args): # noqa
     """
     Merge 2 or more lockfiles.
     """
@@ -132,7 +132,7 @@ def lock_add(conan_api, parser, subparser, *args):
 @conan_subcommand()
 def lock_remove(conan_api, parser, subparser, *args):
     """
-    Remove requires, build-requires or python-requires from an existing or new lockfile.
+    Remove requires, build-requires or python-requires from an existing lockfile.
     References can be supplied with and without revisions like "--requires=pkg/version",
     """
     subparser.add_argument('--requires', action="append", help='Remove references to lockfile.')
@@ -154,3 +154,28 @@ def lock_remove(conan_api, parser, subparser, *args):
                                                   build_requires=args.build_requires,
                                                   config_requires=args.config_requires)
     conan_api.lockfile.save_lockfile(lockfile, args.lockfile_out)
+
+
+@conan_subcommand()
+def lock_update(conan_api, parser, subparser, *args):
+    """
+    Update requires, build-requires or python-requires from an existing lockfile.
+    References that matches the arguments package names will be replaced by the arguments.
+    References can be supplied with and without revisions like "--requires=pkg/version",
+    """
+    subparser.add_argument('--requires', action="append", help='Update references to lockfile.')
+    subparser.add_argument('--build-requires', action="append",
+                           help='Update build-requires from lockfile')
+    subparser.add_argument('--python-requires', action="append",
+                           help='Update python-requires from lockfile')
+    subparser.add_argument('--config-requires', action="append",
+                           help='Update config-requires from lockfile')
+    subparser.add_argument("--lockfile-out", action=OnceArgument, default=LOCKFILE,
+                           help="Filename of the created lockfile")
+    subparser.add_argument("--lockfile", action=OnceArgument, help="Filename of the input lockfile")
+    args = parser.parse_args(*args)
+
+    lockfile = conan_api.lockfile.get_lockfile(lockfile=args.lockfile, partial=True)
+    lockfile.update(requires=args.requires, build_requires=args.build_requires,
+                    python_requires=args.python_requires, config_requires=args.config_requires)
+    conan_api.lockfile.save_lockfile(lockfile, args.lockfile_out)
diff --git a/conan/cli/printers/graph.py b/conan/cli/printers/graph.py
index 404ca7c97..5790f8e84 100644
--- a/conan/cli/printers/graph.py
+++ b/conan/cli/printers/graph.py
@@ -1,5 +1,5 @@
 from conan.api.output import ConanOutput, Color, LEVEL_VERBOSE
-from conans.client.graph.graph import RECIPE_CONSUMER, RECIPE_VIRTUAL, CONTEXT_BUILD, BINARY_SKIP,\
+from conans.client.graph.graph import BINARY_INVALID, BINARY_MISSING, RECIPE_CONSUMER, RECIPE_VIRTUAL, CONTEXT_BUILD, BINARY_SKIP,\
     BINARY_PLATFORM
 
 
@@ -122,13 +122,18 @@ def print_graph_packages(graph):
         output.info(title, Color.BRIGHT_YELLOW)
         for pref, (status, remote) in sorted(reqs_to_print.items(), key=repr):
             name = pref.repr_notime() if status != BINARY_PLATFORM else str(pref.ref)
-            msg = f"{tab}{name} - {status}"
-            if remote is not None and status != BINARY_SKIP:
-                msg += f" ({remote.name})"
+            msg = f"{tab}{name} - "
             if status == BINARY_SKIP:
                 skipped_requires.append(str(pref.ref))
-                output.verbose(msg, Color.BRIGHT_CYAN)
+                output.verbose(f"{msg}{status}", Color.BRIGHT_CYAN)
+            elif status == BINARY_MISSING or status == BINARY_INVALID:
+                output.write(msg, Color.BRIGHT_CYAN)
+                output.writeln(status, Color.BRIGHT_RED)
             else:
+                # Support python36
+                msg += status
+                if remote:
+                    msg += f" ({remote.name})"
                 output.info(msg, Color.BRIGHT_CYAN)
 
     _format_requires("Requirements", requires)
diff --git a/conan/tools/cmake/cmake.py b/conan/tools/cmake/cmake.py
index 6af622fc4..ca49da148 100644
--- a/conan/tools/cmake/cmake.py
+++ b/conan/tools/cmake/cmake.py
@@ -60,7 +60,8 @@ class CMake(object):
         """
 
         Reads the ``CMakePresets.json`` file generated by the
-        :param cli_args: Extra CLI arguments to pass to cmake invocation
+        :param cli_args: List of arguments ``[arg1, arg2, ...]`` that will be passed as extra CLI
+                         arguments to pass to cmake invocation
         :ref:`CMakeToolchain<conan-cmake-toolchain>` to get:
 
            - The generator, to append ``-G="xxx"``.
@@ -77,7 +78,6 @@ class CMake(object):
                           work in the toolchain approach.
         :param build_script_folder: Path to the CMakeLists.txt in case it is not in the declared
                                     ``self.folders.source`` at the ``layout()`` method.
-        :param cli_args: List of extra arguments provided when calling to CMake.
         :param stdout: Use it to redirect stdout to this stream
         :param stderr: Use it to redirect stderr to this stream
         """
@@ -233,7 +233,9 @@ class CMake(object):
                            in that case the build type must be specified at configure time, not build
                            time.
         :param target: Name of the build target to run, by default ``RUN_TESTS`` or ``test``
-        :param cli_args: Same as above ``build()``
+        :param cli_args: Same as above ``build()``, a list of arguments ``[arg1, arg2, ...]``
+                         to be passed as extra arguments for the underlying
+                         build system
         :param build_tool_args: Same as above ``build()``
         :param stdout: Use it to redirect stdout to this stream
         :param stderr: Use it to redirect stderr to this stream
@@ -255,7 +257,8 @@ class CMake(object):
         """
         Equivalent to running ctest ...
 
-        :param cli_args: Extra ctest command line arguments
+        :param cli_args: List of arguments ``[arg1, arg2, ...]`` to be passed
+                         as extra ctest command line arguments
         :param env: the environment files to activate, by default conanbuild + conanrun
         :param stdout: Use it to redirect stdout to this stream
         :param stderr: Use it to redirect stderr to this stream
diff --git a/conan/tools/cmake/toolchain/blocks.py b/conan/tools/cmake/toolchain/blocks.py
index ae023a009..5d1f34051 100644
--- a/conan/tools/cmake/toolchain/blocks.py
+++ b/conan/tools/cmake/toolchain/blocks.py
@@ -23,11 +23,12 @@ from conans.model.version import Version
 from conans.util.files import load
 
 
-class Block(object):
-    def __init__(self, conanfile, toolchain):
+class Block:
+    def __init__(self, conanfile, toolchain, name):
         self._conanfile = conanfile
         self._toolchain = toolchain
         self._context_values = None
+        self._name = name
 
     @property
     def values(self):
@@ -44,7 +45,8 @@ class Block(object):
         if context is None:
             return
 
-        template = Template(self.template, trim_blocks=True, lstrip_blocks=True)
+        template = f"########## '{self._name}' block #############\n" + self.template + "\n\n"
+        template = Template(template, trim_blocks=True, lstrip_blocks=True)
         return template.render(**context)
 
     def context(self):
@@ -56,8 +58,10 @@ class Block(object):
 
 
 class VSRuntimeBlock(Block):
-    template = textwrap.dedent("""
-        # Definition of VS runtime, defined from build_type, compiler.runtime, compiler.runtime_type
+    template = textwrap.dedent("""\
+        # Definition of VS runtime CMAKE_MSVC_RUNTIME_LIBRARY, from settings build_type,
+        # compiler.runtime, compiler.runtime_type
+
         {% set genexpr = namespace(str='') %}
         {% for config, value in vs_runtimes.items() %}
             {% set genexpr.str = genexpr.str +
@@ -67,6 +71,7 @@ class VSRuntimeBlock(Block):
         if(NOT "${POLICY_CMP0091}" STREQUAL NEW)
             message(FATAL_ERROR "The CMake policy CMP0091 must be NEW, but is '${POLICY_CMP0091}'")
         endif()
+        message(STATUS "Conan toolchain: Setting CMAKE_MSVC_RUNTIME_LIBRARY={{ genexpr.str  }}")
         set(CMAKE_MSVC_RUNTIME_LIBRARY "{{ genexpr.str }}")
         """)
 
@@ -116,9 +121,11 @@ class VSRuntimeBlock(Block):
 
 
 class VSDebuggerEnvironment(Block):
-    template = textwrap.dedent("""
+    template = textwrap.dedent("""\
+        # Definition of CMAKE_VS_DEBUGGER_ENVIRONMENT from "bindirs" folders of dependencies
+        # for execution of applications with shared libraries within the VS IDE
+
         {% if vs_debugger_path %}
-        # Definition of CMAKE_VS_DEBUGGER_ENVIRONMENT
         set(CMAKE_VS_DEBUGGER_ENVIRONMENT "{{ vs_debugger_path }}")
         {% endif %}
         """)
@@ -166,7 +173,9 @@ class VSDebuggerEnvironment(Block):
 
 
 class FPicBlock(Block):
-    template = textwrap.dedent("""
+    template = textwrap.dedent("""\
+        # Defining CMAKE_POSITION_INDEPENDENT_CODE for static libraries when necessary
+
         {% if fpic %}
         message(STATUS "Conan toolchain: Setting CMAKE_POSITION_INDEPENDENT_CODE={{ fpic }} (options.fPIC)")
         set(CMAKE_POSITION_INDEPENDENT_CODE {{ fpic }} CACHE BOOL "Position independent code")
@@ -185,11 +194,16 @@ class FPicBlock(Block):
 
 
 class GLibCXXBlock(Block):
-    template = textwrap.dedent("""
+    template = textwrap.dedent("""\
+        # Definition of libcxx from 'compiler.libcxx' setting, defining the
+        # right CXX_FLAGS for that libcxx
+
         {% if set_libcxx %}
+        message(STATUS "Conan toolchain: Defining libcxx as C++ flags: {{ set_libcxx }}")
         string(APPEND CONAN_CXX_FLAGS " {{ set_libcxx }}")
         {% endif %}
         {% if glibcxx %}
+        message(STATUS "Conan toolchain: Adding glibcxx compile definition: {{ glibcxx }}")
         add_compile_definitions({{ glibcxx }})
         {% endif %}
         """)
@@ -200,7 +214,9 @@ class GLibCXXBlock(Block):
 
 
 class SkipRPath(Block):
-    template = textwrap.dedent("""
+    template = textwrap.dedent("""\
+        # Defining CMAKE_SKIP_RPATH
+
         {% if skip_rpath %}
         set(CMAKE_SKIP_RPATH 1 CACHE BOOL "rpaths" FORCE)
         # Policy CMP0068
@@ -216,7 +232,10 @@ class SkipRPath(Block):
 
 
 class ArchitectureBlock(Block):
-    template = textwrap.dedent("""
+    template = textwrap.dedent("""\
+        # Define C++ flags, C flags and linker flags from 'settings.arch'
+
+        message(STATUS "Conan toolchain: Defining architecture flag: {{ arch_flag }}")
         string(APPEND CONAN_CXX_FLAGS " {{ arch_flag }}")
         string(APPEND CONAN_C_FLAGS " {{ arch_flag }}")
         string(APPEND CONAN_SHARED_LINKER_FLAGS " {{ arch_flag }}")
@@ -231,7 +250,10 @@ class ArchitectureBlock(Block):
 
 
 class LinkerScriptsBlock(Block):
-    template = textwrap.dedent("""
+    template = textwrap.dedent("""\
+        # Add linker flags from tools.build:linker_scripts conf
+
+        message(STATUS "Conan toolchain: Defining linker script flag: {{ linker_script_flags }}")
         string(APPEND CONAN_EXE_LINKER_FLAGS {{ linker_script_flags }})
         """)
 
@@ -248,7 +270,9 @@ class LinkerScriptsBlock(Block):
 
 
 class CppStdBlock(Block):
-    template = textwrap.dedent("""
+    template = textwrap.dedent("""\
+        # Define the C++ and C standards from 'compiler.cppstd' and 'compiler.cstd'
+
         {% if cppstd %}
         message(STATUS "Conan toolchain: C++ Standard {{ cppstd }} with extensions {{ cppstd_extensions }}")
         set(CMAKE_CXX_STANDARD {{ cppstd }})
@@ -285,7 +309,9 @@ class CppStdBlock(Block):
 
 
 class SharedLibBock(Block):
-    template = textwrap.dedent("""
+    template = textwrap.dedent("""\
+        # Define BUILD_SHARED_LIBS for shared libraries
+
         message(STATUS "Conan toolchain: Setting BUILD_SHARED_LIBS = {{ shared_libs }}")
         set(BUILD_SHARED_LIBS {{ shared_libs }} CACHE BOOL "Build shared libraries")
         """)
@@ -299,7 +325,9 @@ class SharedLibBock(Block):
 
 
 class ParallelBlock(Block):
-    template = textwrap.dedent("""
+    template = textwrap.dedent("""\
+        # Define VS paralell build /MP flags
+
         string(APPEND CONAN_CXX_FLAGS " /MP{{ parallel }}")
         string(APPEND CONAN_C_FLAGS " /MP{{ parallel }}")
         """)
@@ -318,12 +346,18 @@ class ParallelBlock(Block):
 
 class AndroidSystemBlock(Block):
 
-    template = textwrap.dedent("""
-        # New toolchain things
+    template = textwrap.dedent("""\
+        # Define Android variables ANDROID_PLATFORM, ANDROID_STL, ANDROID_ABI, etc
+        # and include(.../android.toolchain.cmake) from NDK toolchain file
+
+        # New Android toolchain definitions
+        message(STATUS "Conan toolchain: Setting Android platform: {{ android_platform }}")
         set(ANDROID_PLATFORM {{ android_platform }})
         {% if android_stl %}
+        message(STATUS "Conan toolchain: Setting Android stl: {{ android_stl }}")
         set(ANDROID_STL {{ android_stl }})
         {% endif %}
+        message(STATUS "Conan toolchain: Setting Android abi: {{ android_abi }}")
         set(ANDROID_ABI {{ android_abi }})
         {% if android_use_legacy_toolchain_file %}
         set(ANDROID_USE_LEGACY_TOOLCHAIN_FILE {{ android_use_legacy_toolchain_file }})
@@ -363,7 +397,9 @@ class AndroidSystemBlock(Block):
 
 
 class AppleSystemBlock(Block):
-    template = textwrap.dedent("""
+    template = textwrap.dedent("""\
+        # Define Apple architectures, sysroot, deployment target, bitcode, etc
+
         # Set the architectures for which to build.
         set(CMAKE_OSX_ARCHITECTURES {{ cmake_osx_architectures }} CACHE STRING "" FORCE)
         # Setting CMAKE_OSX_SYSROOT SDK, when using Xcode generator the name is enough
@@ -469,7 +505,9 @@ class AppleSystemBlock(Block):
 
 
 class FindFiles(Block):
-    template = textwrap.dedent("""
+    template = textwrap.dedent("""\
+        # Define paths to find packages, programs, libraries, etc.
+
         {% if find_package_prefer_config %}
         set(CMAKE_FIND_PACKAGE_PREFER_CONFIG {{ find_package_prefer_config }})
         {% endif %}
@@ -618,7 +656,9 @@ class FindFiles(Block):
 
 
 class PkgConfigBlock(Block):
-    template = textwrap.dedent("""
+    template = textwrap.dedent("""\
+        # Define pkg-config from 'tools.gnu:pkg_config' executable and paths
+
         {% if pkg_config %}
         set(PKG_CONFIG_EXECUTABLE {{ pkg_config }} CACHE FILEPATH "pkg-config executable")
         {% endif %}
@@ -643,7 +683,9 @@ class PkgConfigBlock(Block):
 
 
 class UserToolchain(Block):
-    template = textwrap.dedent("""
+    template = textwrap.dedent("""\
+        # Include one or more CMake user toolchain from tools.cmake.cmaketoolchain:user_toolchain
+
         {% for user_toolchain in paths %}
         include("{{user_toolchain}}")
         {% endfor %}
@@ -662,7 +704,10 @@ class UserToolchain(Block):
 class ExtraFlagsBlock(Block):
     """This block is adding flags directly from user [conf] section"""
 
-    _template = textwrap.dedent("""
+    _template = textwrap.dedent("""\
+        # Include extra C++, C and linker flags from configuration tools.build:<type>flags
+        # and from CMakeToolchain.extra_<type>_flags
+
         # Conan conf flags start: {{config}}
         {% if cxxflags %}
         string(APPEND CONAN_CXX_FLAGS{{suffix}} "{% for cxxflag in cxxflags %} {{ cxxflag }}{% endfor %}")
@@ -753,7 +798,9 @@ class ExtraFlagsBlock(Block):
 
 
 class CMakeFlagsInitBlock(Block):
-    template = textwrap.dedent("""
+    template = textwrap.dedent("""\
+        # Define CMAKE_<XXX>_FLAGS from CONAN_<XXX>_FLAGS
+
         foreach(config IN LISTS CMAKE_CONFIGURATION_TYPES)
             string(TOUPPER ${config} config)
             if(DEFINED CONAN_CXX_FLAGS_${config})
@@ -782,12 +829,13 @@ class CMakeFlagsInitBlock(Block):
         if(DEFINED CONAN_EXE_LINKER_FLAGS)
           string(APPEND CMAKE_EXE_LINKER_FLAGS_INIT " ${CONAN_EXE_LINKER_FLAGS}")
         endif()
-
         """)
 
 
 class TryCompileBlock(Block):
-    template = textwrap.dedent("""
+    template = textwrap.dedent("""\
+        # Blocks after this one will not be added when running CMake try/checks
+
         get_property( _CMAKE_IN_TRY_COMPILE GLOBAL PROPERTY IN_TRY_COMPILE )
         if(_CMAKE_IN_TRY_COMPILE)
             message(STATUS "Running toolchain IN_TRY_COMPILE")
@@ -821,14 +869,12 @@ class CompilersBlock(Block):
 
 
 class GenericSystemBlock(Block):
-    template = textwrap.dedent("""
-        ########## generic_system block #############
+    template = textwrap.dedent("""\
         # Definition of system, platform and toolset
-        #############################################
+
         {% if cmake_sysroot %}
         set(CMAKE_SYSROOT {{ cmake_sysroot }})
         {% endif %}
-
         {% if cmake_system_name %}
         # Cross building
         set(CMAKE_SYSTEM_NAME {{ cmake_system_name }})
@@ -947,7 +993,8 @@ class GenericSystemBlock(Block):
         return os_host in ('iOS', 'watchOS', 'tvOS', 'visionOS') or (
                 os_host == 'Macos' and (arch_host != arch_build or os_build != os_host))
 
-    def _get_darwin_version(self, os_name, os_version):
+    @staticmethod
+    def _get_darwin_version(os_name, os_version):
         # version mapping from https://en.wikipedia.org/wiki/Darwin_(operating_system)
         version_mapping = {
             "Macos": {
@@ -1063,8 +1110,11 @@ class GenericSystemBlock(Block):
                 "winsdk_version": winsdk_version,
                 "gen_platform_sdk_version": gen_platform_sdk_version}
 
+
 class ExtraVariablesBlock(Block):
-    template = textwrap.dedent(r"""
+    template = textwrap.dedent("""\
+        # Definition of extra CMake variables from tools.cmake.cmaketoolchain:extra_variables
+
         {% if extra_variables %}
         {% for key, value in extra_variables.items() %}
         set({{ key }} {{ value }})
@@ -1072,7 +1122,7 @@ class ExtraVariablesBlock(Block):
         {% endif %}
     """)
 
-    CMAKE_CACHE_TYPES = ["BOOL","FILEPATH", "PATH", "STRING", "INTERNAL"]
+    CMAKE_CACHE_TYPES = ["BOOL", "FILEPATH", "PATH", "STRING", "INTERNAL"]
 
     def get_exact_type(self, key, value):
         if isinstance(value, str):
@@ -1096,27 +1146,30 @@ class ExtraVariablesBlock(Block):
                     raise ConanException(f'tools.cmake.cmaketoolchain:extra_variables "{key}" invalid type "{var_type}" for cache variable. Possible types: {", ".join(self.CMAKE_CACHE_TYPES)}')
                 # Set docstring as variable name if not defined
                 docstring = value.get("docstring") or key
-                force_str = " FORCE" if is_force else "" # Support python < 3.11
+                force_str = " FORCE" if is_force else ""  # Support python < 3.11
                 return f"{var_value} CACHE {var_type} \"{docstring}\"{force_str}"
             else:
                 if is_force:
                     raise ConanException(f'tools.cmake.cmaketoolchain:extra_variables "{key}" "force" is only allowed for cache variables')
                 return var_value
 
-
     def context(self):
         # Reading configuration from "tools.cmake.cmaketoolchain:extra_variables"
-        extra_variables = self._conanfile.conf.get("tools.cmake.cmaketoolchain:extra_variables", default={}, check_type=dict)
+        extra_variables = self._conanfile.conf.get("tools.cmake.cmaketoolchain:extra_variables",
+                                                   default={}, check_type=dict)
         parsed_extra_variables = {}
         for key, value in extra_variables.items():
             parsed_extra_variables[key] = self.get_exact_type(key, value)
         return {"extra_variables": parsed_extra_variables}
 
+
 class OutputDirsBlock(Block):
 
     @property
     def template(self):
-        return textwrap.dedent("""
+        return textwrap.dedent("""\
+           # Definition of CMAKE_INSTALL_XXX folders
+
            {% if package_folder %}
            set(CMAKE_INSTALL_PREFIX "{{package_folder}}")
            {% endif %}
@@ -1152,6 +1205,81 @@ class OutputDirsBlock(Block):
                 "default_res": self._get_cpp_info_value("resdirs")}
 
 
+class VariablesBlock(Block):
+    @property
+    def template(self):
+        return textwrap.dedent("""\
+            # Definition of CMake variables from CMakeToolchain.variables values
+
+            {% macro iterate_configs(var_config, action) %}
+            {% for it, values in var_config.items() %}
+                {% set genexpr = namespace(str='') %}
+                {% for conf, value in values -%}
+                set(CONAN_DEF_{{ conf }}{{ it }} "{{ value }}")
+                {% endfor %}
+                {% for conf, value in values -%}
+                    {% set genexpr.str = genexpr.str +
+                                          '$<IF:$<CONFIG:' + conf + '>,${CONAN_DEF_' + conf|string + it|string + '},' %}
+                    {% if loop.last %}{% set genexpr.str = genexpr.str + '""' -%}{%- endif -%}
+                {% endfor %}
+                {% for i in range(values|count) %}{% set genexpr.str = genexpr.str + '>' %}
+                {% endfor %}
+                set({{ it }} {{ genexpr.str }} CACHE STRING
+                    "Variable {{ it }} conan-toolchain defined")
+            {% endfor %}
+            {% endmacro %}
+            # Variables
+            {% for it, value in variables.items() %}
+            {% if value is boolean %}
+            set({{ it }} {{ "ON" if value else "OFF"}} CACHE BOOL "Variable {{ it }} conan-toolchain defined")
+            {% else %}
+            set({{ it }} "{{ value }}" CACHE STRING "Variable {{ it }} conan-toolchain defined")
+            {% endif %}
+            {% endfor %}
+            # Variables  per configuration
+            {{ iterate_configs(variables_config, action='set') }}
+            """)
+
+    def context(self):
+        return {"variables": self._toolchain.variables,
+                "variables_config": self._toolchain.variables.configuration_types}
+
+
+class PreprocessorBlock(Block):
+    @property
+    def template(self):
+        return textwrap.dedent("""\
+        # Preprocessor definitions from CMakeToolchain.preprocessor_definitions values
+
+        {% for it, value in preprocessor_definitions.items() %}
+        {% if value is none %}
+        add_compile_definitions("{{ it }}")
+        {% else %}
+        add_compile_definitions("{{ it }}={{ value }}")
+        {% endif %}
+        {% endfor %}
+        # Preprocessor definitions per configuration
+        {% for name, values in preprocessor_definitions_config.items() %}
+        {%- for (conf, value) in values %}
+        {% if value is none %}
+        set(CONAN_DEF_{{conf}}_{{name}} "{{name}}")
+        {% else %}
+        set(CONAN_DEF_{{conf}}_{{name}} "{{name}}={{value}}")
+        {% endif %}
+        {% endfor %}
+        add_compile_definitions(
+        {%- for (conf, value) in values %}
+        $<$<CONFIG:{{conf}}>:${CONAN_DEF_{{conf}}_{{name}}}>
+        {%- endfor -%})
+        {% endfor %}
+        """)
+
+    def context(self):
+        return {"preprocessor_definitions": self._toolchain.preprocessor_definitions,
+                "preprocessor_definitions_config":
+                    self._toolchain.preprocessor_definitions.configuration_types}
+
+
 class ToolchainBlocks:
     def __init__(self, conanfile, toolchain, items=None):
         self._blocks = OrderedDict()
@@ -1159,7 +1287,7 @@ class ToolchainBlocks:
         self._toolchain = toolchain
         if items:
             for name, block in items:
-                self._blocks[name] = block(conanfile, toolchain)
+                self._blocks[name] = block(conanfile, toolchain, name)
 
     def keys(self):
         return self._blocks.keys()
@@ -1173,6 +1301,16 @@ class ToolchainBlocks:
             del self._blocks[arg]
 
     def select(self, name, *args):
+        """
+        keep the blocks provided as arguments, remove the others, except pre-existing "variables"
+        and "preprocessor", to not break behavior
+        """
+        self._conanfile.output.warning("CMakeToolchain.select is deprecated. Use blocks.enabled()"
+                                       " instead", warn_tag="deprecated")
+        to_keep = [name] + list(args) + ["variables", "preprocessor"]
+        self._blocks = OrderedDict((k, v) for k, v in self._blocks.items() if k in to_keep)
+
+    def enabled(self, name, *args):
         """
         keep the blocks provided as arguments, remove the others
         """
@@ -1182,7 +1320,7 @@ class ToolchainBlocks:
     def __setitem__(self, name, block_type):
         # Create a new class inheriting Block with the elements of the provided one
         block_type = type('proxyUserBlock', (Block,), dict(block_type.__dict__))
-        self._blocks[name] = block_type(self._conanfile, self._toolchain)
+        self._blocks[name] = block_type(self._conanfile, self._toolchain, name)
 
     def __getitem__(self, name):
         return self._blocks[name]
diff --git a/conan/tools/cmake/toolchain/toolchain.py b/conan/tools/cmake/toolchain/toolchain.py
index 0692751eb..6d834ec9b 100644
--- a/conan/tools/cmake/toolchain/toolchain.py
+++ b/conan/tools/cmake/toolchain/toolchain.py
@@ -9,11 +9,12 @@ from conan.internal import check_duplicated_generator
 from conan.tools.build import use_win_mingw
 from conan.tools.cmake.presets import write_cmake_presets
 from conan.tools.cmake.toolchain import CONAN_TOOLCHAIN_FILENAME
-from conan.tools.cmake.toolchain.blocks import ExtraVariablesBlock, ToolchainBlocks, UserToolchain, GenericSystemBlock, \
+from conan.tools.cmake.toolchain.blocks import ExtraVariablesBlock, ToolchainBlocks, UserToolchain, \
+    GenericSystemBlock, \
     AndroidSystemBlock, AppleSystemBlock, FPicBlock, ArchitectureBlock, GLibCXXBlock, VSRuntimeBlock, \
     CppStdBlock, ParallelBlock, CMakeFlagsInitBlock, TryCompileBlock, FindFiles, PkgConfigBlock, \
     SkipRPath, SharedLibBock, OutputDirsBlock, ExtraFlagsBlock, CompilersBlock, LinkerScriptsBlock, \
-    VSDebuggerEnvironment
+    VSDebuggerEnvironment, VariablesBlock, PreprocessorBlock
 from conan.tools.cmake.utils import is_multi_configuration
 from conan.tools.env import VirtualBuildEnv, VirtualRunEnv
 from conan.tools.intel import IntelCC
@@ -56,39 +57,18 @@ class Variables(OrderedDict):
                     data[key] = str(var).replace('"', '\\"')
 
 
-class CMakeToolchain(object):
+class CMakeToolchain:
 
     filename = CONAN_TOOLCHAIN_FILENAME
 
-    # TODO: Clean this macro, do it explicitly for variables
-    _template = textwrap.dedent("""
-        {% macro iterate_configs(var_config, action) %}
-            {% for it, values in var_config.items() %}
-                {% set genexpr = namespace(str='') %}
-                {% for conf, value in values -%}
-                set(CONAN_DEF_{{ conf }}{{ it }} "{{ value }}")
-                {% endfor %}
-                {% for conf, value in values -%}
-                    {% set genexpr.str = genexpr.str +
-                                          '$<IF:$<CONFIG:' + conf + '>,${CONAN_DEF_' + conf|string + it|string + '},' %}
-                    {% if loop.last %}{% set genexpr.str = genexpr.str + '""' -%}{%- endif -%}
-                {% endfor %}
-                {% for i in range(values|count) %}{% set genexpr.str = genexpr.str + '>' %}
-                {% endfor %}
-                set({{ it }} {{ genexpr.str }} CACHE STRING
-                    "Variable {{ it }} conan-toolchain defined")
-            {% endfor %}
-        {% endmacro %}
-
+    _template = textwrap.dedent("""\
         # Conan automatically generated toolchain file
         # DO NOT EDIT MANUALLY, it will be overwritten
 
         # Avoid including toolchain file several times (bad if appending to variables like
         #   CMAKE_CXX_FLAGS. See https://github.com/android/ndk/issues/323
         include_guard()
-
         message(STATUS "Using Conan toolchain: ${CMAKE_CURRENT_LIST_FILE}")
-
         if(${CMAKE_VERSION} VERSION_LESS "3.15")
             message(FATAL_ERROR "The 'CMakeToolchain' generator only works with CMake >= 3.15")
         endif()
@@ -97,41 +77,6 @@ class CMakeToolchain(object):
         {{ conan_block }}
         {% endfor %}
 
-        # Variables
-        {% for it, value in variables.items() %}
-        {% if value is boolean %}
-        set({{ it }} {{ "ON" if value else "OFF"}} CACHE BOOL "Variable {{ it }} conan-toolchain defined")
-        {% else %}
-        set({{ it }} "{{ value }}" CACHE STRING "Variable {{ it }} conan-toolchain defined")
-        {% endif %}
-        {% endfor %}
-        # Variables  per configuration
-        {{ iterate_configs(variables_config, action='set') }}
-
-        # Preprocessor definitions
-        {% for it, value in preprocessor_definitions.items() %}
-        {% if value is none %}
-        add_compile_definitions("{{ it }}")
-        {% else %}
-        add_compile_definitions("{{ it }}={{ value }}")
-        {% endif %}
-        {% endfor %}
-        # Preprocessor definitions per configuration
-        {% for name, values in preprocessor_definitions_config.items() %}
-        {%- for (conf, value) in values %}
-        {% if value is none %}
-        set(CONAN_DEF_{{conf}}_{{name}} "{{name}}")
-        {% else %}
-        set(CONAN_DEF_{{conf}}_{{name}} "{{name}}={{value}}")
-        {% endif %}
-        {% endfor %}
-        add_compile_definitions(
-        {%- for (conf, value) in values %}
-        $<$<CONFIG:{{conf}}>:${CONAN_DEF_{{conf}}_{{name}}}>
-        {%- endfor -%})
-        {% endfor %}
-
-
         if(CMAKE_POLICY_DEFAULT_CMP0091)  # Avoid unused and not-initialized warnings
         endif()
         """)
@@ -171,7 +116,9 @@ class CMakeToolchain(object):
                                        ("pkg_config", PkgConfigBlock),
                                        ("rpath", SkipRPath),
                                        ("shared", SharedLibBock),
-                                       ("output_dirs", OutputDirsBlock)])
+                                       ("output_dirs", OutputDirsBlock),
+                                       ("variables", VariablesBlock),
+                                       ("preprocessor", PreprocessorBlock)])
 
         # Set the CMAKE_MODULE_PATH and CMAKE_PREFIX_PATH to the deps .builddirs
         self.find_builddirs = True
@@ -188,10 +135,7 @@ class CMakeToolchain(object):
 
         blocks = self.blocks.process_blocks()
         ctxt_toolchain = {
-            "variables": self.variables,
-            "variables_config": self.variables.configuration_types,
-            "preprocessor_definitions": self.preprocessor_definitions,
-            "preprocessor_definitions_config": self.preprocessor_definitions.configuration_types,
+
             "conan_blocks": blocks
         }
 
diff --git a/conan/tools/files/conandata.py b/conan/tools/files/conandata.py
index 95e6273d3..3b1c47600 100644
--- a/conan/tools/files/conandata.py
+++ b/conan/tools/files/conandata.py
@@ -47,7 +47,7 @@ def trim_conandata(conanfile, raise_if_missing=True):
     only
     """
     if not hasattr(conanfile, "export_folder") or conanfile.export_folder is None:
-        raise ConanException("The 'trim_conandata()' can only be used in the 'export()' method")
+        raise ConanException("The 'trim_conandata()' tool can only be used in the 'export()' method or 'post_export()' hook")
     path = os.path.join(conanfile.export_folder, "conandata.yml")
     if not os.path.exists(path):
         if raise_if_missing:
diff --git a/conans/client/conf/__init__.py b/conans/client/conf/__init__.py
index 0ac9b8a2d..33c9db521 100644
--- a/conans/client/conf/__init__.py
+++ b/conans/client/conf/__init__.py
@@ -62,6 +62,7 @@ os:
                 ios_version: *ios_version
     Android:
         api_level: [ANY]
+        ndk_version: [null, ANY]
     FreeBSD:
     SunOS:
     AIX:
diff --git a/conans/client/profile_loader.py b/conans/client/profile_loader.py
index c532e6b69..ad4808177 100644
--- a/conans/client/profile_loader.py
+++ b/conans/client/profile_loader.py
@@ -41,6 +41,7 @@ def profile_plugin(profile):
     _check_correct_cppstd(settings)
     _check_correct_cstd(settings)
 
+
 def _check_correct_cppstd(settings):
     from conan.tools.scm import Version
     def _error(compiler, cppstd, min_version, version):
diff --git a/conans/model/graph_lock.py b/conans/model/graph_lock.py
index 29e87de77..999c57cc1 100644
--- a/conans/model/graph_lock.py
+++ b/conans/model/graph_lock.py
@@ -83,6 +83,21 @@ class _LockRequires:
         self._requires = OrderedDict((k, v) for k, v in self._requires.items() if k not in remove)
         return remove
 
+    def update(self, refs, name):
+        if not refs:
+            return
+        for r in refs:
+            r = RecipeReference.loads(r)
+            new_reqs = {}
+            for k, v in self._requires.items():
+                if r.name == k.name:
+                    ConanOutput().info(f"Replacing {name}: {k.repr_notime()} -> {repr(r)}")
+                else:
+                    new_reqs[k] = v
+            self._requires = new_reqs
+            self._requires[r] = None  # No package-id at the moment
+        self.sort()
+
     def sort(self):
         self._requires = OrderedDict(reversed(sorted(self._requires.items())))
 
@@ -211,6 +226,12 @@ class Lockfile(object):
         _remove(python_requires, self._python_requires, "python_require")
         _remove(config_requires, self._conf_requires, "config_requires")
 
+    def update(self, requires=None, build_requires=None, python_requires=None, config_requires=None):
+        self._requires.update(requires, "require")
+        self._build_requires.update(build_requires, "build_requires")
+        self._python_requires.update(python_requires, "python_requires")
+        self._conf_requires.update(config_requires, "config_requires")
+
     @staticmethod
     def deserialize(data):
         """ constructs a GraphLock from a json like dict


Test patch:
diff --git a/pytest.ini b/pytest.ini
index ceb01dcd0..a69be4e90 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -1,3 +1,3 @@
 [pytest]
 norecursedirs = '.*', 'dist', 'CVS', '_darcs', '{arch}', '*.egg', 'venv', 'assets'
-testpaths = 'conans/test'
+testpaths = 'test'
diff --git a/test/README.md b/test/README.md
index 574c30349..22809fc9f 100644
--- a/test/README.md
+++ b/test/README.md
@@ -3,18 +3,18 @@
 
 Conan tests fall into three categories:
 
-- **Unit tests** in `conans/test/unittests` folder. These tests should test small pieces of code like
+- **Unit tests** in `test/unittests` folder. These tests should test small pieces of code like
   functions, methods, or properties. As long as it's possible they should not rely on anything
   external like the file system or system configuration,  and in case they need to do it should be
   mocked.
 
-- **Integration tests** in `conans/test/integration` folder. We consider integration tests the ones that
+- **Integration tests** in `test/integration` folder. We consider integration tests the ones that
   only will need pure python to execute, but that may test the interaction between different Conan
   modules. They could test the result of the execution of one or several Conan commands, but shouldn't
   depend on any external tools like compilers, build systems, or version-control system
   tools.
 
-- **Functional tests** in `conans/test/functional` folder. Under this category, we add tests that are
+- **Functional tests** in `test/functional` folder. Under this category, we add tests that are
   testing the complete Conan functionality. They may call external tools (please read the section
   below to check the tools installed on the CI). These tests should be avoided as long as
   it's possible as they may take considerable CI time.
@@ -70,7 +70,7 @@ def test_vcvars_priority(self):
     ...
 ```
 
-If the test needs any of those tools to run it should be marked as using that tool and moved to the `conans/test/functional` folder.
+If the test needs any of those tools to run it should be marked as using that tool and moved to the `test/functional` folder.
 
 ### Parametrizing tests
 
diff --git a/test/integration/command_v2/test_list_lru.py b/test/integration/command_v2/test_list_lru.py
index f1bb46131..54962ed4a 100644
--- a/test/integration/command_v2/test_list_lru.py
+++ b/test/integration/command_v2/test_list_lru.py
@@ -1,3 +1,4 @@
+import json
 import time
 
 import pytest
@@ -50,3 +51,17 @@ class TestLRU:
         assert "pkg" in c.out
         assert "da39a3ee5e6b4b0d3255bfef95601890afd80709" not in c.out
         assert "dep" not in c.out
+
+    def test_update_lru_when_used_as_dependency(self):
+        """Show that using a recipe as a dependency will update its LRU"""
+        c = TestClient()
+        c.save({"dep/conanfile.py": GenConanfile("dep", "1.0"),
+                "conanfile.py": GenConanfile("app", "1.0").with_require("dep/1.0")})
+
+        c.run("create dep")
+        time.sleep(2)
+        c.run("create .")
+        # Dep is not removed because its lru was updated as part of the above create
+        c.run("remove * --lru=1s -c -f=json", redirect_stdout="removed.json")
+        removed = json.loads(c.load("removed.json"))
+        assert len(removed["Local Cache"]) == 0
diff --git a/test/integration/configuration/test_profile_plugin.py b/test/integration/configuration/test_profile_plugin.py
index a88eb0de7..10e42c159 100644
--- a/test/integration/configuration/test_profile_plugin.py
+++ b/test/integration/configuration/test_profile_plugin.py
@@ -26,3 +26,13 @@ class TestErrorsProfilePlugin:
         os.remove(os.path.join(c.cache.plugins_path, "profile.py"))
         c.run("profile show", assert_error=True)
         assert "ERROR: The 'profile.py' plugin file doesn't exist" in c.out
+
+
+def test_android_ndk_version():
+    c = TestClient()
+    c.run("profile show -s os=Android")
+    assert "os.ndk_version" not in c.out
+    c.run("profile show -s os=Android -s os.ndk_version=r26")
+    assert "os.ndk_version=r26" in c.out
+    c.run("profile show -s os=Android -s os.ndk_version=r26a")
+    assert "os.ndk_version=r26a" in c.out
diff --git a/test/integration/lockfile/test_user_overrides.py b/test/integration/lockfile/test_user_overrides.py
index 2f3c3c325..601f365d5 100644
--- a/test/integration/lockfile/test_user_overrides.py
+++ b/test/integration/lockfile/test_user_overrides.py
@@ -327,3 +327,36 @@ class TestLockRemove:
                 "other/1.0@user", "pkg/1.0@team", "engine/1.0"}.difference(removed)
         for pkg in rest:
             assert pkg in lock
+
+
+class TestLockUpdate:
+    @pytest.mark.parametrize("kind, old, new", [
+        ("requires", "math/1.0", "math/1.1"),
+        ("build-requires", "cmake/1.0", "cmake/1.1"),
+        ("python-requires", "mytool/1.0", "mytool/1.1"),
+    ])
+    def test_lock_update(self, kind, old, new):
+        c = TestClient()
+        lock = textwrap.dedent("""\
+            {
+                "version": "0.5",
+                "requires": [
+                    "math/1.0#85d927a4a067a531b1a9c7619522c015%1702683583.3411012",
+                    "math/1.0#12345%1702683584.3411012",
+                    "engine/1.0#fd2b006646a54397c16a1478ac4111ac%1702683583.3544693"
+                ],
+                "build_requires": [
+                    "cmake/1.0#85d927a4a067a531b1a9c7619522c015%1702683583.3411012",
+                    "ninja/1.0#fd2b006646a54397c16a1478ac4111ac%1702683583.3544693"
+                ],
+                "python_requires": [
+                    "mytool/1.0#85d927a4a067a531b1a9c7619522c015%1702683583.3411012",
+                    "othertool/1.0#fd2b006646a54397c16a1478ac4111ac%1702683583.3544693"
+                ]
+            }
+            """)
+        c.save({"conan.lock": lock})
+        c.run(f"lock update --{kind}={new}")
+        lock = c.load("conan.lock")
+        assert old not in lock
+        assert new in lock
diff --git a/test/unittests/tools/cmake/test_cmaketoolchain.py b/test/unittests/tools/cmake/test_cmaketoolchain.py
index 3d66adada..8ba5ff7f0 100644
--- a/test/unittests/tools/cmake/test_cmaketoolchain.py
+++ b/test/unittests/tools/cmake/test_cmaketoolchain.py
@@ -58,10 +58,16 @@ def test_remove(conanfile):
     assert "_CMAKE_IN_TRY_COMPILE" in content
 
 
-def test_filter(conanfile):
+def test_select_blocks(conanfile):
     toolchain = CMakeToolchain(conanfile)
     toolchain.blocks.select("generic_system")
     content = toolchain.content
+    assert "########## 'generic_system' block #############" in content
+    assert "########## 'cmake_flags_init' block #############" not in content
+    assert "########## 'libcxx' block #############" not in content
+    # These are not removed by default, to not break behavior
+    assert "########## 'variables' block #############" in content
+    assert "########## 'preprocessor' block #############" in content
     assert 'CMAKE_SYSTEM_NAME' in content
     assert "CMAKE_CXX_FLAGS_INIT" not in content
     assert "_CMAKE_IN_TRY_COMPILE" not in content
@@ -70,10 +76,24 @@ def test_filter(conanfile):
     toolchain = CMakeToolchain(conanfile)
     toolchain.blocks.select("generic_system", "cmake_flags_init")
     content = toolchain.content
+    assert "########## 'generic_system' block #############" in content
+    assert "########## 'cmake_flags_init' block #############" in content
+    assert "########## 'libcxx' block #############" not in content
+    # These are not removed by default, to not break behavior
+    assert "########## 'variables' block #############" in content
+    assert "########## 'preprocessor' block #############" in content
     assert 'CMAKE_SYSTEM_NAME' in content
     assert "CMAKE_CXX_FLAGS_INIT" in content
     assert "_CMAKE_IN_TRY_COMPILE" not in content
 
+    # remove multiple
+    toolchain = CMakeToolchain(conanfile)
+    toolchain.blocks.enabled("generic_system")
+    content = toolchain.content
+    assert "########## 'generic_system' block #############" in content
+    assert "########## 'variables' block #############" not in content
+    assert "########## 'preprocessor' block #############" not in content
+
 
 def test_dict_keys(conanfile):
     toolchain = CMakeToolchain(conanfile)