Repository: conan-io/conan
Pull request #16327 (2024-05-23 11:56:36)

allowing multiple negation patterns for --build
Changelog: Feature: Allow multiple ``--build=missing:~pattern1 --build=missing:~pattern2`` patterns
Docs: Omit

Close https://github.com/conan-io/conan/issues/16323


Patch:
diff --git a/conan/api/subapi/cache.py b/conan/api/subapi/cache.py
index 77d8c6df8..e8504efc1 100644
--- a/conan/api/subapi/cache.py
+++ b/conan/api/subapi/cache.py
@@ -177,7 +177,10 @@ class CacheAPI:
         for ref, ref_bundle in package_list.refs().items():
             ref.timestamp = revision_timestamp_now()
             ref_bundle["timestamp"] = ref.timestamp
-            recipe_layout = cache.get_or_create_ref_layout(ref)  # DB folder entry
+            try:
+                recipe_layout = cache.recipe_layout(ref)
+            except ConanException:
+                recipe_layout = cache.create_ref_layout(ref)  # new DB folder entry
             recipe_folder = ref_bundle["recipe_folder"]
             rel_path = os.path.relpath(recipe_layout.base_folder, cache_folder)
             rel_path = rel_path.replace("\\", "/")
@@ -187,7 +190,11 @@ class CacheAPI:
             for pref, pref_bundle in package_list.prefs(ref, ref_bundle).items():
                 pref.timestamp = revision_timestamp_now()
                 pref_bundle["timestamp"] = pref.timestamp
-                pkg_layout = cache.get_or_create_pkg_layout(pref)  # DB Folder entry
+                try:
+                    pkg_layout = cache.pkg_layout(pref)
+                except ConanException:
+                    pkg_layout = cache.create_pkg_layout(pref)  # DB Folder entry
+                # FIXME: This is not taking into account the existence of previous package
                 unzipped_pkg_folder = pref_bundle["package_folder"]
                 out.info(f"Restore: {pref} in {unzipped_pkg_folder}")
                 # If the DB folder entry is different to the disk unzipped one, we need to move it
@@ -203,6 +210,7 @@ class CacheAPI:
                     pref_bundle["package_folder"] = db_pkg_folder
                 unzipped_metadata_folder = pref_bundle.get("metadata_folder")
                 if unzipped_metadata_folder:
+                    # FIXME: Restore metadata is not incremental, but destructive
                     out.info(f"Restore: {pref} metadata in {unzipped_metadata_folder}")
                     db_metadata_folder = os.path.relpath(pkg_layout.metadata(), cache_folder)
                     db_metadata_folder = db_metadata_folder.replace("\\", "/")
diff --git a/conan/internal/cache/cache.py b/conan/internal/cache/cache.py
index edb2bb94a..92af0a603 100644
--- a/conan/internal/cache/cache.py
+++ b/conan/internal/cache/cache.py
@@ -8,8 +8,7 @@ from conan.internal.cache.conan_reference_layout import RecipeLayout, PackageLay
 # TODO: Random folders are no longer accessible, how to get rid of them asap?
 # TODO: We need the workflow to remove existing references.
 from conan.internal.cache.db.cache_database import CacheDatabase
-from conans.errors import ConanReferenceAlreadyExistsInDB, ConanReferenceDoesNotExistInDB, \
-    ConanException
+from conans.errors import ConanReferenceAlreadyExistsInDB, ConanException
 from conans.model.package_ref import PkgReference
 from conans.model.recipe_ref import RecipeReference
 from conans.util.dates import revision_timestamp_now
@@ -138,31 +137,29 @@ class PkgCache:
         # we use abspath to convert cache forward slash in Windows to backslash
         return PackageLayout(pref, os.path.abspath(os.path.join(self._base_folder, pref_path)))
 
