Repository: conan-io/conan

Issue:
[feature] Re consider the install() method 
### What is your suggestion?

This has been appearing again, give it another try

### Have you read the CONTRIBUTING guide?

- [X] I've read the CONTRIBUTING guide
[bug] __pycache__ presence violates cached package integrity
### Describe the bug

OS: Ubuntu-22.04
Conan version: 2.5.0
Conan profile:
```
[settings]
arch=x86_64
build_type=RelWithDebInfo
compiler=clang
compiler.cppstd=20
compiler.libcxx=libstdc++11
compiler.version=15
os=Linux
[conf]
tools.build:compiler_executables={'c': 'clang-15', 'cpp': 'clang++-15'}
```

Hello,

I came across this issue where GLib package has some python scripts in res folder, which are executed by GLib conan tests. This execution creates `__pycache__` files, which aren't supposed to be packaged, but cache integrity checker still evaluates them and errors out, because they are not mentioned in the manifest file. Cache integrity is kind of important to me, because it is executed as part of `conan upload`.

Regards,
Vilius

### How to reproduce it

```shell
$ conan create recipes/glib/all/conanfile.py --version 2.78.3 --build missing
$ conan cache check-integrity glib

Found 1 pkg/version recipes matching glib in local cache
glib/2.78.3: Integrity checked: ok
ERROR: glib/2.78.3:7f28aa2046b112e6880a8ce24f0b337eba9f5e55: Manifest mismatch
ERROR: Folder: /home/runner/.conan2/p/b/glib500aee2c65c90/p
ERROR:     'res/glib-2.0/codegen/__pycache__/utils.cpython-312.pyc' (manifest: None, file: 11b0ae06f1cdfefbc5ce50724f84b998)
ERROR:     'res/glib-2.0/codegen/__pycache__/__init__.cpython-312.pyc' (manifest: None, file: 6332e31c47ed9672e6eeadf2ad32d2fb)
ERROR:     'res/glib-2.0/codegen/__pycache__/codegen_md.cpython-312.pyc' (manifest: None, file: 4ea4ae2cf7c05fb48c28249addaa6261)
ERROR:     'res/glib-2.0/codegen/__pycache__/codegen.cpython-312.pyc' (manifest: None, file: 5c9a01394688c58cabc7b6718b6796d7)
ERROR:     'res/glib-2.0/codegen/__pycache__/codegen_rst.cpython-312.pyc' (manifest: None, file: 30e8c339625a71f34303159988cdb6ad)
ERROR:     'res/glib-2.0/codegen/__pycache__/dbustypes.cpython-312.pyc' (manifest: None, file: b636ecfbcb1155c8f33dc055da1f775d)
ERROR:     'res/glib-2.0/codegen/__pycache__/config.cpython-312.pyc' (manifest: None, file: fa337bc9277fd63cd045362af3eb9945)
ERROR:     'res/glib-2.0/codegen/__pycache__/parser.cpython-312.pyc' (manifest: None, file: 02e5daeb7be2ee940cfad7b9760cebcf)
ERROR:     'res/glib-2.0/codegen/__pycache__/codegen_main.cpython-312.pyc' (manifest: None, file: 2a4daec49470b399b7ba70aeec4dc6d4)
ERROR:     'res/glib-2.0/codegen/__pycache__/codegen_docbook.cpython-312.pyc' (manifest: None, file: 538d97fe4b8866d1f52f593362f00fdc)
ERROR: There are corrupted artifacts, check the error logs
```

Issue isolated and reproduced in this GitHub actions run - https://github.com/ViliusSutkus89/conan-center-index/actions/runs/10031877077/job/27722820637


Discussion:

Patch:
diff --git a/conan/api/subapi/cache.py b/conan/api/subapi/cache.py
index 19a6db3dace..83967243151 100644
--- a/conan/api/subapi/cache.py
+++ b/conan/api/subapi/cache.py
@@ -63,6 +63,8 @@ def package_path(self, pref: PkgReference):
         app = ConanApp(self.conan_api)
         pref = _resolve_latest_pref(app, pref)
         ref_layout = app.cache.pkg_layout(pref)
