Repository: conan-io/conan
Pull request #2654 (2018-03-21 16:39:29)

Feature/install output
- [x] Refer to the issue that supports this Pull Request.  Partially address: #1864 #1895 #1998 
- [ ] I've opened another PR in the Conan docs repo to the ``develop`` branch, documenting this one. Also adding a description of the changes in the ``changelog.rst`` file. https://github.com/conan-io/docs

- Missing docs. 
- Probably missing tests and slow review, @memsharded you could take charge of it if you want/can.
- I think it could be mature enough to be included as an experimental feature in 1.2 but feedback is welcome



Patch:
diff --git a/conans/client/action_recorder.py b/conans/client/action_recorder.py
new file mode 100644
index 000000000..5121978cd
--- /dev/null
+++ b/conans/client/action_recorder.py
@@ -0,0 +1,125 @@
+
+# FIXME: The functions from the tracer.py module should be called here, I removed from there some
+# of them because it has to be called in the remote manager, not in the proxy, where we have info
+# about the downloaded files prior to unzip them
+
+import time
+from collections import namedtuple, OrderedDict
+
+# Install actions
+INSTALL_CACHE = 0
+INSTALL_DOWNLOADED = 1
+INSTALL_BUILT = 2
+INSTALL_ERROR = -1
+
+# Actions errors
+INSTALL_ERROR_MISSING = "missing"
+INSTALL_ERROR_NETWORK = "network"
+INSTALL_ERROR_MISSING_BUILD_FOLDER = "missing_build_folder"
+INSTALL_ERROR_BUILDING = "building"
+
+
+class Action(namedtuple("Action", "type, doc, time")):
+
+    def __new__(cls, the_type, doc=None):
+        doc = doc or {}
+        the_time = time.time()
+        return super(cls, Action).__new__(cls, the_type, doc, the_time)
+
+
+class ActionRecorder(object):
+
+    def __init__(self):
+        self._inst_recipes_actions = OrderedDict()
+        self._inst_packages_actions = OrderedDict()
+
+    # ###### INSTALL METHODS ############
+
+    def _add_recipe_action(self, reference, action):
+        if reference not in self._inst_recipes_actions:
+            self._inst_recipes_actions[reference] = []
+        self._inst_recipes_actions[reference].append(action)
+
+    def _add_package_action(self, reference, action):
+        if reference not in self._inst_packages_actions:
+            self._inst_packages_actions[reference] = []
+        self._inst_packages_actions[reference].append(action)
+
+    # RECIPE METHODS
+    def recipe_fetched_from_cache(self, reference):
+        self._add_recipe_action(reference, Action(INSTALL_CACHE))
+
+    def recipe_downloaded(self, reference, remote):
+        self._add_recipe_action(reference, Action(INSTALL_DOWNLOADED, {"remote": remote}))
+
+    def recipe_install_error(self, reference, error_type, description, remote):
+        doc = {"type": error_type, "description": description, "remote": remote}
+        self._add_recipe_action(reference, Action(INSTALL_ERROR, doc))
+
+    # PACKAGE METHODS
+    def package_built(self, reference):
+        self._add_package_action(reference, Action(INSTALL_BUILT))
+
+    def package_fetched_from_cache(self, reference):
+        self._add_package_action(reference, Action(INSTALL_CACHE))
+
+    def package_downloaded(self, reference, remote):
+        self._add_package_action(reference, Action(INSTALL_DOWNLOADED, {"remote": remote}))
+
+    def package_install_error(self, reference, error_type, description, remote=None):
+        if reference not in self._inst_packages_actions:
+            self._inst_packages_actions[reference] = []
+        doc = {"type": error_type, "description": description, "remote": remote}
+        self._inst_packages_actions[reference].append(Action(INSTALL_ERROR, doc))
+
+    @property
+    def install_errored(self):
+        all_values = list(self._inst_recipes_actions.values()) + list(self._inst_packages_actions.values())
+        for acts in all_values:
+            for act in acts:
+                if act.type == INSTALL_ERROR:
+                    return True
+        return False
+
+    def _get_installed_packages(self, reference):
+        ret = []
+        for _package_ref, _package_actions in self._inst_packages_actions.items():
+            # Could be a download and then an access to cache, we want the first one
+            _package_action = _package_actions[0]
+            if _package_ref.conan == reference:
+                ret.append((_package_ref, _package_action))
+        return ret
+
+    def get_install_info(self):
+        ret = {"error": self.install_errored,
+               "installed": []}
+
+        def get_doc_for_ref(the_ref, the_action):
+            error = None if the_action.type != INSTALL_ERROR else the_action.doc
+            doc = {"id": str(the_ref),
+                   "downloaded": the_action.type == INSTALL_DOWNLOADED,
+                   "built": the_action.type == INSTALL_BUILT,
+                   "cache": the_action.type == INSTALL_CACHE,
+                   "error": error,
+                   "remote": the_action.doc.get("remote", None),
+                   "time": the_action.time}
+            if doc["remote"] is None and error:
+                doc["remote"] = error.get("remote", None)
+            return doc
+
+        for ref, actions in self._inst_recipes_actions.items():
+            # Could be a download and then an access to cache, we want the first one
+            action = actions[0]
+            recipe_doc = get_doc_for_ref(ref, action)
+            del recipe_doc["built"]  # Avoid confusions
+            packages = self._get_installed_packages(ref)
+            tmp = {"recipe": recipe_doc,
+                   "packages": []}
+
+            for p_ref, p_action in packages:
+                p_doc = get_doc_for_ref(p_ref.package_id, p_action)
+                tmp["packages"].append(p_doc)
+
+            ret["installed"].append(tmp)
+
+        return ret
diff --git a/conans/client/build/cmake.py b/conans/client/build/cmake.py
index 832576aeb..a88e150da 100644
--- a/conans/client/build/cmake.py
+++ b/conans/client/build/cmake.py
@@ -169,6 +169,7 @@ class CMake(object):
         if self._cmake_system_name is False:
             return ret
 
+        # System name and system version
         if self._cmake_system_name is not True:  # String not empty
             ret["CMAKE_SYSTEM_NAME"] = self._cmake_system_name
             ret["CMAKE_SYSTEM_VERSION"] = os_ver
@@ -183,9 +184,13 @@ class CMake(object):
                     else:
                         ret["CMAKE_SYSTEM_NAME"] = "Generic"
 
+        # system processor
+        cmake_system_processor = os.getenv("CONAN_CMAKE_SYSTEM_PROCESSOR", None)
+        if cmake_system_processor:
+            ret["CMAKE_SYSTEM_PROCESSOR"] = cmake_system_processor
+
         if ret:  # If enabled cross compile