-    def get_or_create_ref_layout(self, ref: RecipeReference):
-        """ called by RemoteManager.get_recipe()
+    def create_ref_layout(self, ref: RecipeReference):
+        """ called exclusively by:
+        - RemoteManager.get_recipe()
+        - cache restore
         """
-        try:
-            return self.recipe_layout(ref)
-        except ConanReferenceDoesNotExistInDB:
-            assert ref.revision, "Recipe revision must be known to create the package layout"
-            reference_path = self._get_path(ref)
-            self._db.create_recipe(reference_path, ref)
-            self._create_path(reference_path, remove_contents=False)
-            return RecipeLayout(ref, os.path.join(self._base_folder, reference_path))
-
-    def get_or_create_pkg_layout(self, pref: PkgReference):
-        """ called by RemoteManager.get_package() and  BinaryInstaller
+        assert ref.revision, "Recipe revision must be known to create the package layout"
+        reference_path = self._get_path(ref)
+        self._db.create_recipe(reference_path, ref)
+        self._create_path(reference_path, remove_contents=False)
+        return RecipeLayout(ref, os.path.join(self._base_folder, reference_path))
+
+    def create_pkg_layout(self, pref: PkgReference):
+        """ called by:
+         - RemoteManager.get_package()
+         - cacje restpre
         """
-        try:
-            return self.pkg_layout(pref)
-        except ConanReferenceDoesNotExistInDB:
-            assert pref.ref.revision, "Recipe revision must be known to create the package layout"
-            assert pref.package_id, "Package id must be known to create the package layout"
-            assert pref.revision, "Package revision should be known to create the package layout"
-            package_path = self._get_path_pref(pref)
-            self._db.create_package(package_path, pref, None)
-            self._create_path(package_path, remove_contents=False)
-            return PackageLayout(pref, os.path.join(self._base_folder, package_path))
+        assert pref.ref.revision, "Recipe revision must be known to create the package layout"
+        assert pref.package_id, "Package id must be known to create the package layout"
+        assert pref.revision, "Package revision should be known to create the package layout"
+        package_path = self._get_path_pref(pref)
+        self._db.create_package(package_path, pref, None)
+        self._create_path(package_path, remove_contents=False)
+        return PackageLayout(pref, os.path.join(self._base_folder, package_path))
 
     def update_recipe_timestamp(self, ref: RecipeReference):
         """ when the recipe already exists in cache, but we get a new timestamp from a server
diff --git a/conan/internal/cache/conan_reference_layout.py b/conan/internal/cache/conan_reference_layout.py
index 592c03c68..3a4aab9b0 100644
--- a/conan/internal/cache/conan_reference_layout.py
+++ b/conan/internal/cache/conan_reference_layout.py
@@ -81,14 +81,6 @@ class RecipeLayout(LayoutBase):
         src_folder = self.source()
         rmdir(src_folder)
 
-    def export_remove(self):
-        export_folder = self.export()
-        rmdir(export_folder)
-        export_src_folder = os.path.join(self._base_folder, EXPORT_SRC_FOLDER)
-        rmdir(export_src_folder)
-        download_export = self.download_export()
-        rmdir(download_export)
-
 
 class PackageLayout(LayoutBase):
 
diff --git a/conan/tools/build/cppstd.py b/conan/tools/build/cppstd.py
index 01ef4d71f..bf66fc624 100644
--- a/conan/tools/build/cppstd.py
+++ b/conan/tools/build/cppstd.py
@@ -11,12 +11,7 @@ def check_min_cppstd(conanfile, cppstd, gnu_extensions=False):
         In case the current cppstd doesn't fit the minimal version required
         by cppstd, a ConanInvalidConfiguration exception will be raised.
 
-        1. If settings.compiler.cppstd, the tool will use settings.compiler.cppstd to compare
-        2. It not settings.compiler.cppstd, the tool will use compiler to compare (reading the
-           default from cppstd_default)
-        3. If not settings.compiler is present (not declared in settings) will raise because it
-           cannot compare.
-        4. If can not detect the default cppstd for settings.compiler, a exception will be raised.
+        settings.compiler.cppstd must be defined, otherwise ConanInvalidConfiguration is raised
 
     :param conanfile: The current recipe object. Always use ``self``.
     :param cppstd: Minimal cppstd version required
@@ -31,12 +26,7 @@ def check_max_cppstd(conanfile, cppstd, gnu_extensions=False):
         In case the current cppstd doesn't fit the maximum version required
         by cppstd, a ConanInvalidConfiguration exception will be raised.
 
-        1. If settings.compiler.cppstd, the tool will use settings.compiler.cppstd to compare
-        2. It not settings.compiler.cppstd, the tool will use compiler to compare (reading the
-           default from cppstd_default)
-        3. If not settings.compiler is present (not declared in settings) will raise because it
-           cannot compare.
-        4. If can not detect the default cppstd for settings.compiler, a exception will be raised.
+        settings.compiler.cppstd must be defined, otherwise ConanInvalidConfiguration is raised
 
     :param conanfile: The current recipe object. Always use ``self``.
     :param cppstd: Maximum cppstd version required
@@ -126,12 +116,7 @@ def _check_cppstd(conanfile, cppstd, comparator, gnu_extensions):
         In case the current cppstd doesn't fit the maximum version required
         by cppstd, a ConanInvalidConfiguration exception will be raised.
 
-        1. If settings.compiler.cppstd, the tool will use settings.compiler.cppstd to compare
-        2. It not settings.compiler.cppstd, the tool will use compiler to compare (reading the
-           default from cppstd_default)
-        3. If not settings.compiler is present (not declared in settings) will raise because it
-           cannot compare.
-        4. If can not detect the default cppstd for settings.compiler, a exception will be raised.
+        settings.compiler.cppstd must be defined, otherwise ConanInvalidConfiguration is raised
 
     :param conanfile: The current recipe object. Always use ``self``.
     :param cppstd: Required cppstd version.
@@ -257,6 +242,7 @@ def _mcst_lcc_supported_cppstd(version):
 
     return ["98", "gnu98", "11", "gnu11", "14", "gnu14", "17", "gnu17", "20", "gnu20"]
 
+
 def _qcc_supported_cppstd(version):
     """
     [98, gnu98, 11, gnu11, 14, gnu14, 17, gnu17]
