Repository: conan-io/conan

Issue:
post_package hook corrupts packages
As it is done after the manifest is created, examples like the one in the docs:

```python
 import os
 from conans import tools

 SIGNATURE = "this is my signature"

 def post_package(output, conanfile, conanfile_path, **kwargs):
     sign_path = os.path.join(conanfile.package_folder, ".sign")
     tools.save(sign_path, SIGNATURE)
     output.success("Package signed successfully")
```

Will break the manifest, and ``conan upload --check`` will reject the package as corrupted




Discussion:
This is something for the `docs` repo, right? Or is there anything to change/implement in Conan?
I think we should change the order of the hook, to allow file creating without breaking manifests.
I think it is useful to have a hook that is executed after the package is created and the manifest computed, so I would look for a different approach: the hook can tell Conan back if the package has been modified (return True/False value?), the hook itself can recompute the manifest (it would require exposing and documenting internal functions), or we can always add more hooks (I would prefer not to). 
Yes, it might be useful, but right now I cannot think of any example that really needs to have the manifest already computed. We would need to have stronger evidence or users request to support this case. 

Exposing internal functions to hooks doesn't seem the way to go, and having to rely on the user reminding to return True for correct behavior seems fragile.

In the meanwhile, the current approach for the use case that we already have example for (package signing) is broken, so the priority would be to fix it. If the package signing uses the manifest for its creation, it will be broken too when the manifest is recomputed, so wouldn't it be better if the manifest is not there? 


We've been talking a lot in the past about generating quality artifacts after the package has been created (test reports, doxygen,...) and some users may have hooks that are using the packaged folder to generate those artifacts (store them somewhere else). I cannot know if having the manifest there already computed is important or not for those users.

I totally agree that there is not a good alternative, but changing this behavior may break someone. Some users will want to execute it before the manifest and others afterward (they might be using the manifest as a sentinel to know that the package is ready or even to mark a package as invalid an prevent the upload).

Let me know about the decision and I will write a PR for it.
Run it by default before the manifest calculation. If any issue or breaking behavior is reported we will think about it.