+        if os.path.exists(ref_layout.finalize()):
+            return ref_layout.finalize()
         return _check_folder_existence(pref, "package", ref_layout.package())
 
     def check_integrity(self, package_list):
diff --git a/conan/internal/cache/conan_reference_layout.py b/conan/internal/cache/conan_reference_layout.py
index 9933dcbbbb8..9bcd58160c7 100644
--- a/conan/internal/cache/conan_reference_layout.py
+++ b/conan/internal/cache/conan_reference_layout.py
@@ -10,6 +10,7 @@
 SRC_FOLDER = "s"
 BUILD_FOLDER = "b"
 PACKAGES_FOLDER = "p"
+FINALIZE_FOLDER = "f"
 EXPORT_FOLDER = "e"
 EXPORT_SRC_FOLDER = "es"
 DOWNLOAD_EXPORT_FOLDER = "d"
@@ -55,7 +56,7 @@ def export_sources(self):
         return os.path.join(self._base_folder, EXPORT_SRC_FOLDER)
 
     def metadata(self):
-        return os.path.join(self.download_export(), "metadata")
+        return os.path.join(self.download_export(), METADATA)
 
     def download_export(self):
         return os.path.join(self._base_folder, DOWNLOAD_EXPORT_FOLDER)
@@ -99,11 +100,14 @@ def build(self):
     def package(self):
         return os.path.join(self._base_folder, PACKAGES_FOLDER)
 
+    def finalize(self):
+        return os.path.join(self._base_folder, FINALIZE_FOLDER)
+
     def download_package(self):
         return os.path.join(self._base_folder, DOWNLOAD_EXPORT_FOLDER)
 
     def metadata(self):
-        return os.path.join(self.download_package(), "metadata")
+        return os.path.join(self.download_package(), METADATA)
 
     def package_manifests(self):
         package_folder = self.package()