diff --git a/conans/client/graph/build_mode.py b/conans/client/graph/build_mode.py
index 5ed470988..f7a3cd61f 100644
--- a/conans/client/graph/build_mode.py
+++ b/conans/client/graph/build_mode.py
@@ -15,6 +15,7 @@ class BuildMode:
         self.editable = False
         self.patterns = []
         self.build_missing_patterns = []
+        self._build_missing_excluded = []
         self._excluded_patterns = []
         if params is None:
             return
@@ -34,7 +35,10 @@ class BuildMode:
             else:
                 if param.startswith("missing:"):
                     clean_pattern = param[len("missing:"):]
-                    self.build_missing_patterns.append(clean_pattern)
+                    if clean_pattern and clean_pattern[0] in ["!", "~"]:
+                        self._build_missing_excluded.append(clean_pattern[1:])
+                    else:
+                        self.build_missing_patterns.append(clean_pattern)
                 else:
                     clean_pattern = param
                     if clean_pattern and clean_pattern[0] in ["!", "~"]:
@@ -86,6 +90,12 @@ class BuildMode:
         return False
 
     def should_build_missing(self, conanfile):
+        if self._build_missing_excluded:
+            for pattern in self._build_missing_excluded:
+                if ref_matches(conanfile.ref, pattern, is_consumer=False):
+                    return False
+            return True  # If it has not been excluded by the negated patterns, it is included
+
         for pattern in self.build_missing_patterns:
-            if ref_matches(conanfile.ref, pattern, is_consumer=False):
+            if ref_matches(conanfile.ref, pattern, is_consumer=conanfile._conan_is_consumer):
                 return True