-            for env_var in ["CONAN_CMAKE_SYSTEM_PROCESSOR",
-                            "CONAN_CMAKE_FIND_ROOT_PATH",
+            for env_var in ["CONAN_CMAKE_FIND_ROOT_PATH",
                             "CONAN_CMAKE_FIND_ROOT_PATH_MODE_PROGRAM",
                             "CONAN_CMAKE_FIND_ROOT_PATH_MODE_LIBRARY",
                             "CONAN_CMAKE_FIND_ROOT_PATH_MODE_INCLUDE"]:
diff --git a/conans/client/cmd/copy.py b/conans/client/cmd/copy.py
index b01745ed4..ed212cfa9 100644
--- a/conans/client/cmd/copy.py
+++ b/conans/client/cmd/copy.py
@@ -7,8 +7,8 @@ from conans.client.loader_parse import load_conanfile_class
 from conans.client.proxy import ConanProxy
 
 
-def _prepare_sources(client_cache, user_io, remote_manager, reference):
-    remote_proxy = ConanProxy(client_cache, user_io, remote_manager, None)
+def _prepare_sources(client_cache, user_io, remote_manager, reference, recorder):
+    remote_proxy = ConanProxy(client_cache, user_io, remote_manager, None, recorder)
     conan_file_path = client_cache.conanfile(reference)
     conanfile = load_conanfile_class(conan_file_path)
     remote_proxy.complete_recipe_sources(conanfile, reference,
@@ -28,14 +28,14 @@ def _get_package_ids(client_cache, reference, package_ids):
     return package_ids
 
 
-def cmd_copy(reference, user_channel, package_ids, client_cache, user_io, remote_manager,
+def cmd_copy(reference, user_channel, package_ids, client_cache, user_io, remote_manager, recorder,
              force=False):
     """
     param package_ids: Falsey=do not copy binaries. True=All existing. []=list of ids
     """
     src_ref = ConanFileReference.loads(reference)
 
-    short_paths = _prepare_sources(client_cache, user_io, remote_manager, src_ref)
+    short_paths = _prepare_sources(client_cache, user_io, remote_manager, src_ref, recorder)
     package_ids = _get_package_ids(client_cache, src_ref, package_ids)
     package_copy(src_ref, user_channel, package_ids, client_cache, user_io,
                  short_paths, force)
diff --git a/conans/client/cmd/uploader.py b/conans/client/cmd/uploader.py
index 25dcfe970..d48cf1d20 100644
--- a/conans/client/cmd/uploader.py
+++ b/conans/client/cmd/uploader.py
@@ -20,10 +20,11 @@ def _is_a_reference(ref):
 
 class CmdUpload(object):
 
-    def __init__(self, client_cache, user_io, remote_manager, remote):
+    def __init__(self, client_cache, user_io, remote_manager, remote, recorder):
         self._client_cache = client_cache
         self._user_io = user_io
-        self._remote_proxy = ConanProxy(self._client_cache, self._user_io, remote_manager, remote)
+        self._remote_proxy = ConanProxy(self._client_cache, self._user_io, remote_manager, remote,
+                                        recorder)
         self._cache_search = DiskSearchManager(self._client_cache)
 
     def upload(self, conan_reference_or_pattern, package_id=None, all_packages=None,
diff --git a/conans/client/command.py b/conans/client/command.py
index fed967f09..3f5333fd2 100644
--- a/conans/client/command.py
+++ b/conans/client/command.py
@@ -199,6 +199,9 @@ class Command(object):
         parser.add_argument('-kb', '--keep-build', default=False, action='store_true',
                             help='Optional. Do not remove the build folder in local cache. '
                                  'Use for testing purposes only')
+        parser.add_argument("-j", "--json", default=None, action=OnceArgument,
+                            help='Path to a json file where the install information '
+                                 'will be written')
 
         _add_manifests_arguments(parser)
         _add_common_install_arguments(parser, build_help=_help_build_policies)
@@ -211,13 +214,19 @@ class Command(object):
             # Now if parameter --test-folder=None (string None) we have to skip tests
             args.test_folder = False
 
-        return self._conan.create(args.path, name, version, user, channel,
-                                  args.profile, args.settings, args.options,
-                                  args.env, args.test_folder, args.not_export,
-                                  args.build, args.keep_source, args.keep_build, args.verify,
-                                  args.manifests, args.manifests_interactive,
-                                  args.remote, args.update,
-                                  test_build_folder=args.test_build_folder)
+        cwd = os.getcwd()
+
+        try:
+            self._conan.create(args.path, name, version, user, channel,
+                               args.profile, args.settings, args.options,
+                               args.env, args.test_folder, args.not_export,
+                               args.build, args.keep_source, args.keep_build, args.verify,
+                               args.manifests, args.manifests_interactive,
+                               args.remote, args.update,
+                               test_build_folder=args.test_build_folder)
+        finally:
+            if args.json:
+                self._outputer.json_install(self._conan.recorder.get_install_info(), args.json, cwd)
 
     def download(self, *args):
         """Downloads recipe and binaries to the local cache, without using settings.
@@ -270,35 +279,43 @@ class Command(object):
 
         parser.add_argument("--no-imports", action='store_true', default=False,
                             help='Install specified packages but avoid running imports')
+        parser.add_argument("-j", "--json", default=None, action=OnceArgument,
+                            help='Path to a json file where the install information '
+                                 'will be written')
 
         _add_common_install_arguments(parser, build_help=_help_build_policies)
 
         args = parser.parse_args(*args)
+        cwd = os.getcwd()
 
         try:
-            reference = ConanFileReference.loads(args.path)
-        except ConanException:
-            return self._conan.install(path=args.path,
-                                       settings=args.settings, options=args.options,
-                                       env=args.env,
-                                       remote=args.remote,
-                                       verify=args.verify, manifests=args.manifests,
-                                       manifests_interactive=args.manifests_interactive,
-                                       build=args.build, profile_name=args.profile,
-                                       update=args.update, generators=args.generator,
-                                       no_imports=args.no_imports,
-                                       install_folder=args.install_folder)
-        else:
-            return self._conan.install_reference(reference, settings=args.settings,
-                                                 options=args.options,
-                                                 env=args.env,
-                                                 remote=args.remote,
-                                                 verify=args.verify, manifests=args.manifests,
-                                                 manifests_interactive=args.manifests_interactive,
-                                                 build=args.build, profile_name=args.profile,
-                                                 update=args.update,
-                                                 generators=args.generator,
-                                                 install_folder=args.install_folder)
+            try:
+                reference = ConanFileReference.loads(args.path)
+            except ConanException:
+                self._conan.install(path=args.path,
+                                    settings=args.settings, options=args.options,
+                                    env=args.env,
+                                    remote=args.remote,
+                                    verify=args.verify, manifests=args.manifests,
+                                    manifests_interactive=args.manifests_interactive,
+                                    build=args.build, profile_name=args.profile,
+                                    update=args.update, generators=args.generator,
+                                    no_imports=args.no_imports,
+                                    install_folder=args.install_folder)
+            else:
+                self._conan.install_reference(reference, settings=args.settings,
+                                              options=args.options,
+                                              env=args.env,
+                                              remote=args.remote,
+                                              verify=args.verify, manifests=args.manifests,
+                                              manifests_interactive=args.manifests_interactive,
+                                              build=args.build, profile_name=args.profile,
+                                              update=args.update,
+                                              generators=args.generator,
+                                              install_folder=args.install_folder)
+        finally:
+            if args.json:
+                self._outputer.json_install(self._conan.recorder.get_install_info(), args.json, cwd)
 
     def config(self, *args):
         """Manages configuration. Edits the conan.conf or installs config files.
diff --git a/conans/client/conan_api.py b/conans/client/conan_api.py
index 89e6b1138..555a1f84b 100644
--- a/conans/client/conan_api.py
+++ b/conans/client/conan_api.py
@@ -5,6 +5,7 @@ import requests
 
 import conans
 from conans import __version__ as client_version, tools
+from conans.client.action_recorder import ActionRecorder
 from conans.client.client_cache import ClientCache
 from conans.client.conf import MIN_SERVER_COMPATIBLE_VERSION, ConanClientConfigParser
 from conans.client.manager import ConanManager, existing_info_files
@@ -187,11 +188,13 @@ class ConanAPIV1(object):
         self._user_io = user_io
         self._runner = runner
         self._remote_manager = remote_manager
+        self.recorder = ActionRecorder()
         self._manager = ConanManager(client_cache, user_io, runner, remote_manager, search_manager,
-                                     _settings_preprocessor)
+                                     _settings_preprocessor, self.recorder)
         if not interactive:
             self._user_io.disable_input()
 
+
     @api_method
     def new(self, name, header=False, pure_c=False, test=False, exports_sources=False, bare=False,
             cwd=None, visual_versions=None, linux_gcc_versions=None, linux_clang_versions=None,
@@ -319,6 +322,8 @@ class ConanAPIV1(object):
                                   update=update,
                                   keep_build=keep_build)
 
+
+
     @api_method
     def export_pkg(self, conanfile_path, name, channel, source_folder=None, build_folder=None,
                    package_folder=None, install_folder=None, profile_name=None, settings=None,
@@ -591,7 +596,7 @@ class ConanAPIV1(object):
         from conans.client.cmd.copy import cmd_copy
         # FIXME: conan copy does not support short-paths in Windows
         cmd_copy(reference, user_channel, packages, self._client_cache,
-                 self._user_io, self._remote_manager, force=force)
+                 self._user_io, self._remote_manager, self.recorder, force=force)
 
     @api_method
     def authenticate(self, name, password, remote=None):
@@ -640,7 +645,7 @@ class ConanAPIV1(object):
         if force and no_overwrite:
             raise ConanException("'no_overwrite' argument cannot be used together with 'force'")
 
-        uploader = CmdUpload(self._client_cache, self._user_io, self._remote_manager, remote)
+        uploader = CmdUpload(self._client_cache, self._user_io, self._remote_manager, remote, self.recorder)
         return uploader.upload(pattern, package, all_packages, force, confirm, retry, retry_wait,
                                skip_upload, integrity_check, no_overwrite)
 
diff --git a/conans/client/conan_command_output.py b/conans/client/conan_command_output.py
index a30a49e24..ada98845c 100644
--- a/conans/client/conan_command_output.py
+++ b/conans/client/conan_command_output.py
@@ -46,6 +46,13 @@ class CommandOutputer(object):
                 json_output = os.path.join(cwd, json_output)
             save(json_output, json_str)
 
+    def json_install(self, info, json_output, cwd):
+        cwd = os.path.abspath(cwd or os.getcwd())
+        if not os.path.isabs(json_output):
+            json_output = os.path.join(cwd, json_output)
+        save(json_output, json.dumps(info))
+        self.user_io.out.info("json file created at '%s'" % json_output)
+
     def _read_dates(self, deps_graph):
         ret = {}
         for ref, _ in sorted(deps_graph.nodes):
diff --git a/conans/client/installer.py b/conans/client/installer.py
index 668ebd284..5aca5d673 100644
--- a/conans/client/installer.py
+++ b/conans/client/installer.py
@@ -4,6 +4,8 @@ import shutil
 import platform
 
 from conans.client import tools
+from conans.client.action_recorder import INSTALL_ERROR_MISSING_BUILD_FOLDER, INSTALL_ERROR_BUILDING, \
+    INSTALL_ERROR_MISSING
 from conans.model.conan_file import get_env_context_manager
 from conans.model.env_info import EnvInfo
 from conans.model.user_info import UserInfo
@@ -19,9 +21,10 @@ from conans.client.generators import write_generators, TXTGenerator
 from conans.model.build_info import CppInfo
 from conans.client.output import ScopedOutput
 from conans.client.source import config_source
-from conans.util.tracer import log_package_built
 from conans.util.env_reader import get_env
 from conans.client.importer import remove_imports
+
+from conans.util.tracer import log_package_built
 from conans.client.tools.env import pythonpath
 
 
@@ -175,16 +178,18 @@ class _ConanPackageBuilder(object):
             remove_imports(self._conan_file, copied_files, self._out)
 
 
-def _raise_package_not_found_error(conan_file, conan_ref, package_id, out):
+def _raise_package_not_found_error(conan_file, conan_ref, package_id, out, recorder):
     settings_text = ", ".join(conan_file.info.full_settings.dumps().splitlines())
     options_text = ", ".join(conan_file.info.full_options.dumps().splitlines())
 
-    out.warn('''Can't find a '%s' package for the specified options and settings:
+    msg = '''Can't find a '%s' package for the specified options and settings:
 - Settings: %s
 - Options: %s
 - Package ID: %s
-''' % (conan_ref, settings_text, options_text, package_id))
-
+''' % (conan_ref, settings_text, options_text, package_id)
+    out.warn(msg)
+    recorder.package_install_error(PackageReference(conan_ref, package_id),
+                                   INSTALL_ERROR_MISSING, msg, remote=None)
     raise ConanException('''Missing prebuilt package for '%s'
 Try to build it from sources with "--build %s"
 Or read "http://docs.conan.io/en/latest/faq/troubleshooting.html#error-missing-prebuilt-package"
@@ -230,18 +235,20 @@ class ConanInstaller(object):
     """ main responsible of retrieving binary packages or building them from source
     locally in case they are not found in remotes
     """
-    def __init__(self, client_cache, output, remote_proxy, build_mode, build_requires):
+    def __init__(self, client_cache, output, remote_proxy, build_mode, build_requires, recorder):
         self._client_cache = client_cache
         self._out = output
         self._remote_proxy = remote_proxy
         self._build_requires = build_requires
         self._build_mode = build_mode
         self._built_packages = set()  # To avoid re-building twice the same package reference
+        self._recorder = recorder
 
     def install(self, deps_graph, profile_build_requires, keep_build=False):
         """ given a DepsGraph object, build necessary nodes or retrieve them
         """
         t1 = time.time()
+
         # order by levels and separate the root node (conan_ref=None) from the rest
         nodes_by_level = deps_graph.by_levels()
         root_level = nodes_by_level.pop()
@@ -337,7 +344,7 @@ class ConanInstaller(object):
                        profile_build_requires, flat, deps_graph):
         build_allowed = self._build_mode.allowed(conan_file, conan_ref)
         if not build_allowed:
-            _raise_package_not_found_error(conan_file, conan_ref, package_id, output)
+            _raise_package_not_found_error(conan_file, conan_ref, package_id, output, self._recorder)
 
         skip_build = conan_file.develop and keep_build
         if skip_build:
@@ -360,7 +367,11 @@ class ConanInstaller(object):
 
         if skip_build:
             if not os.path.exists(builder.build_folder):
-                raise ConanException("--keep-build specified, but build folder not found")
+                msg = "--keep-build specified, but build folder not found"
+                self._recorder.package_install_error(package_ref,
+                                                     INSTALL_ERROR_MISSING_BUILD_FOLDER,
+                                                     msg, remote=None)
+                raise ConanException(msg)
         else:
             with self._client_cache.conanfile_write_lock(conan_ref):
                 self._remote_proxy.get_recipe_sources(conan_ref, conan_file.short_paths)
@@ -368,15 +379,20 @@ class ConanInstaller(object):
 
         with self._client_cache.conanfile_read_lock(conan_ref):
             with self._client_cache.package_lock(builder.build_reference):
-                if not skip_build:
-                    builder.build()
-                builder.package()
-
-                self._remote_proxy.handle_package_manifest(package_ref, installed=True)
-
-                # Log build
-                self._log_built_package(builder.build_folder, package_ref, time.time() - t1)
-                self._built_packages.add((conan_ref, package_id))
+                try:
+                    if not skip_build:
+                        builder.build()
+                    builder.package()
+                except ConanException as exc:
+                    self._recorder.package_install_error(package_ref, INSTALL_ERROR_BUILDING,
+                                                         str(exc), remote=None)
+                    raise exc
+                else:
+                    self._remote_proxy.handle_package_manifest(package_ref, installed=True)
+
+                    # Log build
+                    self._log_built_package(builder.build_folder, package_ref, time.time() - t1)
+                    self._built_packages.add((conan_ref, package_id))
 
     def _get_remote_package(self, conan_file, package_reference, output, package_folder):
         """Get remote package. It won't check if it's outdated"""
@@ -414,6 +430,7 @@ class ConanInstaller(object):
         log_file = os.path.join(build_folder, RUN_LOG_NAME)
         log_file = log_file if os.path.exists(log_file) else None
         log_package_built(package_ref, duration, log_file)
+        self._recorder.package_built(package_ref)
 
     @staticmethod
     def _propagate_info(conan_file, conan_ref, flat, deps_graph):
diff --git a/conans/client/manager.py b/conans/client/manager.py
index 775d40dc0..e8fd7e622 100644
--- a/conans/client/manager.py
+++ b/conans/client/manager.py
@@ -31,7 +31,6 @@ from conans.paths import CONANFILE, CONANINFO, CONANFILE_TXT, CONAN_MANIFEST, BU
 from conans.util.files import save, rmdir, normalize, mkdir, load
 from conans.util.log import logger
 from conans.client.loader_parse import load_conanfile_class
-from conans.client.store.localdb import LocalDB
 
 
 class BuildMode(object):
@@ -106,7 +105,7 @@ class ConanManager(object):
     business logic
     """
     def __init__(self, client_cache, user_io, runner, remote_manager, search_manager,
-                 settings_preprocessor):
+                 settings_preprocessor, recorder):
         assert isinstance(user_io, UserIO)
         assert isinstance(client_cache, ClientCache)
         self._client_cache = client_cache
@@ -115,6 +114,7 @@ class ConanManager(object):
         self._remote_manager = remote_manager
         self._search_manager = search_manager
         self._settings_preprocessor = settings_preprocessor
+        self._recorder = recorder
 
     def _load_consumer_conanfile(self, conanfile_path, info_folder, output,
                                  deps_info_required=False):
@@ -170,7 +170,7 @@ class ConanManager(object):
 
         remote_proxy = ConanProxy(self._client_cache, self._user_io, self._remote_manager,
                                   remote_name=None, update=False, check_updates=False,
-                                  manifest_manager=None)
+                                  manifest_manager=None, recorder=self._recorder)
 
         loader = self.get_loader(profile)
         conanfile = loader.load_virtual([reference], scope_options=True)
@@ -215,7 +215,8 @@ class ConanManager(object):
         @param only_recipe: download only the recipe
         """
         assert(isinstance(reference, ConanFileReference))
-        remote_proxy = ConanProxy(self._client_cache, self._user_io, self._remote_manager, remote)
+        remote_proxy = ConanProxy(self._client_cache, self._user_io, self._remote_manager, remote,
+                                  recorder=self._recorder)
         remote, _ = remote_proxy._get_remote()
         package = self._remote_manager.search_recipes(remote, reference, None)
         if not package:  # Search the reference first, and raise if it doesn't exist
@@ -273,18 +274,20 @@ class ConanManager(object):
 
     def info_build_order(self, reference, profile, build_order, remote, check_updates):
         remote_proxy = ConanProxy(self._client_cache, self._user_io, self._remote_manager, remote,
-                                  update=False, check_updates=check_updates)
+                                  update=False, check_updates=check_updates,
+                                  recorder=self._recorder)
         deps_graph, _, _ = self._get_deps_graph(reference, profile, remote_proxy)
         result = deps_graph.build_order(build_order)
         return result
 
     def info_nodes_to_build(self, reference, profile, build_modes, remote, check_updates):
         remote_proxy = ConanProxy(self._client_cache, self._user_io, self._remote_manager, remote,
-                                  update=False, check_updates=check_updates)
+                                  update=False, check_updates=check_updates,
+                                  recorder=self._recorder)
         deps_graph, _, conanfile = self._get_deps_graph(reference, profile, remote_proxy)
         build_mode = BuildMode(build_modes, self._user_io.out)
         installer = ConanInstaller(self._client_cache, self._user_io.out, remote_proxy, build_mode,
-                                   None)
+                                   None, recorder=self._recorder)
         nodes = installer.nodes_to_build(deps_graph)
         counter = Counter(ref.conan.name for ref, _ in nodes)
         ret = [ref if counter[ref.conan.name] > 1 else str(ref.conan) for ref, _ in nodes]
@@ -304,7 +307,8 @@ class ConanManager(object):
         @param remote: install only from that remote
         """
         remote_proxy = ConanProxy(self._client_cache, self._user_io, self._remote_manager, remote,
-                                  update=False, check_updates=check_updates)
+                                  update=False, check_updates=check_updates,
+                                  recorder=self._recorder)
 
         deps_graph, graph_builder, conanfile = self._get_deps_graph(reference, profile, remote_proxy)
 
@@ -335,6 +339,7 @@ class ConanManager(object):
         @param no_imports: Install specified packages but avoid running imports
         @param inject_require: Reference to add as a requirement to the conanfile
         """
+
         if generators is not False:
             generators = set(generators) if generators else set()
             generators.add("txt")  # Add txt generator by default
@@ -344,7 +349,8 @@ class ConanManager(object):
                                            verify=manifest_verify,
                                            interactive=manifest_interactive) if manifest_folder else None
         remote_proxy = ConanProxy(self._client_cache, self._user_io, self._remote_manager, remote,
-                                  update=update, manifest_manager=manifest_manager)
+                                  update=update, manifest_manager=manifest_manager,
+                                  recorder=self._recorder)
 
         loader = self.get_loader(profile)
         if not install_reference:
@@ -380,7 +386,7 @@ class ConanManager(object):
         build_mode = BuildMode(build_modes, self._user_io.out)
         build_requires = BuildRequires(loader, graph_builder, registry)
         installer = ConanInstaller(self._client_cache, output, remote_proxy, build_mode,
-                                   build_requires)
+                                   build_requires, recorder=self._recorder)
 
         # Apply build_requires to consumer conanfile
         if not isinstance(reference, ConanFileReference):
@@ -524,13 +530,15 @@ class ConanManager(object):
         @param force: if True, it will be deleted without requesting anything
         @param packages_query: Only if src is a reference. Query settings and options
         """
-        remote_proxy = ConanProxy(self._client_cache, self._user_io, self._remote_manager, remote)
+        remote_proxy = ConanProxy(self._client_cache, self._user_io, self._remote_manager, remote,
+                                  recorder=self._recorder)
         remover = ConanRemover(self._client_cache, self._remote_manager, self._user_io, remote_proxy)
         remover.remove(pattern, remote, src, build_ids, package_ids_filter, force=force,
                        packages_query=packages_query, outdated=outdated)
 
     def get_path(self, reference, package_id=None, path=None, remote=None):
-        remote_proxy = ConanProxy(self._client_cache, self._user_io, self._remote_manager, remote)
+        remote_proxy = ConanProxy(self._client_cache, self._user_io, self._remote_manager, remote,
+                                  recorder=self._recorder)
         if not path and not package_id:
             path = "conanfile.py"
         elif not path and package_id:
diff --git a/conans/client/proxy.py b/conans/client/proxy.py
index 36c8d8645..9c7c9c987 100644
--- a/conans/client/proxy.py
+++ b/conans/client/proxy.py
@@ -1,17 +1,19 @@
 import os
 
+from requests.exceptions import RequestException
+
 from conans.client.loader_parse import load_conanfile_class
 from conans.client.local_file_getter import get_path
 from conans.client.output import ScopedOutput
 from conans.client.remote_registry import RemoteRegistry
 from conans.client.remover import DiskRemover
+from conans.client.action_recorder import INSTALL_ERROR_MISSING, INSTALL_ERROR_NETWORK
 from conans.errors import (ConanException, NotFoundException, NoRemoteAvailable)
 from conans.model.ref import PackageReference
 from conans.paths import EXPORT_SOURCES_TGZ_NAME
 from conans.util.files import rmdir, mkdir
 from conans.util.log import logger
-from conans.util.tracer import log_package_got_from_local_cache,\
-    log_recipe_got_from_local_cache
+from conans.util.tracer import log_recipe_got_from_local_cache, log_package_got_from_local_cache
 
 
 class ConanProxy(object):
@@ -19,7 +21,7 @@ class ConanProxy(object):
     getting conanfiles, uploading, removing from remote, etc.
     It uses the RemoteRegistry to control where the packages come from.
     """
-    def __init__(self, client_cache, user_io, remote_manager, remote_name,
+    def __init__(self, client_cache, user_io, remote_manager, remote_name, recorder,
                  update=False, check_updates=False, manifest_manager=False):
         self._client_cache = client_cache
         self._out = user_io.out
@@ -29,6 +31,7 @@ class ConanProxy(object):
         self._update = update
         self._check_updates = check_updates or update  # Update forces check (and of course the update)
         self._manifest_manager = manifest_manager
+        self._recorder = recorder
 
     @property
     def registry(self):
@@ -96,6 +99,7 @@ class ConanProxy(object):
             output.success('Already installed!')
             installed = True
             log_package_got_from_local_cache(package_ref)
+            self._recorder.package_fetched_from_cache(package_ref)
         else:
             installed = self._retrieve_remote_package(package_ref, package_folder,
                                                       output)
@@ -134,6 +138,7 @@ class ConanProxy(object):
 
         if os.path.exists(conanfile_path):
             log_recipe_got_from_local_cache(conan_reference)
+            self._recorder.recipe_fetched_from_cache(conan_reference)
             if self._check_updates:
                 ret = self.update_available(conan_reference)
                 if ret != 0:  # Found and not equal
@@ -196,12 +201,12 @@ class ConanProxy(object):
         """ returns the requested conanfile object, retrieving it from
         remotes if necessary. Can raise NotFoundException
         """
-        def _retrieve_from_remote(remote):
-            output.info("Trying with '%s'..." % remote.name)
+        def _retrieve_from_remote(the_remote):
+            output.info("Trying with '%s'..." % the_remote.name)
             export_path = self._client_cache.export(conan_reference)
-            result = self._remote_manager.get_recipe(conan_reference, export_path, remote)
-            self._registry.set_ref(conan_reference, remote)
-            return result
+            self._remote_manager.get_recipe(conan_reference, export_path, the_remote)
+            self._registry.set_ref(conan_reference, the_remote)
+            self._recorder.recipe_downloaded(conan_reference, the_remote.url)
 
         if self._remote_name:
             output.info("Not found, retrieving from server '%s' " % self._remote_name)
@@ -213,23 +218,33 @@ class ConanProxy(object):
 
         if ref_remote:
             try:
-                return _retrieve_from_remote(ref_remote)
+                _retrieve_from_remote(ref_remote)
+                return
             except NotFoundException:
-                raise NotFoundException("%s was not found in remote '%s'" % (str(conan_reference),
-                                                                             ref_remote.name))
+                msg = "%s was not found in remote '%s'" % (str(conan_reference), ref_remote.name)
+                self._recorder.recipe_install_error(conan_reference, INSTALL_ERROR_MISSING,
+                                                    msg, ref_remote.url)
+                raise NotFoundException(msg)
+            except RequestException as exc:
+                self._recorder.recipe_install_error(conan_reference, INSTALL_ERROR_NETWORK,
+                                                    str(exc), ref_remote.url)
+                raise exc
 
         output.info("Not found in local cache, looking in remotes...")
         remotes = self._registry.remotes
         for remote in remotes:
             logger.debug("Trying with remote %s" % remote.name)
             try:
-                return _retrieve_from_remote(remote)
+                _retrieve_from_remote(remote)
+                return
             # If not found continue with the next, else raise
             except NotFoundException as exc:
                 if remote == remotes[-1]:  # Last element not found
+                    msg = "Unable to find '%s' in remotes" % str(conan_reference)
                     logger.debug("Not found in any remote, raising...%s" % exc)
-                    raise NotFoundException("Unable to find '%s' in remotes"
-                                            % str(conan_reference))
+                    self._recorder.recipe_install_error(conan_reference, INSTALL_ERROR_MISSING,
+                                                            msg, None)
+                    raise NotFoundException(msg)
 
         raise ConanException("No remote defined")
 
@@ -392,7 +407,12 @@ class ConanProxy(object):
             # Will raise if not found NotFoundException
             self._remote_manager.get_package(package_ref, package_folder, remote)
             output.success('Package installed %s' % package_id)
+            self._recorder.package_downloaded(package_ref, remote.url)
+
             return True
         except NotFoundException as e:
-            output.warn('Binary for %s not in remote: %s' % (package_id, str(e)))
+            msg = 'Binary for %s not in remote: %s' % (package_id, str(e))
+            output.warn(msg)
+            self._recorder.package_install_error(package_ref, INSTALL_ERROR_MISSING, msg,
+                                                 remote.url)
             return False
diff --git a/conans/client/remote_manager.py b/conans/client/remote_manager.py
index 666b1ee59..53d271a81 100644
--- a/conans/client/remote_manager.py
+++ b/conans/client/remote_manager.py
@@ -14,8 +14,11 @@ from conans.util.files import gzopen_without_timestamps
 from conans.util.files import tar_extract, rmdir, exception_message_safe, mkdir
 from conans.util.files import touch
 from conans.util.log import logger
-from conans.util.tracer import log_package_upload, log_recipe_upload,\
-    log_recipe_download, log_package_download, log_recipe_sources_download, log_uncompressed_file, log_compressed_files
+# FIXME: Eventually, when all output is done, tracer functions should be moved to the recorder class
+from conans.util.tracer import (log_package_upload, log_recipe_upload,
+                                log_recipe_sources_download,
+                                log_uncompressed_file, log_compressed_files, log_recipe_download,
+                                log_package_download)
 from conans.client.source import merge_directories
 
 


Test patch:
diff --git a/conans/test/build_helpers/cmake_test.py b/conans/test/build_helpers/cmake_test.py
index 04a91b478..63ee5fc7b 100644
--- a/conans/test/build_helpers/cmake_test.py
+++ b/conans/test/build_helpers/cmake_test.py
@@ -394,10 +394,12 @@ build_type: [ Release]
         if platform.system() == "Windows":
             self.assertNotIn("-DCMAKE_SYSROOT=", cmake.flags)
 
-        # Now activate cross build and check sysroot
-        with(tools.environment_append({"CONAN_CMAKE_SYSTEM_NAME": "Android"})):
+        # Now activate cross build and check sysroot and system processor
+        with(tools.environment_append({"CONAN_CMAKE_SYSTEM_NAME": "Android",
+                                       "CONAN_CMAKE_SYSTEM_PROCESSOR": "somevalue"})):
             cmake = CMake(conan_file)
             self.assertEquals(cmake.definitions["CMAKE_SYSROOT"], "/path/to/sysroot")
+            self.assertEquals(cmake.definitions["CMAKE_SYSTEM_PROCESSOR"], "somevalue")
 
     def test_deprecated_behaviour(self):
         """"Remove when deprecate the old settings parameter to CMake and
diff --git a/conans/test/command/json_output_test.py b/conans/test/command/json_output_test.py
new file mode 100644
index 000000000..77965cfe8
--- /dev/null
+++ b/conans/test/command/json_output_test.py
@@ -0,0 +1,168 @@
+import json
+import os
+import unittest
+
+from conans.test.utils.cpp_test_files import cpp_hello_conan_files
+from conans.test.utils.tools import TestServer, TestClient
+from conans.util.files import save, load
+
+
+class JsonOutputTest(unittest.TestCase):
+
+    def setUp(self):
+        self.servers = {"default": TestServer()}
+        self.client = TestClient(servers=self.servers)
+
+    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 ")
+        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")
+        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"])
+
+        # Result of an install retrieving only the recipe
+        self.client.run("upload CC/1.0@private_user/channel -c")
+        self.client.run("remove '*' -f")
+        self.client.run("install CC/1.0@private_user/channel --json=myfile.json --build missing ")
+        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")
+        self.assertFalse(my_json["installed"][0]["recipe"]["cache"])
+        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"])
+
+        # Upload the binary too
+        self.client.run("upload CC/1.0@private_user/channel --all -c")
+        self.client.run("remove '*' -f")
+        self.client.run("install CC/1.0@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")
+        self.assertFalse(my_json["installed"][0]["recipe"]["cache"])
+        self.assertTrue(my_json["installed"][0]["recipe"]["downloaded"])
+        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"])
+
+        # Force build
+        self.client.run("remove '*' -f")
+        self.client.run("install CC/1.0@private_user/channel --json=myfile.json --build")
+        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")
+        self.assertFalse(my_json["installed"][0]["recipe"]["cache"])
+        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.assertFalse(my_json["installed"][0]["packages"][0]["downloaded"])
+
+    def test_errors(self):
+
+        # Missing recipe
+        error = self.client.run("install CC/1.0@private_user/channel --json=myfile.json",
+                                ignore_error=True)
+        self.assertTrue(error)
+        my_json = json.loads(load(os.path.join(self.client.current_folder, "myfile.json")))
+        self.assertTrue(my_json["error"])
+        self.assertEquals(len(my_json["installed"]), 1)
+        self.assertFalse(my_json["installed"][0]["recipe"]["cache"])
+        self.assertFalse(my_json["installed"][0]["recipe"]["downloaded"])
+        self.assertEquals(my_json["installed"][0]["recipe"]["error"],
+                          {'type': 'missing', 'remote': None,
+                          'description': "Unable to find 'CC/1.0@private_user/channel' in remotes"})
+
+        # Missing binary package
+        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("upload CC/1.0@private_user/channel -c")
+        self.client.run("remove '*' -f")
+        error = self.client.run("install CC/1.0@private_user/channel --json=myfile.json",
+                                ignore_error=True)
+        my_json = json.loads(load(os.path.join(self.client.current_folder, "myfile.json")))
+
+        self.assertTrue(error)
+        self.assertTrue(my_json["error"])
+        self.assertEquals(len(my_json["installed"]), 1)
+        self.assertFalse(my_json["installed"][0]["recipe"]["cache"])
+        self.assertTrue(my_json["installed"][0]["recipe"]["downloaded"])
+        self.assertFalse(my_json["installed"][0]["recipe"]["error"])
+        self.assertEquals(len(my_json["installed"][0]["packages"]), 1)
+        self.assertFalse(my_json["installed"][0]["packages"][0]["cache"])
+        self.assertFalse(my_json["installed"][0]["packages"][0]["downloaded"])
+        self.assertEquals(my_json["installed"][0]["packages"][0]["error"]["type"], "missing")
+        self.assertIsNone(my_json["installed"][0]["packages"][0]["error"]["remote"])
+        self.assertIn("Can't find a 'CC/1.0@private_user/channel' package",
+                      my_json["installed"][0]["packages"][0]["error"]["description"])
+
+        # Error building
+        files["conanfile.py"] = files["conanfile.py"].replace("def build2(self):",
+                                                              """
+    def build(self):
+        raise Exception("Build error!")
+        """)
+
+        self.client.save(files, clean_first=True)
+        self.client.run("create . private_user/channel --json=myfile.json ", ignore_error=True)
+        my_json = json.loads(load(os.path.join(self.client.current_folder, "myfile.json")))
+        self.assertTrue(my_json["error"])
+        self.assertEquals(my_json["installed"][0]["packages"][0]["error"]["type"], "building")
+        self.assertIsNone(my_json["installed"][0]["packages"][0]["error"]["remote"])
+        self.assertIn("CC/1.0@private_user/channel: Error in build() method, line 36",
+                      my_json["installed"][0]["packages"][0]["error"]["description"])
+
+    def test_json_generation(self):
+
+        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('upload "*" -c --all')
+
+        files = cpp_hello_conan_files("BB", "1.0", build=False)
+        files["conanfile.py"] += """
+    def configure(self):
+        self.options["CC"].static = False
+
+    def build_requirements(self):
+        self.build_requires("CC/1.0@private_user/channel")
+
+"""
+        self.client.save(files, clean_first=True)
+        self.client.run("create . private_user/channel --build missing")
+        self.client.run('upload "*" -c --all')
+
+        files = cpp_hello_conan_files("AA", "1.0",
+                                      deps=["BB/1.0@private_user/channel"], build=False)
+        self.client.save(files, clean_first=True)
+        self.client.run("create . private_user/channel")
+        self.client.run('upload "*" -c --all')
+
+        save(os.path.join(self.client.client_cache.profiles_path, "mybr"),
+             """
+include(default)
+[build_requires]
+AA*: CC/1.0@private_user/channel
+""")
+        files = cpp_hello_conan_files("PROJECT", "1.0",
+                                      deps=["AA/1.0@private_user/channel"], build=False)
+        self.client.save(files, clean_first=True)
+        self.client.run("install . --profile mybr --json=myfile.json --build AA --build BB")
+        my_json = load(os.path.join(self.client.current_folder, "myfile.json"))
+        my_json = json.loads(my_json)
+
+        # Installed the build require CC with two options
+        self.assertEquals(len(my_json["installed"][2]["packages"]), 2)
+        self.assertEquals(my_json["installed"][2]["recipe"]["id"], "CC/1.0@private_user/channel")
+        self.assertTrue(my_json["installed"][2]["recipe"]["cache"])
+        self.assertTrue(my_json["installed"][2]["packages"][0]["cache"])
+        self.assertTrue(my_json["installed"][2]["packages"][1]["cache"])
diff --git a/conans/test/download_test.py b/conans/test/download_test.py
index 314127b90..7177d31f7 100644
--- a/conans/test/download_test.py
+++ b/conans/test/download_test.py
@@ -1,6 +1,7 @@
 import os
 import unittest
 
+from conans.client.action_recorder import ActionRecorder
 from conans.client.manager import CONANFILE
 from conans.client.proxy import ConanProxy
 from conans.errors import NotFoundException, ConanException
@@ -50,7 +51,8 @@ class DownloadTest(unittest.TestCase):
         client2 = TestClient(servers=servers, requester_class=BuggyRequester)
         conan_ref = ConanFileReference.loads("Hello/1.2.1@frodo/stable")
         package_ref = PackageReference(conan_ref, "123123123")
-        installer = ConanProxy(client2.paths, client2.user_io, client2.remote_manager, "default")
+        installer = ConanProxy(client2.paths, client2.user_io, client2.remote_manager,
+                               "default", recorder=ActionRecorder())
 
         with self.assertRaises(NotFoundException):
             installer.get_recipe(conan_ref)
@@ -62,7 +64,8 @@ class DownloadTest(unittest.TestCase):
                 return Response(False, 500)
 
         client2 = TestClient(servers=servers, requester_class=BuggyRequester2)
-        installer = ConanProxy(client2.paths, client2.user_io, client2.remote_manager, "default")
+        installer = ConanProxy(client2.paths, client2.user_io, client2.remote_manager, "default",
+                               recorder=ActionRecorder())
 
         try:
             installer.get_recipe(conan_ref)
@@ -122,7 +125,8 @@ class DownloadTest(unittest.TestCase):
         client2 = TestClient(servers=servers)
         client2.init_dynamic_vars()
 
-        installer = ConanProxy(client2.paths, client2.user_io, client2.remote_manager, "default")
+        installer = ConanProxy(client2.paths, client2.user_io, client2.remote_manager, "default",
+                               recorder=ActionRecorder())
 
         installer.get_recipe(conan_ref)
         installer.get_package(package_ref, short_paths=False)
diff --git a/conans/test/util/action_recorder_test.py b/conans/test/util/action_recorder_test.py
new file mode 100644
index 000000000..0a93a1570
--- /dev/null
+++ b/conans/test/util/action_recorder_test.py
@@ -0,0 +1,92 @@
+import unittest
+
+from conans.client.action_recorder import (ActionRecorder, INSTALL_ERROR_MISSING,
+                                           INSTALL_ERROR_NETWORK)
+from conans.model.ref import ConanFileReference, PackageReference
+
+
+class ActionRecorderTest(unittest.TestCase):
+
+    def setUp(self):
+        self.ref1 = ConanFileReference.loads("lib1/1.0@conan/stable")
+        self.ref2 = ConanFileReference.loads("lib2/1.0@conan/stable")
+        self.ref3 = ConanFileReference.loads("lib3/1.0@conan/stable")
+
+        self.ref_p1 = PackageReference(self.ref1, "1")
+        self.ref_p2 = PackageReference(self.ref2, "2")
+        self.ref_p3 = PackageReference(self.ref3, "3")
+
+    def incomplete_process_test(self):
+        tracer = ActionRecorder()
+        tracer.recipe_install_error(self.ref1, INSTALL_ERROR_NETWORK, "SSL wtf", "http://drl.com")
+        install_info = tracer.get_install_info()
+        self.assertTrue(install_info["error"])
+        self.assertEquals(install_info["installed"][0]["packages"], [])
+
+    def double_actions_test(self):
+        tracer = ActionRecorder()
+        tracer.recipe_downloaded(self.ref1, "http://drl.com")
+        tracer.recipe_fetched_from_cache(self.ref1)
+        tracer.package_downloaded(self.ref_p1, "http://drl.com")
+        tracer.package_fetched_from_cache(self.ref_p1)
+
+        install_info = tracer.get_install_info()
+        self.assertFalse(install_info["error"])
+
+        first_installed = install_info["installed"][0]
+        self.assertFalse(first_installed["recipe"]["cache"])
+        self.assertTrue(first_installed["recipe"]["downloaded"])
+        self.assertIsNone(first_installed["recipe"]["error"])
+        self.assertEquals(str(first_installed["recipe"]["id"]), "lib1/1.0@conan/stable")
+
+        self.assertFalse(first_installed["packages"][0]["cache"])
+        self.assertTrue(first_installed["packages"][0]["downloaded"])
+        self.assertIsNone(first_installed["packages"][0]["error"])
+        self.assertEquals(first_installed["packages"][0]["remote"], 'http://drl.com')
+        self.assertEquals(str(first_installed["packages"][0]["id"]), "1")
+
+    def test_install(self):
+        tracer = ActionRecorder()
+        tracer.recipe_fetched_from_cache(self.ref1)
+        tracer.package_downloaded(self.ref_p1, "http://drl.com")
+        tracer.recipe_downloaded(self.ref2, "http://drl.com")
+        tracer.package_install_error(self.ref_p2, INSTALL_ERROR_MISSING, "no package found",
+                                     remote="https://drl.com")
+
+        tracer.recipe_fetched_from_cache(self.ref3)
+        tracer.package_built(self.ref_p3)
+
+        install_info = tracer.get_install_info()
+        self.assertTrue(install_info["error"])
+
+        first_installed = install_info["installed"][0]
+        self.assertTrue(first_installed["recipe"]["cache"])
+        self.assertFalse(first_installed["recipe"]["downloaded"])
+        self.assertIsNone(first_installed["recipe"]["error"])
+        self.assertEquals(str(first_installed["recipe"]["id"]), "lib1/1.0@conan/stable")
+
+        self.assertFalse(first_installed["packages"][0]["cache"])
+        self.assertTrue(first_installed["packages"][0]["downloaded"])
+        self.assertIsNone(first_installed["packages"][0]["error"])
+        self.assertEquals(first_installed["packages"][0]["remote"], 'http://drl.com')
+        self.assertEquals(str(first_installed["packages"][0]["id"]), "1")
+
+        second_installed = install_info["installed"][1]
+        self.assertFalse(second_installed["recipe"]["cache"])
+        self.assertTrue(second_installed["recipe"]["downloaded"])
+        self.assertIsNone(second_installed["recipe"]["error"])
+        self.assertEquals(str(second_installed["recipe"]["id"]), "lib2/1.0@conan/stable")
+
+        self.assertFalse(second_installed["packages"][0]["cache"])
+        self.assertEquals(second_installed["packages"][0]["error"],
+                          {'type': 'missing', 'description': 'no package found',
+                           'remote': 'https://drl.com'})
+        self.assertEquals(second_installed["packages"][0]["remote"], 'https://drl.com')
+        self.assertEquals(str(second_installed["packages"][0]["id"]), "2")
+
+        third_installed = install_info["installed"][2]
+        self.assertFalse(third_installed["packages"][0]["cache"])
+        self.assertFalse(third_installed["packages"][0]["error"])
+        self.assertTrue(third_installed["packages"][0]["built"])
+        self.assertIsNone(third_installed["packages"][0]["remote"])
+        self.assertEquals(str(third_installed["packages"][0]["id"]), "3")
diff --git a/conans/test/utils/tools.py b/conans/test/utils/tools.py
index e0c485f60..1296e5f3f 100644
--- a/conans/test/utils/tools.py
+++ b/conans/test/utils/tools.py
@@ -163,7 +163,6 @@ class TestRequester(object):
                 kwargs["content_type"] = "application/json"
             kwargs.pop("json", None)
 
-
         return app, url
 
     @staticmethod