diff --git a/conans/client/installer.py b/conans/client/installer.py
index ea679f44e56..b2937dda895 100644
--- a/conans/client/installer.py
+++ b/conans/client/installer.py
@@ -338,7 +338,9 @@ def _handle_package(self, package, install_reference, handled_count, total_count
             # Call the info method
             conanfile.folders.set_base_package(pkg_folder)
             conanfile.folders.set_base_pkg_metadata(pkg_metadata)
-            self._call_package_info(conanfile, pkg_folder, is_editable=False)
+            self._call_finalize_method(conanfile, package_layout.finalize())
+            # Use package_folder which has been updated previously by install_method if necessary
+            self._call_package_info(conanfile, conanfile.package_folder, is_editable=False)
 
     def _handle_node_editable(self, install_node):
         # It will only run generation
@@ -454,3 +456,15 @@ def _call_package_info(self, conanfile, package_folder, is_editable):
                 self._hook_manager.execute("post_package_info", conanfile=conanfile)
 
         conanfile.cpp_info.check_component_requires(conanfile)
+
+    def _call_finalize_method(self, conanfile, finalize_folder):
+        if hasattr(conanfile, "finalize"):
+            conanfile.folders.set_finalize_folder(finalize_folder)
+            if not os.path.exists(finalize_folder):
+                mkdir(finalize_folder)
+                conanfile.output.highlight("Calling finalize()")
+                with conanfile_exception_formatter(conanfile, "finalize"):
+                    with conanfile_remove_attr(conanfile, ['cpp_info', 'settings', 'options'], 'finalize'):
+                        conanfile.finalize()
+
+            conanfile.output.success(f"Finalized folder {finalize_folder}")
diff --git a/conans/model/conan_file.py b/conans/model/conan_file.py
index 0c6bedc7e6d..50cecb9b01e 100644
--- a/conans/model/conan_file.py
+++ b/conans/model/conan_file.py
@@ -164,6 +164,7 @@ def serialize(self):
         result["build_folder"] = self.build_folder
         result["generators_folder"] = self.generators_folder
         result["package_folder"] = self.package_folder
+        result["immutable_package_folder"] = self.immutable_package_folder
 
         result["cpp_info"] = self.cpp_info.serialize()
         result["conf_info"] = self.conf_info.serialize()
@@ -318,6 +319,10 @@ def package_folder(self):
         """
         return self.folders.base_package
 
+    @property
+    def immutable_package_folder(self):
+        return self.folders.immutable_package_folder
+
     @property
     def generators_folder(self):
         return self.folders.generators_folder
diff --git a/conans/model/conanfile_interface.py b/conans/model/conanfile_interface.py
index 2130ff85be2..29a1607923f 100644
--- a/conans/model/conanfile_interface.py
+++ b/conans/model/conanfile_interface.py
@@ -39,6 +39,10 @@ def recipe_metadata_folder(self):
     def package_folder(self):
         return self._conanfile.package_folder
 
+    @property
+    def immutable_package_folder(self):
+        return self._conanfile.immutable_package_folder
+
     @property
     def package_metadata_folder(self):
         return self._conanfile.package_metadata_folder
diff --git a/conans/model/layout.py b/conans/model/layout.py
index a06795550f8..de453f917fe 100644
--- a/conans/model/layout.py
+++ b/conans/model/layout.py
@@ -45,6 +45,7 @@ def __init__(self):
 
         self._base_recipe_metadata = None
         self._base_pkg_metadata = None
+        self._immutable_package_folder = None
 
         self.source = ""
         self.build = ""
@@ -142,6 +143,14 @@ def package_folder(self):
         """For the cache, the package folder is only the base"""
         return self._base_package
 
+    def set_finalize_folder(self, folder):
+        self._immutable_package_folder = self.package_folder
+        self.set_base_package(folder)
+
+    @property
+    def immutable_package_folder(self):
+        return self._immutable_package_folder or self.package_folder
+
     @property
     def generators_folder(self):
         if self._base_generators is None:


Test patch:
diff --git a/conan/test/assets/genconanfile.py b/conan/test/assets/genconanfile.py
index ebd4ae121a5..717b12b272a 100644
--- a/conan/test/assets/genconanfile.py
+++ b/conan/test/assets/genconanfile.py
@@ -27,6 +27,7 @@ def __init__(self, name=None, version=None):
         self._provides = None
         self._deprecated = None
         self._package_lines = None
+        self._finalize_lines = None
         self._package_files = None
         self._package_files_env = None
         self._package_files_link = None
@@ -48,6 +49,8 @@ def __init__(self, name=None, version=None):
         self._cmake_build = False
         self._class_attributes = None
 
+        self._is_tested_ref_build_require = None
+
     def with_package_type(self, value):
         self._package_type = value
         return self
@@ -143,6 +146,10 @@ def with_test_requires(self, *refs):
             self._test_requires.append(ref_str)
         return self
 
+    def with_test_reference_as_build_require(self):
+        self._is_tested_ref_build_require = True
+        return self
+
     def with_build_requirement(self, ref, **kwargs):
         self._build_requirements = self._build_requirements or []
         ref_str = self._get_full_ref_str(ref)
@@ -155,9 +162,10 @@ def with_tool_requirement(self, ref, **kwargs):
         self._tool_requirements.append((ref_str, kwargs))
         return self
 
-    def with_import(self, i):
-        if i not in self._imports:
-            self._imports.append(i)
+    def with_import(self, *imports):
+        for i in imports:
+            if i not in self._imports:
+                self._imports.append(i)
         return self
 
     def with_setting(self, setting):
@@ -207,6 +215,12 @@ def with_package(self, *lines):
             self._package_lines.append(line)
         return self
 
+    def with_finalize(self, *lines):
+        self._finalize_lines = self._finalize_lines or []
+        for line in lines:
+            self._finalize_lines.append(line)
+        return self
+
     def with_build_msg(self, msg):
         self._build_messages = self._build_messages or []
         self._build_messages.append(msg)
@@ -222,14 +236,16 @@ def with_package_info(self, cpp_info=None, env_info=None):
             self._package_info["env_info"] = env_info
         return self
 
-    def with_package_id(self, line):
+    def with_package_id(self, *lines):
         self._package_id_lines = self._package_id_lines or []
-        self._package_id_lines.append(line)
+        for line in lines:
+            self._package_id_lines.append(line)
         return self
 
-    def with_test(self, line):
+    def with_test(self, *lines):
         self._test_lines = self._test_lines or []
-        self._test_lines.append(line)
+        for line in lines:
+            self._test_lines.append(line)
         return self
 
     def with_cmake_build(self):
@@ -360,6 +376,10 @@ def _package_method(self):
         return (self._package_lines or self._package_files or self._package_files_env or
                 self._package_files_link)
 
+    @property
+    def _finalize_method(self):
+        return self._finalize_lines
+
     @property
     def _package_method_render(self):
         lines = []
@@ -388,6 +408,19 @@ def package(self):
 {}
     """.format("\n".join(lines))
 
+    @property
+    def _finalize_method_render(self):
+        lines = []
+        if self._finalize_lines:
+            lines.extend("        {}".format(line) for line in self._finalize_lines)
+
+        if not lines:
+            return ""
+        return """
+    def finalize(self):
+{}
+        """.format("\n".join(lines))
+
     @property
     def _build_render(self):
         if not self._build_messages and not self._cmake_build:
@@ -432,11 +465,20 @@ def package_id(self):
 
     @property
     def _test_lines_render(self):
-        lines = ["",
-                 "    def requirements(self):",
-                 '         self.requires(self.tested_reference_str)',
-                 "",
-                 '    def test(self):'] + ['        %s' % m for m in self._test_lines]
+        if self._is_tested_ref_build_require:
+            lines = ["",
+                     "    def build_requirements(self):",
+                     '         self.tool_requires(self.tested_reference_str)',
+                     "",
+                     '    def test(self):']
+        else:
+            lines = ["",
+                     "    def requirements(self):",
+                     '         self.requires(self.tested_reference_str)',
+                     "",
+                     '    def test(self):']
+
+        lines += ['        %s' % m for m in self._test_lines]
         return "\n".join(lines)
 
     @property
@@ -463,7 +505,8 @@ def __repr__(self):
                        "exports_sources", "exports", "generators", "requires", "build_requires",
                        "tool_requires", "test_requires", "requirements", "python_requires",
                        "revision_mode", "settings", "options", "default_options", "build",
-                       "package_method", "package_info", "package_id_lines", "test_lines"
+                       "package_method", "package_info", "package_id_lines", "test_lines",
+                       "finalize_method"
                        ):
             if member == "requirements":
                 # FIXME: This seems exclusive, but we could mix them?