diff --git a/conans/client/graph/graph_builder.py b/conans/client/graph/graph_builder.py
index 64c9a4bad..d248d25bb 100644
--- a/conans/client/graph/graph_builder.py
+++ b/conans/client/graph/graph_builder.py
@@ -237,6 +237,7 @@ class DepsGraphBuilder(object):
             graph.aliased[alias] = pointed_ref  # Caching the alias
             new_req = Requirement(pointed_ref)  # FIXME: Ugly temp creation just for alias check
             alias = new_req.alias
+            node.conanfile.output.warning("Requirement 'alias' is provided in Conan 2 mainly for compatibility and upgrade from Conan 1, but it is an undocumented and legacy feature. Please update to use standard versioning mechanisms", warn_tag="legacy")
 
     def _resolve_recipe(self, ref, graph_lock):
         result = self._proxy.get_recipe(ref, self._remotes, self._update, self._check_update)
diff --git a/conans/client/installer.py b/conans/client/installer.py
index 7c05e2724..5f62d1527 100644
--- a/conans/client/installer.py
+++ b/conans/client/installer.py
@@ -304,30 +304,29 @@ class BinaryInstaller:
 
         pref = PkgReference(install_reference.ref, package.package_id, package.prev)
 
-        if pref.revision is None:
-            assert package.binary == BINARY_BUILD
-            package_layout = self._cache.create_build_pkg_layout(pref)
-        else:
-            package_layout = self._cache.get_or_create_pkg_layout(pref)
-
         if package.binary == BINARY_BUILD:
+            assert pref.revision is None
             ConanOutput()\
                 .subtitle(f"Installing package {pref.ref} ({handled_count} of {total_count})")
             ConanOutput(scope=str(pref.ref))\
                 .highlight("Building from source")\
                 .info(f"Package {pref}")
+            package_layout = self._cache.create_build_pkg_layout(pref)
             self._handle_node_build(package, package_layout)
             # Just in case it was recomputed
             package.package_id = package.nodes[0].pref.package_id  # Just in case it was recomputed
             package.prev = package.nodes[0].pref.revision
             package.binary = package.nodes[0].binary
             pref = PkgReference(install_reference.ref, package.package_id, package.prev)
-        elif package.binary == BINARY_CACHE:
-            node = package.nodes[0]
-            pref = node.pref
-            self._cache.update_package_lru(pref)
-            assert node.prev, "PREV for %s is None" % str(pref)
-            node.conanfile.output.success(f'Already installed! ({handled_count} of {total_count})')
+        else:
+            assert pref.revision is not None
+            package_layout = self._cache.pkg_layout(pref)
+            if package.binary == BINARY_CACHE:
+                node = package.nodes[0]
+                pref = node.pref
+                self._cache.update_package_lru(pref)
+                assert node.prev, "PREV for %s is None" % str(pref)
+                node.conanfile.output.success(f'Already installed! ({handled_count} of {total_count})')
 
         # Make sure that all nodes with same pref compute package_info()
         pkg_folder = package_layout.package()
diff --git a/conans/client/remote_manager.py b/conans/client/remote_manager.py
index 9827b83d7..51ce2e2d1 100644
--- a/conans/client/remote_manager.py
+++ b/conans/client/remote_manager.py
@@ -49,8 +49,7 @@ class RemoteManager:
         assert ref.revision, "get_recipe without revision specified"
         assert ref.timestamp, "get_recipe without ref.timestamp specified"
 
-        layout = self._cache.get_or_create_ref_layout(ref)
-        layout.export_remove()
+        layout = self._cache.create_ref_layout(ref)
 
         export_folder = layout.export()
         local_folder_remote = self._local_folder_remote(remote)
@@ -132,8 +131,7 @@ class RemoteManager:
 
         assert pref.revision is not None
 
-        pkg_layout = self._cache.get_or_create_pkg_layout(pref)
-        pkg_layout.package_remove()  # Remove first the destination folder
+        pkg_layout = self._cache.create_pkg_layout(pref)
         with pkg_layout.set_dirty_context_manager():
             self._get_package(pkg_layout, pref, remote, output, metadata)
 
