Repository: conan-io/conan

Issue:
Log cpp_info as json in conan outputs
Very regularly when creating packages, both OSS and enterprise side, a lot of time is spent troubleshooting various problems relating to package consumption.  Often, the problem is a result of incorrect `package_info()` method.  Very often, looking at a recipe does not make it clear what the actual values of `cpp_info` will be.  This is because we have dynamic mechanisms like `tools.collect_libs()` and also now with `python_requires`, but even simple conditionals based on platform and settings can make it virtually impossible to determine what was passed during a build.  Today, the only way to investigate what was actually passed is by looking at the `conanbuildinfo` files which were generated, and in docker builds, those are deleted on the fly. 

The request here is self-explanatory, simple to implement, and i think would be universally useful.  Add a logging statement that shows exactly the values of `cpp_info`, which is really a simple dict()`.  My suggestion would be to serialize it as JSON, but that's a minor detail. 

Printing it by default would be far too verbose.  However, Conan has some special log settings already, so perhaps there could be a new log setting to control this item. 

For example:  `CONAN_LOG_CPP_INFO`

I'm pretty sure this will dramatically improve the debugging experience for users who are starting to deploy Conan in large enterprise codebases.  I will probably be turning it on for all my CI builds in my company. 

- [x] I've read the [CONTRIBUTING guide](https://raw.githubusercontent.com/conan-io/conan/develop/.github/CONTRIBUTING.md).
- [x] I've specified the Conan version, operating system version and any tool that can be relevant.
- [x] I've explained the steps to reproduce the error or the motivation/use case of the question/suggestion.




Discussion:
I understand and like the idea. We need to decide how will it look to implement it asap.  Could make sense to include a node in the [install json output](https://docs.conan.io/en/latest/reference/commands/output/install.html) in each "package"? 
Yes, love that idea, so i've opened a separate ticket for enhancing the outputs of other commands. https://github.com/conan-io/conan/issues/3648

Resolving patch:
diff --git a/conans/client/installer.py b/conans/client/installer.py
--- a/conans/client/installer.py
+++ b/conans/client/installer.py
@@ -324,6 +324,7 @@ def _handle_node_cache(self, node, package_ref, keep_build, processed_package_re
                 clean_dirty(package_folder)
             # Call the info method
             self._call_package_info(conan_file, package_folder)
+            self._recorder.package_cpp_info(package_ref, conan_file.cpp_info)
 
     def _handle_node_workspace(self, node, workspace_package, inverse_levels, deps_graph):
         conan_ref, conan_file = node.conan_ref, node.conanfile
diff --git a/conans/client/recorder/action_recorder.py b/conans/client/recorder/action_recorder.py
--- a/conans/client/recorder/action_recorder.py
+++ b/conans/client/recorder/action_recorder.py
@@ -4,7 +4,7 @@
 # about the downloaded files prior to unzip them
 
 from datetime import datetime
-from collections import namedtuple, OrderedDict
+from collections import namedtuple, OrderedDict, defaultdict
 
 # Install actions
 from conans.model.ref import ConanFileReference, PackageReference
@@ -35,6 +35,7 @@ def __init__(self):
         self._inst_recipes_actions = OrderedDict()
         self._inst_packages_actions = OrderedDict()
         self._inst_recipes_develop = set()  # Recipes being created (to set dependency=False)
+        self._inst_packages_info = defaultdict(dict)
 
     # ###### INSTALL METHODS ############
     def add_recipe_being_developed(self, reference):
@@ -80,6 +81,16 @@ def package_install_error(self, reference, error_type, description, remote_name=
         doc = {"type": error_type, "description": description, "remote": remote_name}
         self._inst_packages_actions[reference].append(Action(INSTALL_ERROR, doc))
 
+    def package_cpp_info(self, reference, cpp_info):
+        assert isinstance(reference, PackageReference)
+        # assert isinstance(cpp_info, CppInfo)
+        doc = {}
+        for it, value in vars(cpp_info).items():
+            if it.startswith("_") or not value:
+                continue
+            doc[it] = value
+        self._inst_packages_info[reference]['cpp_info'] = doc
+
     @property
     def install_errored(self):
         all_values = list(self._inst_recipes_actions.values()) + list(self._inst_packages_actions.values())
@@ -119,6 +130,12 @@ def get_doc_for_ref(the_ref, the_action):
                    "time": the_action.time}
             if isinstance(the_ref, ConanFileReference):
                 doc["dependency"] = not self.in_development_recipe(the_ref)
+                doc["name"] = the_ref.name
+                doc["version"] = the_ref.version
+                doc["user"] = the_ref.user
+                doc["channel"] = the_ref.channel
+                if the_ref.revision:
+                    doc["revision"] = the_ref.revision
             else:
                 doc["built"] = the_action.type == INSTALL_BUILT
 
@@ -136,6 +153,8 @@ def get_doc_for_ref(the_ref, the_action):
 
             for p_ref, p_action in packages:
                 p_doc = get_doc_for_ref(p_ref.package_id, p_action)
+                package_data = self._inst_packages_info.get(p_ref, {})
+                p_doc.update(package_data)
                 tmp["packages"].append(p_doc)
 
             ret["installed"].append(tmp)


Test patch:
diff --git a/conans/test/command/json_output_test.py b/conans/test/command/json_output_test.py
--- a/conans/test/command/json_output_test.py
+++ b/conans/test/command/json_output_test.py
@@ -17,7 +17,7 @@ def test_simple_fields(self):
         # Result of a create
         files = cpp_hello_conan_files("CC", "1.0", build=False)
         self.client.save(files, clean_first=True)
-        self.client.run("create . private_user/channel --json=myfile.json ")
+        self.client.run("create . private_user/channel --json=myfile.json")
         my_json = json.loads(load(os.path.join(self.client.current_folder, "myfile.json")))
         self.assertFalse(my_json["error"])
         self.assertEquals(my_json["installed"][0]["recipe"]["id"], "CC/1.0@private_user/channel")
@@ -25,6 +25,7 @@ def test_simple_fields(self):
         self.assertTrue(my_json["installed"][0]["recipe"]["cache"])
         self.assertIsNone(my_json["installed"][0]["recipe"]["remote"])
         self.assertTrue(my_json["installed"][0]["packages"][0]["built"])
+        self.assertTrue(my_json["installed"][0]["packages"][0]["cpp_info"])
 
         # Result of an install retrieving only the recipe
         self.client.run("upload CC/1.0@private_user/channel -c")
@@ -41,6 +42,7 @@ def test_simple_fields(self):
         self.assertTrue(my_json["installed"][0]["recipe"]["downloaded"])
         self.assertIsNotNone(my_json["installed"][0]["recipe"]["remote"])
         self.assertTrue(my_json["installed"][0]["packages"][0]["built"])
+        self.assertTrue(my_json["installed"][0]["packages"][0]["cpp_info"])
 
         # Upload the binary too
         self.client.run("upload CC/1.0@private_user/channel --all -c")
@@ -55,6 +57,7 @@ def test_simple_fields(self):
         self.assertIsNotNone(my_json["installed"][0]["recipe"]["remote"])
         self.assertFalse(my_json["installed"][0]["packages"][0]["built"])
         self.assertTrue(my_json["installed"][0]["packages"][0]["downloaded"])
+        self.assertTrue(my_json["installed"][0]["packages"][0]["cpp_info"])
 
         # Force build
         self.client.run("remove '*' -f")
@@ -68,6 +71,7 @@ def test_simple_fields(self):
         self.assertIsNotNone(my_json["installed"][0]["recipe"]["remote"])
         self.assertTrue(my_json["installed"][0]["packages"][0]["built"])
         self.assertFalse(my_json["installed"][0]["packages"][0]["downloaded"])
+        self.assertTrue(my_json["installed"][0]["packages"][0]["cpp_info"])
 
     def test_errors(self):