diff --git a/test/integration/conanfile/test_finalize_method.py b/test/integration/conanfile/test_finalize_method.py
new file mode 100644
index 00000000000..0d1f02ce8bd
--- /dev/null
+++ b/test/integration/conanfile/test_finalize_method.py
@@ -0,0 +1,363 @@
+import json
+import os
+import textwrap
+
+import pytest
+
+from conan.test.assets.genconanfile import GenConanfile
+
+from conan.test.utils.tools import TestClient
+from conans.util.files import load, save
+
+conanfile_dep = textwrap.dedent("""
+    import os
+    from conan import ConanFile
+    from conan.tools.files import save, copy
+
+    class TestConan(ConanFile):
+        name = "dep"
+        version = "1.0"
+        def package(self):
+            save(self, os.path.join(self.package_folder, "file.txt"), "Hello World!")
+            save(self, os.path.join(self.package_folder, "file2.txt"), "Hello World 2!")
+
+        def finalize(self):
+            self.output.info(f"Running finalize method in {self.package_folder}")
+            copy(self, "file.txt", src=self.immutable_package_folder, dst=self.package_folder)
+            save(self, os.path.join(self.package_folder, "finalized.txt"), "finalized file")
+
+        def package_info(self):
+            self.output.info(f"Running package_info method in {self.package_folder}")
+    """)
+
+
+class TestBasicLocalFlows:
+
+    @pytest.fixture
+    def client(self):
+        tc = TestClient(light=True)
+        tc.save({"dep/conanfile.py": conanfile_dep})
+        tc.run("export dep")
+        return tc
+
+    def test_basic_finalize_method(self, client):
+        client.run("create dep")
+        layout = client.created_layout()
+        assert layout.package().endswith("p")
+        assert f"Package folder {layout.package()}" in client.out
+        assert f"Running finalize method in {layout.finalize()}" in client.out
+        assert f"Running package_info method in {layout.finalize()}" in client.out
+        client.run("install --requires=dep/1.0")
+        assert f"Running package_info method in {layout.finalize()}" in client.out
+        # Only issue is that the PackageLayout has no idea about the redirected package folder
+        # So we have to know to check for it in tests, but oh well
+        assert "finalized.txt" in os.listdir(layout.finalize())
+        assert "finalized.txt" not in os.listdir(layout.package())
+
+    def test_dependency_finalize_method(self, client):
+        client.save({"app/conanfile.py": textwrap.dedent("""
+                 from conan import ConanFile
+                 class TestConan(ConanFile):
+                     name = "app"
+                     version = "1.0"
+                     requires = "dep/1.0"
+                     def generate(self):
+                         self.output.info("Running generate method")
+                         dep_pkg_folder = self.dependencies["dep"].package_folder
+                         self.output.info(f"Dep package folder: {dep_pkg_folder}")
+                 """)})
+        client.run("create dep")
+        dep_layout = client.created_layout()
+        client.run("create app")
+        assert f"Dep package folder: {dep_layout.package()}" not in client.out
+        assert f"Dep package folder: {dep_layout.finalize()}" in client.out
+
+    def test_no_non_info_access(self):
+        client = TestClient(light=True)
+        client.save({"conanfile.py": GenConanfile("dep", "1.0")
+                     .with_finalize("self.output.info('settings.os: ' + self.settings.os)")})
+        client.run("create .", assert_error=True)
+        assert "'self.settings' access in 'finalize()' method is forbidden" in client.out
+
+    def test_finalize_moves_from_package(self):
+        client = TestClient(light=True)
+        client.save({"conanfile.py": GenConanfile("dep", "1.0")
+                     .with_import("from conan.tools.files import save, rename",
+                                  "import os")
+                     .with_option("move", [True, False])
+                     .with_package('save(self, os.path.join(self.package_folder, "file.txt"), "Hello World!")',
+                                   "save(self, os.path.join(self.package_folder, 'file2.txt'), 'Hello World 2!')")
+                     # This is NOT allowed, moving from package to finalize is forbidden, only as test to ensure consistency
+                     .with_finalize("rename(self, os.path.join(self.immutable_package_folder, 'file.txt'), os.path.join(self.package_folder, 'file.txt')) if self.info.options.move else None")})
+        client.run("create . -o=dep/*:move=True")
+        dep_moved_layout = client.created_layout()
+        assert "file.txt" in os.listdir(dep_moved_layout.finalize())
+        assert "file.txt" not in os.listdir(dep_moved_layout.package())
+
+        client.run("create . -o=dep/*:move=False")
+        dep_kept_layout = client.created_layout()
+        assert "file.txt" not in os.listdir(dep_kept_layout.finalize())
+        assert "file.txt" in os.listdir(dep_kept_layout.package())
+
+        # Now we can check that the package_id is the same for both
+        assert dep_moved_layout.reference.package_id != dep_kept_layout.reference.package_id
+
+        # This now breaks if we try to cache check-integrity the moved package
+        client.run(f"cache check-integrity {dep_moved_layout.reference}", assert_error=True)
+        assert "There are corrupted artifacts" in client.out
+
+        client.run(f"cache check-integrity {dep_kept_layout.reference}")
+        assert "There are corrupted artifacts" not in client.out
+
+    def test_cache_path_command(self, client):
+        client.run("create dep")
+        dep_layout = client.created_layout()
+        pref = client.created_package_reference("dep/1.0")
+        client.run(f"cache path {pref}")
+        assert dep_layout.package() not in client.out
+        assert dep_layout.finalize() in client.out
+
+    def test_remove_deletes_correct_folders(self, client):
+        client.run("create dep")
+        dep_layout = client.created_layout()
+        client.run("remove * -c")
+        assert not os.path.exists(dep_layout.package())
+        assert not os.path.exists(dep_layout.finalize())
+
+    def test_save_restore_cache(self, client):
+        # Not created in the cache, just exported, nothing breaks because there is not even a package there
+        client.run("cache save *:*")
+        client.run("remove * -c")
+        client.run("cache restore conan_cache_save.tgz")
+
+        # Now create the package and then save/restore
+        client.run("create dep")
+        dep_layout = client.created_layout()
+        client.run("cache save *:* -f=json", redirect_stdout="saved.json")
+        saved = json.loads(client.load("saved.json"))
+        pref = dep_layout.reference
+        saved_pkg_folder = saved["Local Cache"]["dep/1.0"]["revisions"][pref.ref.revision]["packages"][pref.package_id]["revisions"][pref.revision]["package_folder"]
+        assert saved_pkg_folder in dep_layout.package().replace("\\", "/")
+        client.run("remove * -c")
+        assert not os.path.exists(dep_layout.package())
+        assert not os.path.exists(dep_layout.finalize())
+        client.run("cache restore conan_cache_save.tgz")
+        client.run(f"cache path {dep_layout.reference}")
+        package_folder = client.out.strip()
+
+        # The finalize() folder does not exist as restoring is not considered usage, so it never runs
+        # so this is just the immutable package_folder
+        assert "finalized.txt" not in os.listdir(package_folder)
+
+        # But as soon as you call conan finalize, finalize() is called and so it's used,
+        # so package_folder will be the finalize folder
+        client.run("install --requires=dep/1.0")
+        client.run(f"cache path {dep_layout.reference}")
+        package_folder = client.out.strip()
+        assert "finalized.txt" in os.listdir(package_folder)
+
+    def test_graph_info_output(self, client):
+        client.run("create dep")
+        dep_layout = client.created_layout()
+        client.run("install --requires=dep/1.0 -f=json", redirect_stdout="finalize.json")
+        finalize_output = json.loads(client.load("finalize.json"))
+        assert finalize_output["graph"]["nodes"]["1"]["package_folder"] == dep_layout.finalize()
+        assert finalize_output["graph"]["nodes"]["1"]["immutable_package_folder"] == dep_layout.package()
+
+    def test_create_pkglist_output(self, client):
+        client.run("create dep -f=json", redirect_stdout="created.json")
+        created_pkgid = client.created_package_id("dep/1.0")
+        client.run("list --graph=created.json --graph-binaries=build")
+        assert created_pkgid in client.out
+
+    def test_vendorized_basic(self, client):
+        client.run("create dep")
+        client.save({"vendor/conanfile.py": GenConanfile("vendor", "1.0")
+                     .with_import("from conan.tools.files import copy")
+                     .with_class_attribute("vendor=True")
+                     .with_requires("dep/1.0")
+                     .with_package("copy(self, 'file.txt', src=self.dependencies['dep'].package_folder, dst=self.package_folder)",
+                                   "copy(self, 'finalized.txt', src=self.dependencies['dep'].package_folder, dst=self.package_folder)",
+                                   "copy(self, 'file2.txt', src=self.dependencies['dep'].immutable_package_folder, dst=self.package_folder)")})
+        client.run("create vendor")
+        vendor_layout = client.created_layout()
+        assert "file.txt" in os.listdir(vendor_layout.package())
+        assert "finalized.txt" in os.listdir(vendor_layout.package())
+        assert "file2.txt" in os.listdir(vendor_layout.package())
+
+    def test_check_integrity(self, client):
+        client.run("create dep")
+        dep_layout = client.created_layout()
+        client.run(f"cache check-integrity {dep_layout.reference}")
+        assert "There are corrupted artifacts" not in client.out
+        # Even if we re-change the finalize folder contents, it should still be fine
+        save(os.path.join(dep_layout.finalize(), "finalized.txt"), "Modified!")
+        client.run(f"cache check-integrity {dep_layout.reference}")
+        assert "There are corrupted artifacts" not in client.out
+        # But as soon as we change the package, it should still fail like a normal package would
+        save(os.path.join(dep_layout.package(), "file.txt"), "Modified!")
+        client.run(f"cache check-integrity {dep_layout.reference}", assert_error=True)
+        assert "There are corrupted artifacts" in client.out
+
+    @pytest.mark.parametrize("with_finalize_method", [True, False])
+    def test_access_immutable_from_consumer(self, client, with_finalize_method):
+        if not with_finalize_method:
+            client.save({"dep/conanfile.py": GenConanfile("dep", "1.0")})
+        client.save({"app/conanfile.py": GenConanfile("app", "1.0")
+                     .with_requires("dep/1.0")
+                     .with_package("dep = self.dependencies['dep/1.0']",
+                                   "self.output.info(f'Immutable package: {dep.immutable_package_folder}')",
+                                   # TODO: Think about if we want this interface
+                                   # "self.output.info(f'finalize: {dep.finalize_folder}')",
+                                   "self.output.info(f'Package: {dep.package_folder}')")})
+        client.run("create dep")
+        dep_layout = client.created_layout()
+        client.run("create app")
+        assert f"app/1.0: Immutable package: {dep_layout.package()}" in client.out
+        # assert f"app/1.0: finalize: {dep_layout.finalize()}" in client.out
+        if with_finalize_method:
+            assert f"app/1.0: Package: {dep_layout.finalize()}" in client.out
+        else:
+            assert f"app/1.0: Package: {dep_layout.package()}" in client.out
+
+    def test_cache_modification_of_custom_conf_based_on_settings(self):
+        tc = TestClient(light=True)
+        tc.save({"conanfile.py": GenConanfile("dep", "1.0")
+                .with_import("from conan.tools.files import save",
+                             "import os")
+                .with_option("myoption", [True, False])
+                .with_option("otheroption", [True, False])
+                .with_default_option("myoption", False)
+                .with_default_option("otheroption", False)
+                .with_setting("os")
+                .with_package_id("del self.info.options.myoption")
+                .with_finalize("save(self, os.path.join(self.package_folder, 'file.txt'), 'Hello World!')",
+                              "save(self, os.path.join(self.package_folder, 'os.conf'), str(self.info.settings.os))",
+                              "save(self, os.path.join(self.package_folder, 'option.conf'), str(self.info.options.get_safe('myoption')))",
+                              "save(self, os.path.join(self.package_folder, 'otheroption.conf'), str(self.info.options.otheroption))")})
+        tc.run("create . -s=os=Linux -o=&:myoption=True -o=&:otheroption=True")
+        layout = tc.created_layout()
+        assert "file.txt" in os.listdir(layout.finalize())
+        assert tc.load(os.path.join(layout.finalize(), "os.conf")) == "Linux"
+        # This is problematic, it means that the mapping for finalize() and package_id would not be 1:1 and could be outdated
+        assert tc.load(os.path.join(layout.finalize(), "option.conf")) == "None"
+        assert tc.load(os.path.join(layout.finalize(), "otheroption.conf")) == "True"
+
+
+class TestToolRequiresFlows:
+    def test_tool_requires(self):
+        tc = TestClient(light=True)
+        tc.save({"dep/conanfile.py": textwrap.dedent("""
+            import os
+            from conan import ConanFile
+            from conan.tools.files import save, copy
+
+            class TestConan(ConanFile):
+                name = "dep"
+                version = "1.0"
+                package_type = "application"
+                def package(self):
+                    save(self, os.path.join(self.package_folder, "bin", "executable.txt"), "Base")
+
+                def finalize(self):
+                    self.output.info(f"Running finalize method in {self.package_folder}")
+                    copy(self, "*", src=self.immutable_package_folder, dst=self.package_folder)
+                    save(self, os.path.join(self.package_folder, "bin", "finalized.txt"), "finalized file")
+
+                def package_info(self):
+                    self.output.info(f"Running package_info method in {self.package_folder}")
+                    self.cpp_info.bindirs = ["bin"]
+
+            """), "app/conanfile.py": textwrap.dedent("""
+            from conan import ConanFile
+            import os
+
+            class TestConan(ConanFile):
+                name = "app"
+                version = "1.0"
+
+                def build_requirements(self):
+                    self.tool_requires("dep/1.0")
+
+                def build(self):
+                    self.output.info("Running build method")
+                    bindir = self.dependencies.build['dep'].cpp_info.bindir
+                    self.output.info(f"Dep bindir: {bindir}")
+                    self.output.info(f"Is finalized? {os.path.exists(os.path.join(bindir, 'finalized.txt'))}")
+            """)})
+        tc.run("create dep --build-require")
+        dep_layout = tc.created_layout()
+        tc.run("create app")
+        # This fails. cpp_info is using the original package folder to construct the final path
+        assert f"Dep bindir: {dep_layout.finalize()}" in tc.out
+        assert "app/1.0: Is finalized? True" in tc.out
+
+    def test_test_package_uses_created_tool_which_modifies_pkgfolder(self):
+        tc = TestClient(light=True)
+        tc.save({"conanfile.py": GenConanfile("app", "1.0")
+                .with_import("from conan.tools.files import save")
+                .with_package_type("application")
+                .with_package("save(self, 'file.txt', 'Hello World!')")
+                .with_package_info({"bindirs": ["bin"]}, {})
+                .with_finalize("save(self, 'finalized.txt', 'finalized file')"),
+                 "test_package/conanfile.py": GenConanfile()
+                .with_import("from conan.tools.files import save",
+                             "import os")
+                .with_test_reference_as_build_require()
+                .with_test("bindir = self.dependencies.build[self.tested_reference_str].cpp_info.bindir",
+                           "self.output.info(f'Bindir: {bindir}')",
+                           "save(self, os.path.join(bindir, '__pycache__.pyc'), 'Test file')")})
+        tc.run("create . --build-require")
+        app_layout = tc.created_layout()
+        assert f"Bindir: {os.path.join(app_layout.finalize(), 'bin')}" in tc.out
+        tc.run(f"cache check-integrity {app_layout.reference}")
+        assert "There are corrupted artifacts" not in tc.out
+
+
+class TestRemoteFlows:
+
+    @pytest.fixture
+    def client(self):
+        tc = TestClient(light=True, default_server_user=True)
+        tc.save({"dep/conanfile.py": conanfile_dep})
+        tc.run("export dep")
+        return tc
+
+    def test_remote_upload_finalize_method(self, client):
+        client.run("create dep")
+        created_pref = client.created_package_reference("dep/1.0")
+        client.run("upload * -r=default -c")
+
+        # Only the package folder is uploaded, not the finalize folder
+        uploaded_pref_path = client.servers["default"].test_server.server_store.package(created_pref)
+        manifest_contents = load(os.path.join(uploaded_pref_path, "conanmanifest.txt"))
+        assert "file.txt" in manifest_contents
+        assert "finalized.txt" not in manifest_contents
+
+        client.run("remove * -c")
+        client.run(f"download {created_pref} -r=default")
+        downloaded_pref_layout = client.get_latest_pkg_layout(created_pref)
+        assert "file.txt" in os.listdir(downloaded_pref_layout.package())
+        # Download is not an "usage" of the package, so no finalize() is yet executed
+        assert "finalized.txt" not in os.listdir(downloaded_pref_layout.package())
+        assert not os.path.exists(os.path.join(downloaded_pref_layout.finalize()))
+
+        client.run(f"cache path {created_pref}")
+        package_folder = client.out.strip()
+        assert package_folder == downloaded_pref_layout.package()
+        assert package_folder.endswith("p")
+        # Now this finalize will run the finalize() method
+        client.run("install --requires=dep/1.0")
+        assert f"Running finalize method in {downloaded_pref_layout.finalize()}" in client.out
+
+        client.run("remove * -c")
+        client.run("install --requires=dep/1.0 -r=default")
+        assert "dep/1.0: Calling finalize()"
+        assert f"Running finalize method in {downloaded_pref_layout.finalize()}" in client.out
+
+    def test_upload_verify_integrity(self, client):
+        client.run("create dep")
+        dep_layout = client.created_layout()
+        client.run("upload * -r=default -c --check")
+        assert f"dep/1.0:{dep_layout.reference.package_id}: Integrity checked: ok" in client.out
+        assert "There are corrupted artifacts" not in client.out