diff --git a/conans/client/rest_client_local_recipe_index.py b/conans/client/rest_client_local_recipe_index.py
index 36d3b516e..041406d50 100644
--- a/conans/client/rest_client_local_recipe_index.py
+++ b/conans/client/rest_client_local_recipe_index.py
@@ -10,6 +10,7 @@ from conan.api.model import LOCAL_RECIPES_INDEX
 from conan.api.output import ConanOutput
 from conan.internal.cache.home_paths import HomePaths
 from conans.client.cmd.export import cmd_export
+from conans.client.loader import ConanFileLoader
 from conans.errors import ConanException, PackageNotFoundException, RecipeNotFoundException
 from conans.model.conf import ConfDefinition
 from conans.model.recipe_ref import RecipeReference
@@ -190,6 +191,8 @@ class _LocalRecipesIndexLayout:
         recipes.sort()
         ret = []
         excluded = set()
+
+        loader = ConanFileLoader(None)
         for r in recipes:
             if not fnmatch(r, name_pattern):
                 continue
@@ -207,10 +210,18 @@ class _LocalRecipesIndexLayout:
                 # This check can be removed after compatibility with 2.0
                 conanfile = os.path.join(recipes_dir, r, subfolder, "conanfile.py")
                 conanfile_content = load(conanfile)
+
                 if "from conans" in conanfile_content or "import conans" in conanfile_content:
                     excluded.add(r)
                     continue
-                ret.append(RecipeReference.loads(ref))
+                ref = RecipeReference.loads(ref)
+                try:
+                    recipe = loader.load_basic(conanfile)
+                    ref.user = recipe.user
+                    ref.channel = recipe.channel
+                except Exception as e:
+                    ConanOutput().warning(f"Couldn't load recipe {conanfile}: {e}")
+                ret.append(ref)
         if excluded:
             ConanOutput().warning(f"Excluding recipes not Conan 2.0 ready: {', '.join(excluded)}")
         return ret
diff --git a/conans/model/recipe_ref.py b/conans/model/recipe_ref.py
index 01168f09f..0d86ff3fa 100644
--- a/conans/model/recipe_ref.py
+++ b/conans/model/recipe_ref.py
@@ -154,7 +154,7 @@ class RecipeReference:
         if self.channel and validation_pattern.match(self.channel) is None:
             raise ConanException(f"Invalid package channel '{self.channel}'")
 
-         # Warn if they use .+ in the name/user/channel, as it can be problematic for generators
+        # Warn if they use .+ in the name/user/channel, as it can be problematic for generators
         pattern = re.compile(r'[.+]')
         if pattern.search(self.name):
             ConanOutput().warning(f"Name containing special chars is discouraged '{self.name}'")
@@ -179,8 +179,8 @@ class RecipeReference:
             no_user_channel = True
 
         condition = ((pattern == "&" and is_consumer) or
-                      fnmatch.fnmatchcase(str(self), pattern) or
-                      fnmatch.fnmatchcase(self.repr_notime(), pattern))
+                     fnmatch.fnmatchcase(str(self), pattern) or
+                     fnmatch.fnmatchcase(self.repr_notime(), pattern))
         if no_user_channel:
             condition = condition and not self.user and not self.channel
         if negate:


Test patch:
diff --git a/test/integration/command/alias_test.py b/test/integration/command/alias_test.py
index d8eb389b5..8b5c7243e 100644
--- a/test/integration/command/alias_test.py
+++ b/test/integration/command/alias_test.py
@@ -9,13 +9,13 @@ from conan.test.utils.tools import TestClient, GenConanfile
 class ConanAliasTest(unittest.TestCase):
 
     def test_repeated_alias(self):