Resolving patch:
diff --git a/conans/client/packager.py b/conans/client/packager.py
--- a/conans/client/packager.py
+++ b/conans/client/packager.py
@@ -25,16 +25,17 @@ def export_pkg(conanfile, package_id, src_package_folder, package_folder, hook_m
     copier = FileCopier([src_package_folder], package_folder)
     copier("*", symlinks=True)
 
+    conanfile.package_folder = package_folder
+    hook_manager.execute("post_package", conanfile=conanfile, conanfile_path=conanfile_path,
+                         reference=ref, package_id=package_id)
+
     save(os.path.join(package_folder, CONANINFO), conanfile.info.dumps())
     manifest = FileTreeManifest.create(package_folder)
     manifest.save(package_folder)
-
     _report_files_from_manifest(output, manifest)
 
     output.success("Package '%s' created" % package_id)
-    conanfile.package_folder = package_folder
-    hook_manager.execute("post_package", conanfile=conanfile, conanfile_path=conanfile_path,
-                         reference=ref, package_id=package_id)
+
     prev = manifest.summary_hash
     output.info("Created package revision %s" % prev)
     return prev
@@ -82,12 +83,15 @@ def create_package(conanfile, package_id, source_folder, build_folder, package_f
             raise
         raise ConanException(e)
 
+    hook_manager.execute("post_package", conanfile=conanfile, conanfile_path=conanfile_path,
+                         reference=ref, package_id=package_id)
+
     manifest = _create_aux_files(install_folder, package_folder, conanfile, copy_info)
     _report_files_from_manifest(package_output, manifest)
     package_id = package_id or os.path.basename(package_folder)
+
     output.success("Package '%s' created" % package_id)
-    hook_manager.execute("post_package", conanfile=conanfile, conanfile_path=conanfile_path,
-                         reference=ref, package_id=package_id)
+
     prev = manifest.summary_hash
     output.info("Created package revision %s" % prev)
     return prev
diff --git a/conans/paths/package_layouts/package_cache_layout.py b/conans/paths/package_layouts/package_cache_layout.py
--- a/conans/paths/package_layouts/package_cache_layout.py
+++ b/conans/paths/package_layouts/package_cache_layout.py
@@ -102,7 +102,7 @@ def packages(self):
     @short_path
     def package(self, pref):
         assert isinstance(pref, PackageReference)
-        assert pref.ref == self._ref
+        assert pref.ref == self._ref, "{!r} != {!r}".format(pref.ref, self._ref)
         return os.path.join(self._base_folder, PACKAGES_FOLDER, pref.id)
 
     def scm_folder(self):


Test patch:
diff --git a/conans/test/functional/hooks/test_post_package.py b/conans/test/functional/hooks/test_post_package.py
new file mode 100644
--- /dev/null
+++ b/conans/test/functional/hooks/test_post_package.py
@@ -0,0 +1,73 @@
+# coding=utf-8
+
+import os
+import unittest
+
+from mock import patch
+
+from conans.client.hook_manager import HookManager
+from conans.model.ref import ConanFileReference
+from conans.paths import CONAN_MANIFEST
+from conans.test.utils.tools import TurboTestClient
+
+
+class PostPackageTestCase(unittest.TestCase):
+
+    def test_create_command(self):
+        """ Test that 'post_package' hook is called before computing the manifest
+        """
+        t = TurboTestClient()
+        filename = "hook_file"
+
+        def post_package_hook(conanfile, **kwargs):
+            # There shouldn't be a manifest yet
+            post_package_hook.manifest_path = os.path.join(conanfile.package_folder, CONAN_MANIFEST)
+            self.assertFalse(os.path.exists(post_package_hook.manifest_path))
+            # Add a file
+            open(os.path.join(conanfile.package_folder, filename), "w").close()
+
+        def mocked_load_hooks(hook_manager):
+            hook_manager.hooks["post_package"] = [("_", post_package_hook)]
+
+        with patch.object(HookManager, "load_hooks", new=mocked_load_hooks):
+            pref = t.create(ConanFileReference.loads("name/version@user/channel"))
+
+        # Check that we are considering the same file
+        package_layout = t.cache.package_layout(pref.ref)
+        self.assertEqual(post_package_hook.manifest_path,
+                         os.path.join(package_layout.package(pref), CONAN_MANIFEST))
+        # Now the file exists and contains info about created file
+        self.assertTrue(os.path.exists(post_package_hook.manifest_path))
+        with open(post_package_hook.manifest_path) as f:
+            content = f.read()
+            self.assertIn(filename, content)
+
+    def test_export_pkg_command(self):
+        """ Test that 'post_package' hook is called before computing the manifest
+        """
+        t = TurboTestClient()
+        filename = "hook_file"
+
+        def post_package_hook(conanfile, **kwargs):
+            # There shouldn't be a manifest yet
+            post_package_hook.manifest_path = os.path.join(conanfile.package_folder, CONAN_MANIFEST)
+            self.assertFalse(os.path.exists(post_package_hook.manifest_path))
+            # Add a file
+            open(os.path.join(conanfile.package_folder, filename), "w").close()
+
+        def mocked_load_hooks(hook_manager):
+            hook_manager.hooks["post_package"] = [("_", post_package_hook)]
+
+        with patch.object(HookManager, "load_hooks", new=mocked_load_hooks):
+            pref = t.export_pkg(ref=ConanFileReference.loads("name/version@user/channel"),
+                                args="--package-folder=.")
+
+        # Check that we are considering the same file
+        package_layout = t.cache.package_layout(pref.ref)
+        self.assertEqual(post_package_hook.manifest_path,
+                         os.path.join(package_layout.package(pref), CONAN_MANIFEST))
+        # Now the file exists and contains info about created file
+        self.assertTrue(os.path.exists(post_package_hook.manifest_path))
+        with open(post_package_hook.manifest_path) as f:
+            content = f.read()
+            self.assertIn(filename, content)
diff --git a/conans/test/utils/tools.py b/conans/test/utils/tools.py
--- a/conans/test/utils/tools.py
+++ b/conans/test/utils/tools.py
@@ -906,6 +906,22 @@ def upload_all(self, ref, remote=None, args=None, assert_error=False):
     def remove_all(self):
         self.run("remove '*' -f")
 
+    def export_pkg(self, ref, conanfile=None, args=None, assert_error=False):
+        conanfile = str(conanfile) if conanfile else str(GenConanfile())
+        self.save({"conanfile.py": conanfile})
+        self.run("export-pkg . {} {} --json {}".format(ref.full_str(),
+                                                       args or "", self.tmp_json_name),
+                 assert_error=assert_error)
+        rrev = self.cache.package_layout(ref).recipe_revision()
+        json_path = os.path.join(self.current_folder, self.tmp_json_name)
+        data = json.loads(load(json_path))
+        if assert_error:
+            return None
+        package_id = data["installed"][0]["packages"][0]["id"]
+        package_ref = PackageReference(ref, package_id)
+        prev = self.cache.package_layout(ref.copy_clear_rev()).package_revision(package_ref)
+        return package_ref.copy_with_revs(rrev, prev)
+
     def recipe_exists(self, ref):
         return self.cache.package_layout(ref).recipe_exists()