-        client = TestClient()
+        client = TestClient(light=True)
         client.alias("hello/0.x@lasote/channel",  "hello/0.1@lasote/channel")
         client.alias("hello/0.x@lasote/channel",  "hello/0.2@lasote/channel")
         client.alias("hello/0.x@lasote/channel",  "hello/0.3@lasote/channel")
 
     def test_basic(self):
-        client = TestClient(default_server_user=True)
+        client = TestClient(light=True, default_server_user=True)
         for i in (1, 2):
             client.save({"conanfile.py": GenConanfile().with_name("hello").with_version("0.%s" % i)})
             client.run("export . --user=lasote --channel=channel")
@@ -33,6 +33,7 @@ class ConanAliasTest(unittest.TestCase):
         client.save({"conanfile.txt": "[requires]\nchat/1.0@lasote/channel"}, clean_first=True)
 
         client.run("install . --build=missing")
+        assert "chat/1.0@lasote/channel: WARN: legacy: Requirement 'alias' is provided in Conan 2" in client.out
 
         client.assert_listed_require({"hello/0.1@lasote/channel": "Cache"})
         assert "hello/0.x@lasote/channel: hello/0.1@lasote/channel" in client.out
@@ -64,7 +65,7 @@ class ConanAliasTest(unittest.TestCase):
             If we create an alias with the same name as an existing package, it will
             override the package without any warning.
         """
-        t = TestClient()
+        t = TestClient(light=True)
         conanfile = textwrap.dedent("""
             from conan import ConanFile
             class Pkg(ConanFile):
diff --git a/test/integration/command/create_test.py b/test/integration/command/create_test.py
index 7aa9e659b..bf0cd056e 100644
--- a/test/integration/command/create_test.py
+++ b/test/integration/command/create_test.py
@@ -318,6 +318,12 @@ def test_create_build_missing():
     c.assert_listed_binary({"dep/1.0": ("9a4eb3c8701508aa9458b1a73d0633783ecc2270", "Missing")})
     assert "ERROR: Missing prebuilt package for 'dep/1.0'" in c.out
 
+    # The & placeholder also works
+    c.run("create pkg -s os=Linux --build=missing:&", assert_error=True)
+    c.assert_listed_binary({"pkg/1.0": ("4c0c198b627f9af3e038af4da5e6b3ae205c2435", "Build")})
+    c.assert_listed_binary({"dep/1.0": ("9a4eb3c8701508aa9458b1a73d0633783ecc2270", "Missing")})
+    assert "ERROR: Missing prebuilt package for 'dep/1.0'" in c.out
+
 
 def test_create_no_user_channel():
     """ test the --build=pattern and --build=missing:pattern syntax to build missing packages
@@ -356,6 +362,28 @@ def test_create_no_user_channel():
                             "dep3/0.1": (NO_SETTINGS_PACKAGE_ID, "Build")})
 
 
+def test_create_build_missing_negation():
+    tc = TestClient(light=True)
+    tc.save({"dep/conanfile.py": GenConanfile("dep", "1.0"),
+             "lib/conanfile.py": GenConanfile("lib", "1.0").with_requires("dep/1.0"),
+             "pkg/conanfile.py": GenConanfile("pkg", "1.0").with_requires("lib/1.0")})
+
+    tc.run("export dep")
+    tc.run("export lib")
+    tc.run("create pkg --build=missing:~dep/*", assert_error=True)
+
+    tc.assert_listed_binary({"pkg/1.0": ("a72376edfbbdaf97c8608b5fda53cadebac46a20", "Build"),
+                             "lib/1.0": ("abfcc78fa8242cabcd1e3d92896aa24808c789a3", "Build"),
+                             "dep/1.0": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Missing")})
+
+    tc.run("create pkg --build=missing:~dep/* --build=missing:~lib/*",
+           assert_error=True)
+
+    tc.assert_listed_binary({"pkg/1.0": ("a72376edfbbdaf97c8608b5fda53cadebac46a20", "Build"),
+                             "lib/1.0": ("abfcc78fa8242cabcd1e3d92896aa24808c789a3", "Missing"),
+                             "dep/1.0": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Missing")})
+
+
 def test_create_format_json():
     """
     Tests the ``conan create . -f json`` result
@@ -711,22 +739,22 @@ def test_defaults_in_components():
 
     # The paths are absolute and the components have defaults
     # ".+" Check that there is a path, not only "lib"
-    assert re.search(r"BINDIRS: \['.+bin'\]", str(client.out))
-    assert re.search(r"LIBDIRS: \['.+lib'\]", str(client.out))
-    assert re.search(r"INCLUDEDIRS: \['.+include'\]", str(client.out))
+    assert re.search(r"BINDIRS: \['.+bin']", client.out)
+    assert re.search(r"LIBDIRS: \['.+lib']", client.out)
+    assert re.search(r"INCLUDEDIRS: \['.+include']", client.out)
     assert "WARN: RES DIRS: []"
-    assert re.search(r"WARN: FOO LIBDIRS: \['.+lib'\]", str(client.out))
-    assert re.search(r"WARN: FOO INCLUDEDIRS: \['.+include'\]", str(client.out))
+    assert re.search(r"WARN: FOO LIBDIRS: \['.+lib']", client.out)
+    assert re.search(r"WARN: FOO INCLUDEDIRS: \['.+include']", client.out)
     assert "WARN: FOO RESDIRS: []" in client.out
 
     # The paths are absolute and the components have defaults
     # ".+" Check that there is a path, not only "lib"
-    assert re.search("BINDIRS: \['.+bin'\]", str(client.out))
-    assert re.search("LIBDIRS: \['.+lib'\]", str(client.out))
-    assert re.search("INCLUDEDIRS: \['.+include'\]", str(client.out))
+    assert re.search(r"BINDIRS: \['.+bin']", client.out)
+    assert re.search(r"LIBDIRS: \['.+lib']", client.out)
+    assert re.search(r"INCLUDEDIRS: \['.+include']", client.out)
     assert "WARN: RES DIRS: []"
-    assert bool(re.search("WARN: FOO LIBDIRS: \['.+lib'\]", str(client.out)))
-    assert bool(re.search("WARN: FOO INCLUDEDIRS: \['.+include'\]", str(client.out)))
+    assert bool(re.search(r"WARN: FOO LIBDIRS: \['.+lib']", client.out))
+    assert bool(re.search(r"WARN: FOO INCLUDEDIRS: \['.+include']", client.out))
     assert "WARN: FOO RESDIRS: []" in client.out
 
 
diff --git a/test/integration/graph/core/graph_manager_base.py b/test/integration/graph/core/graph_manager_base.py
index 6ff38f269..7f4ac1f73 100644
--- a/test/integration/graph/core/graph_manager_base.py
+++ b/test/integration/graph/core/graph_manager_base.py
@@ -50,7 +50,7 @@ class GraphManagerTest(unittest.TestCase):
     def _put_in_cache(self, ref, conanfile):
         ref = RecipeReference.loads("{}#123".format(ref))
         ref.timestamp = revision_timestamp_now()
-        layout = self.cache.get_or_create_ref_layout(ref)
+        layout = self.cache.create_ref_layout(ref)
         save(layout.conanfile(), str(conanfile))
         manifest = FileTreeManifest.create(layout.export())
         manifest.save(layout.export())
@@ -61,7 +61,7 @@ class GraphManagerTest(unittest.TestCase):
             ref = RecipeReference.loads(ref)
         ref = RecipeReference.loads(repr(ref) + "#{}".format(revision or 123))  # FIXME: Make access
         ref.timestamp = revision_timestamp_now()
-        recipe_layout = self.cache.get_or_create_ref_layout(ref)
+        recipe_layout = self.cache.create_ref_layout(ref)
         save(recipe_layout.conanfile(), str(test_conanfile))
         manifest = FileTreeManifest.create(recipe_layout.export())
         manifest.save(recipe_layout.export())
diff --git a/test/integration/graph/core/graph_manager_test.py b/test/integration/graph/core/graph_manager_test.py
index 290029814..bf620c4ca 100644
--- a/test/integration/graph/core/graph_manager_test.py
+++ b/test/integration/graph/core/graph_manager_test.py
@@ -349,24 +349,23 @@ class TestLinear(GraphManagerTest):
         _check_transitive(app, [(libb, True, True, False, False)])
         _check_transitive(libb, [(liba, False, False, True, True)])
 
-    def test_generic_build_require_adjust_run_with_package_type(self):
-        # FIXME: parametrize when superclass is not unittest
-        for package_type in ["application", "shared-library", "static-library",
-                             "header-library", "build-scripts", None]:
-            # app --br-> cmake (app)
-            self.recipe_conanfile("cmake/0.1", GenConanfile().with_package_type(package_type))
-            # build require with run=None by default
-            consumer = self.recipe_consumer("app/0.1", build_requires=["cmake/0.1"])
-
-            deps_graph = self.build_consumer(consumer)
-
-            self.assertEqual(2, len(deps_graph.nodes))
-            app = deps_graph.root
-            cmake = app.dependencies[0].dst
-
-            # node, headers, lib, build, run
-            run = package_type in ("application", "shared-library", "build-scripts")
-            _check_transitive(app, [(cmake, False, False, True, run)])
+    @parameterized.expand([("application",), ("shared-library",), ("static-library",),
+                           ("header-library",), ("build-scripts",), (None,)])
+    def test_generic_build_require_adjust_run_with_package_type(self, package_type):
+        # app --br-> cmake (app)
+        self.recipe_conanfile("cmake/0.1", GenConanfile().with_package_type(package_type))
+        # build require with run=None by default
+        consumer = self.recipe_consumer("app/0.1", build_requires=["cmake/0.1"])
+
+        deps_graph = self.build_consumer(consumer)
+
+        self.assertEqual(2, len(deps_graph.nodes))
+        app = deps_graph.root
+        cmake = app.dependencies[0].dst
+
+        # node, headers, lib, build, run
+        run = package_type in ("application", "shared-library", "build-scripts")
+        _check_transitive(app, [(cmake, False, False, True, run)])
 
     def test_direct_header_only(self):
         # app -> liba0.1 (header_only)
diff --git a/test/integration/remote/test_local_recipes_index.py b/test/integration/remote/test_local_recipes_index.py
index e96dddce3..2876832b6 100644
--- a/test/integration/remote/test_local_recipes_index.py
+++ b/test/integration/remote/test_local_recipes_index.py
@@ -270,6 +270,30 @@ class TestInstall:
         assert "zlib/0.1: Copied 1 file: patch1" in client.out
         assert "zlib/0.1: Apply patch (file): patches/patch1" in client.out
 
+    def test_export_user_channel(self):
+        folder = temp_folder()
+        recipes_folder = os.path.join(folder, "recipes")
+        zlib_config = textwrap.dedent("""
+            versions:
+              "0.1":
+                folder: all
+            """)
+        zlib = GenConanfile("zlib").with_class_attribute("user='myuser'")\
+                                   .with_class_attribute("channel='mychannel'")
+        conandata_yml = textwrap.dedent("""\
+            versions:
+              "0.1":
+            """)
+        save_files(recipes_folder, {"zlib/config.yml": zlib_config,
+                                    "zlib/all/conanfile.py": str(zlib),
+                                    "zlib/all/conandata.yml": conandata_yml})
+        client = TestClient()
+        client.run(f"remote add local '{folder}'")
+        client.run("install --requires=zlib/0.1@myuser/mychannel --build=missing")
+        assert "zlib/0.1@myuser/mychannel:" in client.out
+        client.run("list * -r=local")
+        assert "zlib/0.1@myuser/mychannel" in client.out
+
 
 class TestRestrictedOperations:
     def test_upload(self):