Repository: conan-io/conan

Issue:
unstable/incorrect PATH variable definition when the name variable is lowercased
I finally found the time to hunt down the unstable PATH definition which I discovered while writing a test for the virtualenv generator (see #2556).

The basic setup is quite simple. I have two packages, `base` and `derived` where `derived` requires `base`. Both packages should contribute two directories to the PATH variable, `basedir/bin:samebin` and `deriveddir/bin:samebin` respectively. However, in the `base` package PATH is written in lowercase (which was a typo).

Base Package:
~~~python
import os
from conans import ConanFile

class BaseConan(ConanFile):
    name = "base"
    version = "0.1"

    def package_info(self):
        self.env_info.path.extend([os.path.join("basedir", "bin"),"samebin"])
~~~

Derived Package:
~~~python
import os
from conans import ConanFile

class DerivedConan(ConanFile):
    name = "dummy"
    version = "0.1"
    requires = "base/0.1@lasote/testing"

    def package_info(self):
        self.env_info.PATH = [os.path.join("deriveddir", "bin"),"samebin"]
~~~
conanfile.txt:
~~~
[requires]
derived/0.1@lasote/testing
[generators]
virtualenv
~~~

When I now use the derived package, for example with the virtualenv generator, we randomly get one of the two following values for PATH:
* `PATH="deriveddir/bin":"samebin":"basedir/bin":"samebin":$PATH`
* `PATH="basedir/bin":"samebin":"deriveddir/bin":"samebin":$PATH`

The reason for this strange behavior is actually (an undocumented feature?) in the [update method of the EnvValues class](
https://github.com/conan-io/conan/blob/8151c4c39a5ffbf42f21a10d586fe88b8f1c8f04/conans/model/env_info.py#L145) where path is uppercased automatically. However, somehow also the lowercase `path` is still in one of the env dicts. Finally, depending on the random order between `PATH` and `path` in the unordered dict, either the one or the other result is generated.

Interestingly, when `PATH` is used instead of `path` in the `base` package, the returned value is `PATH="deriveddir/bin":"basedir/bin":"samebin":$PATH`. Note that the [lists have been deduplicated](https://github.com/conan-io/conan/blob/8151c4c39a5ffbf42f21a10d586fe88b8f1c8f04/conans/model/env_info.py#L236) which I did not expect. Is this another undocumented feature for env_info?

I am not sure what the best fix for this situation is. On the one hand, using `path` in the `base` package was a typo and I would not recommend to do it in general. On the other hand, given that there is special handling in place it kind of works sometimes which is the absolute worst case. I currently lean towards removing the special case for PATH completely in order to get deterministic results and to fail early when writing an incorrect recipe. However, I am clearly missing the big picture here to actually decide on the best solution. Maybe @lasote can shed some light on this issue.

Best,
Mario


Discussion:

Patch:
diff --git a/conans/client/cmd/create.py b/conans/client/cmd/create.py
new file mode 100644
index 000000000..b9b48f48a
--- /dev/null
+++ b/conans/client/cmd/create.py
@@ -0,0 +1,51 @@
+import os
+from conans.client.cmd.test import PackageTester
+from conans.errors import ConanException
+
+
+def get_test_conanfile_path(tf, conanfile_path):
+    """Searches in the declared test_folder or in the standard locations"""
+
+    if tf is False:
+        # Look up for testing conanfile can be disabled if tf (test folder) is False
+        return None
+
+    test_folders = [tf] if tf else ["test_package", "test"]
+    base_folder = os.path.dirname(conanfile_path)
+    for test_folder_name in test_folders:
+        test_folder = os.path.join(base_folder, test_folder_name)
+        test_conanfile_path = os.path.join(test_folder, "conanfile.py")
+        if os.path.exists(test_conanfile_path):
+            return test_conanfile_path
+    else:
+        if tf:
+            raise ConanException("test folder '%s' not available, or it doesn't have a conanfile.py"
+                                 % tf)
+
+
+def create(reference, manager, user_io, profile, remote, update, build_modes, manifest_folder,
+           manifest_verify, manifest_interactive, keep_build, test_build_folder, test_folder,
+           conanfile_path):
+
+    test_conanfile_path = get_test_conanfile_path(test_folder, conanfile_path)
+
+    if test_conanfile_path:
+        pt = PackageTester(manager, user_io)
+        pt.install_build_and_test(test_conanfile_path, reference, profile, remote, update,
+                                  build_modes=build_modes,
+                                  manifest_folder=manifest_folder,
+                                  manifest_verify=manifest_verify,
+                                  manifest_interactive=manifest_interactive,
+                                  keep_build=keep_build,
+                                  test_build_folder=test_build_folder)
+    else:
+        manager.install(reference=reference,
+                        install_folder=None,  # Not output anything
+                        manifest_folder=manifest_folder,
+                        manifest_verify=manifest_verify,
+                        manifest_interactive=manifest_interactive,
+                        remote_name=remote,
+                        profile=profile,
+                        build_modes=build_modes,
+                        update=update,
+                        keep_build=keep_build)
diff --git a/conans/client/cmd/uploader.py b/conans/client/cmd/uploader.py
index afde653c0..ac7f6297a 100644
--- a/conans/client/cmd/uploader.py
+++ b/conans/client/cmd/uploader.py
@@ -27,10 +27,11 @@ class CmdUpload(object):
         self._registry = registry
         self._cache_search = DiskSearchManager(self._client_cache)
 
-    def upload(self, reference_or_pattern, package_id=None, all_packages=None,
+    def upload(self, recorder, reference_or_pattern, package_id=None, all_packages=None,
                force=False, confirm=False, retry=0, retry_wait=0, skip_upload=False,
                integrity_check=False, no_overwrite=None, remote_name=None):
         """If package_id is provided, conan_reference_or_pattern is a ConanFileReference"""
+
         if package_id and not _is_a_reference(reference_or_pattern):
             raise ConanException("-p parameter only allowed with a valid recipe reference, "
                                  "not with a pattern")
@@ -42,7 +43,8 @@ class CmdUpload(object):
         else:
             references = self._cache_search.search_recipes(reference_or_pattern)
             if not references:
-                raise NotFoundException("No packages found matching pattern '%s'" % reference_or_pattern)
+                raise NotFoundException(("No packages found matching pattern '%s'" %
+                                         reference_or_pattern))
 
         for conan_ref in references:
             upload = True
@@ -54,21 +56,21 @@ class CmdUpload(object):
                     conanfile_path = self._client_cache.conanfile(conan_ref)
                     conan_file = load_conanfile_class(conanfile_path)
                 except NotFoundException:
-                    raise NotFoundException("There is no local conanfile exported as %s"
-                                            % str(conan_ref))
+                    raise NotFoundException(("There is no local conanfile exported as %s" %
+                                          str(conan_ref)))
                 if all_packages:
                     packages_ids = self._client_cache.conan_packages(conan_ref)
                 elif package_id:
                     packages_ids = [package_id, ]
                 else:
                     packages_ids = []
-                self._upload(conan_file, conan_ref, force, packages_ids, retry, retry_wait, skip_upload,
-                             integrity_check, no_overwrite, remote_name=remote_name)
+                self._upload(conan_file, conan_ref, force, packages_ids, retry, retry_wait,
+                             skip_upload, integrity_check, no_overwrite, remote_name, recorder)
 
         logger.debug("====> Time manager upload: %f" % (time.time() - t1))
 
     def _upload(self, conan_file, conan_ref, force, packages_ids, retry, retry_wait, skip_upload,
-                integrity_check, no_overwrite, remote_name):
+                integrity_check, no_overwrite, remote_name, recorder):
         """Uploads the recipes and binaries identified by conan_ref"""
 
         defined_remote = self._registry.get_ref(conan_ref)
@@ -86,6 +88,8 @@ class CmdUpload(object):
                                % (str(conan_ref), upload_remote.name))
         self._upload_recipe(conan_ref, retry, retry_wait, skip_upload, no_overwrite, upload_remote)
 
+        recorder.add_recipe(str(conan_ref), upload_remote.name, upload_remote.url)
+
         if packages_ids:
             # Can't use build_policy_always here because it's not loaded (only load_class)
             if conan_file.build_policy == "always":
@@ -93,9 +97,12 @@ class CmdUpload(object):
                                      "no packages can be uploaded")
             total = len(packages_ids)
             for index, package_id in enumerate(packages_ids):
-                self._upload_package(PackageReference(conan_ref, package_id), index + 1, total,
-                                     retry, retry_wait, skip_upload, integrity_check, no_overwrite,
-                                     upload_remote)
+                ret_upload_package = self._upload_package(PackageReference(conan_ref, package_id),
+                                                          index + 1, total, retry, retry_wait,
+                                                          skip_upload, integrity_check,
+                                                          no_overwrite, upload_remote)
+                if ret_upload_package:
+                    recorder.add_package(str(conan_ref), package_id)
 
         if not defined_remote and not skip_upload:
             self._registry.set_ref(conan_ref, upload_remote)
@@ -126,8 +133,8 @@ class CmdUpload(object):
 
         result = self._remote_manager.upload_package(package_ref, remote, retry, retry_wait,
                                                      skip_upload, integrity_check, no_overwrite)
-        return result
         logger.debug("====> Time uploader upload_package: %f" % (time.time() - t1))
+        return result
 
     def _check_recipe_date(self, conan_ref, remote):
         try:
diff --git a/conans/client/command.py b/conans/client/command.py
index b1df17448..88e47c05b 100644
--- a/conans/client/command.py
+++ b/conans/client/command.py
@@ -248,7 +248,7 @@ class Command(object):
             raise
         finally:
             if args.json and info:
-                self._outputer.json_install(info, args.json, cwd)
+                self._outputer.json_output(info, args.json, cwd)
 
     def download(self, *args):
         """Downloads recipe and binaries to the local cache, without using settings. It works
@@ -338,7 +338,7 @@ class Command(object):
             raise
         finally:
             if args.json and info:
-                self._outputer.json_install(info, args.json, cwd)
+                self._outputer.json_output(info, args.json, cwd)
 
     def config(self, *args):
         """Manages Conan configuration. Edits the conan.conf or installs config files.
@@ -877,14 +877,26 @@ class Command(object):
         parser.add_argument("-no", "--no-overwrite", nargs="?", type=str, choices=["all", "recipe"],
                             action=OnceArgument, const="all",
                             help="Uploads package only if recipe is the same as the remote one")
+        parser.add_argument("-j", "--json", default=None, action=OnceArgument,
+                            help='json file path where the install information will be written to')
 
         args = parser.parse_args(*args)
 
-        return self._conan.upload(pattern=args.pattern_or_reference, package=args.package,
-                                  remote=args.remote, all_packages=args.all, force=args.force,
-                                  confirm=args.confirm, retry=args.retry,
-                                  retry_wait=args.retry_wait, skip_upload=args.skip_upload,
-                                  integrity_check=args.check, no_overwrite=args.no_overwrite)
+        cwd = os.getcwd()
+        info = None
+
+        try:
+            info = self._conan.upload(pattern=args.pattern_or_reference, package=args.package,
+                                      remote=args.remote, all_packages=args.all, force=args.force,
+                                      confirm=args.confirm, retry=args.retry,
+                                      retry_wait=args.retry_wait, skip_upload=args.skip_upload,
+                                      integrity_check=args.check, no_overwrite=args.no_overwrite)
+        except ConanException as exc:
+            info = exc.info
+            raise
+        finally:
+            if args.json and info:
+                self._outputer.json_output(info, args.json, cwd)
 
     def remote(self, *args):
         """Manages the remote list and the package recipes associated to a remote.
@@ -901,6 +913,8 @@ class Command(object):
                                 help='Verify SSL certificated. Default True')
         parser_add.add_argument("-i", "--insert", nargs="?", const=0, type=int, action=OnceArgument,
                                 help="insert remote at specific index")
+        parser_add.add_argument("-f", "--force", default=False, action='store_true',
+                                help="Force addition, will update if existing")
         parser_rm = subparsers.add_parser('remove', help='Remove a remote')
         parser_rm.add_argument('remote', help='Name of the remote')
         parser_upd = subparsers.add_parser('update', help='Update the remote url')
@@ -942,7 +956,7 @@ class Command(object):
             remotes = self._conan.remote_list()
             self._outputer.remote_list(remotes)
         elif args.subcommand == "add":
-            return self._conan.remote_add(remote, url, verify_ssl, args.insert)
+            return self._conan.remote_add(remote, url, verify_ssl, args.insert, args.force)
         elif args.subcommand == "remove":
             return self._conan.remote_remove(remote)
         elif args.subcommand == "rename":
diff --git a/conans/client/conan_api.py b/conans/client/conan_api.py
index bed81708f..90d8214a6 100644
--- a/conans/client/conan_api.py
+++ b/conans/client/conan_api.py
@@ -5,7 +5,8 @@ import requests
 
 import conans
 from conans import __version__ as client_version, tools
-from conans.client.action_recorder import ActionRecorder
+from conans.client.cmd.create import create
+from conans.client.recorder.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
@@ -13,6 +14,7 @@ from conans.client.migrations import ClientMigrator
 from conans.client.output import ConanOutput, ScopedOutput
 from conans.client.profile_loader import read_profile, profile_from_args, \
     read_conaninfo_profile
+from conans.client.recorder.upload_recoder import UploadRecoder
 from conans.client.remote_manager import RemoteManager
 from conans.client.remote_registry import RemoteRegistry
 from conans.client.rest.auth_manager import ConanApiAuthManager
@@ -69,21 +71,15 @@ def api_method(f):
         try:
             curdir = get_cwd()
             log_command(f.__name__, kwargs)
-            the_self._init_manager()
             with tools.environment_append(the_self._client_cache.conan_config.env_vars):
                 # Patch the globals in tools
-                ret = f(*args, **kwargs)
-                if ret is None:  # FIXME: Probably each method should manage its return
-                    return the_self._recorder.get_info()
-                return ret
+                return f(*args, **kwargs)
         except Exception as exc:
             msg = exception_message_safe(exc)
             try:
                 log_exception(exc, msg)
             except:
                 pass
-            if isinstance(exc, ConanException):
-                exc.info = the_self._recorder.get_info()
             raise
         finally:
             os.chdir(curdir)
@@ -220,18 +216,15 @@ class ConanAPIV1(object):
         self._search_manager = search_manager
         self._settings_preprocessor = _settings_preprocessor
         self._registry = RemoteRegistry(self._client_cache.registry, self._user_io.out)
-        self._recorder = None
-        self._manager = None
 
         if not interactive:
             self._user_io.disable_input()
 
-    def _init_manager(self):
+    def _init_manager(self, action_recorder):
         """Every api call gets a new recorder and new manager"""
-        self._recorder = ActionRecorder()
-        self._manager = ConanManager(self._client_cache, self._user_io, self._runner,
-                                     self._remote_manager, self._search_manager,
-                                     self._settings_preprocessor, self._recorder, self._registry)
+        return ConanManager(self._client_cache, self._user_io, self._runner,
+                            self._remote_manager, self._search_manager,
+                            self._settings_preprocessor, action_recorder, self._registry)
 
     @api_method
     def new(self, name, header=False, pure_c=False, test=False, exports_sources=False, bare=False,
@@ -271,7 +264,9 @@ class ConanAPIV1(object):
         profile = profile_from_args(profile_name, settings, options, env, cwd,
                                     self._client_cache)
         reference = ConanFileReference.loads(reference)
-        pt = PackageTester(self._manager, self._user_io)
+        recorder = ActionRecorder()
+        manager = self._init_manager(recorder)
+        pt = PackageTester(manager, self._user_io)
         pt.install_build_and_test(conanfile_path, reference, profile, remote,
                                   update, build_modes=build_modes,
                                   test_build_folder=test_build_folder)
@@ -295,76 +290,49 @@ class ConanAPIV1(object):
         options = options or []
         env = env or []
 
-        cwd = cwd or get_cwd()
-        conanfile_path = _get_conanfile_path(conanfile_path, cwd, py=True)
+        try:
+            cwd = cwd or os.getcwd()
+            recorder = ActionRecorder()
+            conanfile_path = _get_conanfile_path(conanfile_path, cwd, py=True)
 
-        if not name or not version:
-            conanfile = load_conanfile_class(conanfile_path)
-            name, version = conanfile.name, conanfile.version
             if not name or not version:
-                raise ConanException("conanfile.py doesn't declare package name or version")
-
-        reference = ConanFileReference(name, version, user, channel)
-        scoped_output = ScopedOutput(str(reference), self._user_io.out)
-        # Make sure keep_source is set for keep_build
-        if keep_build:
-            keep_source = True
-        # Forcing an export!
-        if not not_export:
-            scoped_output.highlight("Exporting package recipe")
-            cmd_export(conanfile_path, name, version, user, channel, keep_source,
-                       self._user_io.out, self._client_cache)
-
-        if build_modes is None:  # Not specified, force build the tested library
-            build_modes = [name]
-
-        manifests = _parse_manifests_arguments(verify, manifests, manifests_interactive, cwd)
-        manifest_folder, manifest_interactive, manifest_verify = manifests
-        profile = profile_from_args(profile_name, settings, options, env,
-                                    cwd, self._client_cache)
-
-        def get_test_conanfile_path(tf):
-            """Searches in the declared test_folder or in the standard locations"""
-
-            if tf is False:
-                # Look up for testing conanfile can be disabled if tf (test folder) is False
-                return None
-
-            test_folders = [tf] if tf else ["test_package", "test"]
-            base_folder = os.path.dirname(conanfile_path)
-            for test_folder_name in test_folders:
-                test_folder = os.path.join(base_folder, test_folder_name)
-                test_conanfile_path = os.path.join(test_folder, "conanfile.py")
-                if os.path.exists(test_conanfile_path):
-                    return test_conanfile_path
-            else:
-                if tf:
-                    raise ConanException("test folder '%s' not available, "
-                                         "or it doesn't have a conanfile.py" % tf)
-
-        test_conanfile_path = get_test_conanfile_path(test_folder)
-        self._recorder.add_recipe_being_developed(reference)
-
-        if test_conanfile_path:
-            pt = PackageTester(self._manager, self._user_io)
-            pt.install_build_and_test(test_conanfile_path, reference, profile,
-                                      remote, update, build_modes=build_modes,
-                                      manifest_folder=manifest_folder,
-                                      manifest_verify=manifest_verify,
-                                      manifest_interactive=manifest_interactive,
-                                      keep_build=keep_build,
-                                      test_build_folder=test_build_folder)
-        else:
-            self._manager.install(reference=reference,
-                                  install_folder=None,  # Not output anything
-                                  manifest_folder=manifest_folder,
-                                  manifest_verify=manifest_verify,
-                                  manifest_interactive=manifest_interactive,
-                                  remote_name=remote,
-                                  profile=profile,
-                                  build_modes=build_modes,
-                                  update=update,
-                                  keep_build=keep_build)
+                conanfile = load_conanfile_class(conanfile_path)
+                name, version = conanfile.name, conanfile.version
+                if not name or not version:
+                    raise ConanException("conanfile.py doesn't declare package name or version")
+
+            reference = ConanFileReference(name, version, user, channel)
+            scoped_output = ScopedOutput(str(reference), self._user_io.out)
+            # Make sure keep_source is set for keep_build
+            if keep_build:
+                keep_source = True
+            # Forcing an export!
+            if not not_export:
+                scoped_output.highlight("Exporting package recipe")
+                cmd_export(conanfile_path, name, version, user, channel, keep_source,
+                           self._user_io.out, self._client_cache)
+
+            if build_modes is None:  # Not specified, force build the tested library
+                build_modes = [name]
+
+            manifests = _parse_manifests_arguments(verify, manifests, manifests_interactive, cwd)
+            manifest_folder, manifest_interactive, manifest_verify = manifests
+            profile = profile_from_args(profile_name, settings, options, env,
+                                        cwd, self._client_cache)
+
+            manager = self._init_manager(recorder)
+            recorder.add_recipe_being_developed(reference)
+
+            create(reference, manager, self._user_io, profile, remote, update, build_modes,
+                   manifest_folder, manifest_verify, manifest_interactive, keep_build,
+                   test_build_folder, test_folder, conanfile_path)
+
+            return recorder.get_info()
+
+        except ConanException as exc:
+            recorder.error = True
+            exc.info = recorder.get_info()
+            raise
 
     @api_method
     def export_pkg(self, conanfile_path, name, channel, source_folder=None, build_folder=None,
@@ -419,9 +387,11 @@ class ConanAPIV1(object):
             version = conanfile.version
 
         reference = ConanFileReference(name, version, user, channel)
-        self._manager.export_pkg(reference, source_folder=source_folder, build_folder=build_folder,
-                                 package_folder=package_folder, install_folder=install_folder,
-                                 profile=profile, force=force)
+        recorder = ActionRecorder()
+        manager = self._init_manager(recorder)
+        manager.export_pkg(reference, source_folder=source_folder, build_folder=build_folder,
+                           package_folder=package_folder, install_folder=install_folder,
+                           profile=profile, force=force)
 
     @api_method
     def download(self, reference, remote=None, package=None, recipe=False):
@@ -429,7 +399,9 @@ class ConanAPIV1(object):
             raise ConanException("recipe parameter cannot be used together with package")
         # Install packages without settings (fixed ids or all)
         conan_ref = ConanFileReference.loads(reference)
-        self._manager.download(conan_ref, package, remote_name=remote, recipe=recipe)
+        recorder = ActionRecorder()
+        manager = self._init_manager(recorder)
+        manager.download(conan_ref, package, remote_name=remote, recipe=recipe)
 
     @api_method
     def install_reference(self, reference, settings=None, options=None, env=None,
@@ -437,26 +409,33 @@ class ConanAPIV1(object):
                           manifests_interactive=None, build=None, profile_name=None,
                           update=False, generators=None, install_folder=None, cwd=None):
 
-        cwd = cwd or get_cwd()
-        install_folder = _make_abs_path(install_folder, cwd)
-
-        manifests = _parse_manifests_arguments(verify, manifests, manifests_interactive, cwd)
-        manifest_folder, manifest_interactive, manifest_verify = manifests
-
-        profile = profile_from_args(profile_name, settings, options, env, cwd,
-                                    self._client_cache)
-
-        if not generators:  # We don't want the default txt
-            generators = False
-
-        mkdir(install_folder)
-        self._manager.install(reference=reference, install_folder=install_folder, remote_name=remote,
-                              profile=profile, build_modes=build, update=update,
-                              manifest_folder=manifest_folder,
-                              manifest_verify=manifest_verify,
-                              manifest_interactive=manifest_interactive,
-                              generators=generators,
-                              install_reference=True)
+        try:
+            recorder = ActionRecorder()
+            cwd = cwd or os.getcwd()
+            install_folder = _make_abs_path(install_folder, cwd)
+
+            manifests = _parse_manifests_arguments(verify, manifests, manifests_interactive, cwd)
+            manifest_folder, manifest_interactive, manifest_verify = manifests
+
+            profile = profile_from_args(profile_name, settings, options, env, cwd,
+                                        self._client_cache)
+
+            if not generators:  # We don't want the default txt
+                generators = False
+
+            mkdir(install_folder)
+            manager = self._init_manager(recorder)
+            manager.install(reference=reference, install_folder=install_folder,
+                            remote_name=remote, profile=profile, build_modes=build,
+                            update=update, manifest_folder=manifest_folder,
+                            manifest_verify=manifest_verify,
+                            manifest_interactive=manifest_interactive,
+                            generators=generators, install_reference=True)
+            return recorder.get_info()
+        except ConanException as exc:
+            recorder.error = True
+            exc.info = recorder.get_info()
+            raise
 
     @api_method
     def install(self, path="", settings=None, options=None, env=None,
@@ -464,27 +443,34 @@ class ConanAPIV1(object):
                 manifests_interactive=None, build=None, profile_name=None,
                 update=False, generators=None, no_imports=False, install_folder=None, cwd=None):
 
-        cwd = cwd or get_cwd()
-        install_folder = _make_abs_path(install_folder, cwd)
-        conanfile_path = _get_conanfile_path(path, cwd, py=None)
-
-        manifests = _parse_manifests_arguments(verify, manifests, manifests_interactive, cwd)
-        manifest_folder, manifest_interactive, manifest_verify = manifests
-
-        profile = profile_from_args(profile_name, settings, options, env, cwd,
-                                    self._client_cache)
-
-        self._manager.install(reference=conanfile_path,
-                              install_folder=install_folder,
-                              remote_name=remote,
-                              profile=profile,
-                              build_modes=build,
-                              update=update,
-                              manifest_folder=manifest_folder,
-                              manifest_verify=manifest_verify,
-                              manifest_interactive=manifest_interactive,
-                              generators=generators,
-                              no_imports=no_imports)
+        try:
+            recorder = ActionRecorder()
+            cwd = cwd or os.getcwd()
+            install_folder = _make_abs_path(install_folder, cwd)
+            conanfile_path = _get_conanfile_path(path, cwd, py=None)
+
+            manifests = _parse_manifests_arguments(verify, manifests, manifests_interactive, cwd)
+            manifest_folder, manifest_interactive, manifest_verify = manifests
+
+            profile = profile_from_args(profile_name, settings, options, env, cwd,
+                                        self._client_cache)
+            manager = self._init_manager(recorder)
+            manager.install(reference=conanfile_path,
+                            install_folder=install_folder,
+                            remote_name=remote,
+                            profile=profile,
+                            build_modes=build,
+                            update=update,
+                            manifest_folder=manifest_folder,
+                            manifest_verify=manifest_verify,
+                            manifest_interactive=manifest_interactive,
+                            generators=generators,
+                            no_imports=no_imports)
+            return recorder.get_info()
+        except ConanException as exc:
+            recorder.error = True
+            exc.info = recorder.get_info()
+            raise
 
     @api_method
     def config_get(self, item):
@@ -531,7 +517,10 @@ class ConanAPIV1(object):
                          install_folder=None):
         reference, profile = self._info_get_profile(reference, install_folder, profile_name, settings,
                                                     options, env)
-        graph = self._manager.info_build_order(reference, profile, build_order, remote, check_updates)
+
+        recorder = ActionRecorder()
+        manager = self._init_manager(recorder)
+        graph = manager.info_build_order(reference, profile, build_order, remote, check_updates)
         return graph
 
     @api_method
@@ -539,8 +528,11 @@ class ConanAPIV1(object):
                             profile_name=None, remote=None, check_updates=None, install_folder=None):
         reference, profile = self._info_get_profile(reference, install_folder, profile_name, settings,
                                                     options, env)
-        ret = self._manager.info_nodes_to_build(reference, profile, build_modes, remote,
-                                                check_updates)
+
+        recorder = ActionRecorder()
+        manager = self._init_manager(recorder)
+        ret = manager.info_nodes_to_build(reference, profile, build_modes, remote,
+                                          check_updates)
         ref_list, project_reference = ret
         return ref_list, project_reference
 
@@ -549,8 +541,11 @@ class ConanAPIV1(object):
                        profile_name=None, update=False, install_folder=None):
         reference, profile = self._info_get_profile(reference, install_folder, profile_name, settings,
                                                     options, env)
-        ret = self._manager.info_get_graph(reference, remote_name=remote, profile=profile,
-                                           check_updates=update)
+
+        recorder = ActionRecorder()
+        manager = self._init_manager(recorder)
+        ret = manager.info_get_graph(reference, remote_name=remote, profile=profile,
+                                     check_updates=update)
         deps_graph, graph_updates_info, project_reference = ret
         return deps_graph, graph_updates_info, project_reference
 
@@ -566,9 +561,11 @@ class ConanAPIV1(object):
         default_pkg_folder = os.path.join(build_folder, "package")
         package_folder = _make_abs_path(package_folder, cwd, default=default_pkg_folder)
 
-        self._manager.build(conanfile_path, source_folder, build_folder, package_folder,
-                            install_folder, should_configure=should_configure, should_build=should_build,
-                            should_install=should_install)
+        recorder = ActionRecorder()
+        manager = self._init_manager(recorder)
+        manager.build(conanfile_path, source_folder, build_folder, package_folder,
+                      install_folder, should_configure=should_configure, should_build=should_build,
+                      should_install=should_install)
 
     @api_method
     def package(self, path, build_folder, package_folder, source_folder=None, install_folder=None, cwd=None):
@@ -580,8 +577,10 @@ class ConanAPIV1(object):
         default_pkg_folder = os.path.join(build_folder, "package")
         package_folder = _make_abs_path(package_folder, cwd, default=default_pkg_folder)
 
-        self._manager.local_package(package_folder, conanfile_path, build_folder, source_folder,
-                                    install_folder)
+        recorder = ActionRecorder()
+        manager = self._init_manager(recorder)
+        manager.local_package(package_folder, conanfile_path, build_folder, source_folder,
+                              install_folder)
 
     @api_method
     def source(self, path, source_folder=None, info_folder=None, cwd=None):
@@ -594,7 +593,9 @@ class ConanAPIV1(object):
         if not os.path.exists(info_folder):
             raise ConanException("Specified info-folder doesn't exist")
 
-        self._manager.source(conanfile_path, source_folder, info_folder)
+        recorder = ActionRecorder()
+        manager = self._init_manager(recorder)
+        manager.source(conanfile_path, source_folder, info_folder)
 
     @api_method
     def imports(self, path, dest=None, info_folder=None, cwd=None):
@@ -611,7 +612,9 @@ class ConanAPIV1(object):
 
         mkdir(dest)
         conanfile_abs_path = _get_conanfile_path(path, cwd, py=None)
-        self._manager.imports(conanfile_abs_path, dest, info_folder)
+        recorder = ActionRecorder()
+        manager = self._init_manager(recorder)
+        manager.imports(conanfile_abs_path, dest, info_folder)
 
     @api_method
     def imports_undo(self, manifest_path):
@@ -628,9 +631,12 @@ class ConanAPIV1(object):
     @api_method
     def remove(self, pattern, query=None, packages=None, builds=None, src=False, force=False,
                remote=None, outdated=False):
-        self._manager.remove(pattern, package_ids_filter=packages, build_ids=builds,
-                             src=src, force=force, remote_name=remote, packages_query=query,
-                             outdated=outdated)
+
+        recorder = ActionRecorder()
+        manager = self._init_manager(recorder)
+        manager.remove(pattern, package_ids_filter=packages, build_ids=builds,
+                       src=src, force=force, remote_name=remote, packages_query=query,
+                       outdated=outdated)
 
     @api_method
     def copy(self, reference, user_channel, force=False, packages=None):
@@ -685,20 +691,32 @@ class ConanAPIV1(object):
         """ Uploads a package recipe and the generated binary packages to a specified remote
         """
 
+        recorder = UploadRecoder()
+
         if force and no_overwrite:
-            raise ConanException("'no_overwrite' argument cannot be used together with 'force'")
+            exc = ConanException("'no_overwrite' argument cannot be used together with 'force'")
+            recorder.error = True
+            exc.info = recorder.get_info()
+            raise exc
 
-        uploader = CmdUpload(self._client_cache, self._user_io, self._remote_manager, self._registry)
-        return uploader.upload(pattern, package, all_packages, force, confirm, retry, retry_wait,
-                               skip_upload, integrity_check, no_overwrite, remote)
+        uploader = CmdUpload(self._client_cache, self._user_io, self._remote_manager,
+                             self._registry)
+        try:
+            uploader.upload(recorder, pattern, package, all_packages, force, confirm, retry,
+                            retry_wait, skip_upload, integrity_check, no_overwrite, remote)
+            return recorder.get_info()
+        except ConanException as exc:
+            recorder.error = True
+            exc.info = recorder.get_info()
+            raise
 
     @api_method
     def remote_list(self):
         return self._registry.remotes
 
     @api_method
-    def remote_add(self, remote, url, verify_ssl=True, insert=None):
-        return self._registry.add(remote, url, verify_ssl, insert)
+    def remote_add(self, remote, url, verify_ssl=True, insert=None, force=None):
+        return self._registry.add(remote, url, verify_ssl, insert, force)
 
     @api_method
     def remote_remove(self, remote):
diff --git a/conans/client/conan_command_output.py b/conans/client/conan_command_output.py
index b82ffbe62..43806ffeb 100644
--- a/conans/client/conan_command_output.py
+++ b/conans/client/conan_command_output.py
@@ -47,7 +47,7 @@ class CommandOutputer(object):
                 json_output = os.path.join(cwd, json_output)
             save(json_output, json_str)
 
-    def json_install(self, info, json_output, cwd):
+    def json_output(self, info, json_output, cwd):
         cwd = os.path.abspath(cwd or get_cwd())
         if not os.path.isabs(json_output):
             json_output = os.path.join(cwd, json_output)
diff --git a/conans/client/installer.py b/conans/client/installer.py
index f60ca7e54..2046fb3fd 100644
--- a/conans/client/installer.py
+++ b/conans/client/installer.py
@@ -4,7 +4,7 @@ import shutil
 import platform
 
 from conans.client import tools
-from conans.client.action_recorder import INSTALL_ERROR_MISSING_BUILD_FOLDER, INSTALL_ERROR_BUILDING
+from conans.client.recorder.action_recorder import INSTALL_ERROR_MISSING_BUILD_FOLDER, INSTALL_ERROR_BUILDING
 from conans.model.conan_file import get_env_context_manager
 from conans.model.env_info import EnvInfo
 from conans.model.user_info import UserInfo
diff --git a/conans/client/package_installer.py b/conans/client/package_installer.py
index 03de7efda..1c4e298fe 100644
--- a/conans/client/package_installer.py
+++ b/conans/client/package_installer.py
@@ -1,6 +1,6 @@
 import os
 
-from conans.client.action_recorder import INSTALL_ERROR_MISSING
+from conans.client.recorder.action_recorder import INSTALL_ERROR_MISSING
 from conans.errors import (ConanException, NotFoundException, NoRemoteAvailable)
 from conans.model.ref import PackageReference
 from conans.util.files import rmdir, make_read_only
diff --git a/conans/client/proxy.py b/conans/client/proxy.py
index ac74424f1..f55fe3b7e 100644
--- a/conans/client/proxy.py
+++ b/conans/client/proxy.py
@@ -5,7 +5,7 @@ from requests.exceptions import RequestException
 from conans.client.loader_parse import load_conanfile_class
 from conans.client.output import ScopedOutput
 from conans.client.remover import DiskRemover
-from conans.client.action_recorder import INSTALL_ERROR_MISSING, INSTALL_ERROR_NETWORK
+from conans.client.recorder.action_recorder import INSTALL_ERROR_MISSING, INSTALL_ERROR_NETWORK
 from conans.errors import (ConanException, NotFoundException, NoRemoteAvailable)
 from conans.model.ref import PackageReference
 from conans.util.log import logger
diff --git a/conans/client/recorder/__init__.py b/conans/client/recorder/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/conans/client/action_recorder.py b/conans/client/recorder/action_recorder.py
similarity index 100%
rename from conans/client/action_recorder.py
rename to conans/client/recorder/action_recorder.py
diff --git a/conans/client/recorder/upload_recoder.py b/conans/client/recorder/upload_recoder.py
new file mode 100644
index 000000000..f6d712d5f
--- /dev/null
+++ b/conans/client/recorder/upload_recoder.py
@@ -0,0 +1,47 @@
+from collections import namedtuple, OrderedDict
+from datetime import datetime
+
+
+class _UploadRecipe(namedtuple("UploadRecipe", "reference, remote_name, remote_url, time")):
+
+    def __new__(cls, reference, remote_name, remote_url):
+        the_time = datetime.utcnow()
+        return super(cls, _UploadRecipe).__new__(cls, reference, remote_name, remote_url, the_time)
+
+    def to_dict(self):
+        return {"id": self.reference, "remote_name": self.remote_name,
+                "remote_url": self.remote_url, "time": self.time}
+
+
+class _UploadPackage(namedtuple("UploadPackage", "package_id, time")):
+
+    def __new__(cls, package_id):
+        the_time = datetime.utcnow()
+        return super(cls, _UploadPackage).__new__(cls, package_id, the_time)
+
+    def to_dict(self):
+        return {"id": self.package_id, "time": self.time}
+
+
+class UploadRecoder(object):
+
+    def __init__(self):
+        self.error = False
+        self._info = OrderedDict()
+
+    def add_recipe(self, reference, remote_name, remote_url):
+        self._info[reference] = {"recipe": _UploadRecipe(reference, remote_name, remote_url),
+                                 "packages": []}
+
+    def add_package(self, reference, package_id):
+        self._info[reference]["packages"].append(_UploadPackage(package_id))
+
+    def get_info(self):
+        info = {"error": self.error, "uploaded": []}
+
+        for item in self._info.values():
+            recipe_info = item["recipe"].to_dict()
+            packages_info = [package.to_dict() for package in item["packages"]]
+            info["uploaded"].append({"recipe": recipe_info, "packages": packages_info})
+
+        return info
diff --git a/conans/client/remote_registry.py b/conans/client/remote_registry.py
index 20f08f0d5..bf6df2bab 100644
--- a/conans/client/remote_registry.py
+++ b/conans/client/remote_registry.py
@@ -1,8 +1,10 @@
 import os
+import fasteners
+
+from collections import OrderedDict, namedtuple
+
 from conans.errors import ConanException, NoRemoteAvailable
 from conans.util.files import load, save
-from collections import OrderedDict, namedtuple
-import fasteners
 from conans.util.config_parser import get_bool_from_text_value
 from conans.util.log import logger
 
@@ -155,12 +157,46 @@ class RemoteRegistry(object):
             refs[conan_reference] = remote
             self._save(remotes, refs)
 
-    def add(self, remote_name, remote, verify_ssl=True, insert=None):
+    def _upsert(self, remote_name, url, verify_ssl, insert):
+        self._remotes = None  # invalidate cached remotes
+        with fasteners.InterProcessLock(self._filename + ".lock", logger=logger):
+            remotes, refs = self._load()
+            # Remove duplicates
+            remotes.pop(remote_name, None)
+            remotes_list = []
+            renamed = None
+            for name, r in remotes.items():
+                if r[0] != url:
+                    remotes_list.append((name, r))
+                else:
+                    renamed = name
+
+            if insert is not None:
+                try:
+                    insert_index = int(insert)
+                except ValueError:
+                    raise ConanException("insert argument must be an integer")
+                remotes_list.insert(insert_index, (remote_name, (url, verify_ssl)))
+                remotes = OrderedDict(remotes_list)
+            else:
+                remotes = OrderedDict(remotes_list)
+                remotes[remote_name] = (url, verify_ssl)
+
+            if renamed:
+                for k, v in refs.items():
+                    if v == renamed:
+                        refs[k] = remote_name
+            self._save(remotes, refs)
+
+    def add(self, remote_name, url, verify_ssl=True, insert=None, force=None):
+        if force:
+            return self._upsert(remote_name, url, verify_ssl, insert)
+
         def exists_function(remotes):
             if remote_name in remotes:
                 raise ConanException("Remote '%s' already exists in remotes (use update to modify)"
                                      % remote_name)
-        self._add_update(remote_name, remote, verify_ssl, exists_function, insert)
+        self._add_update(remote_name, url, verify_ssl, exists_function, insert)
 
     def remove(self, remote_name):
         self._remotes = None  # invalidate cached remotes
@@ -172,11 +208,11 @@ class RemoteRegistry(object):
             refs = {k: v for k, v in refs.items() if v != remote_name}
             self._save(remotes, refs)
 
-    def update(self, remote_name, remote, verify_ssl=True, insert=None):
+    def update(self, remote_name, url, verify_ssl=True, insert=None):
         def exists_function(remotes):
             if remote_name not in remotes:
                 raise ConanException("Remote '%s' not found in remotes" % remote_name)
-        self._add_update(remote_name, remote, verify_ssl, exists_function, insert)
+        self._add_update(remote_name, url, verify_ssl, exists_function, insert)
 
     def rename(self, remote_name, new_remote_name):
         self._remotes = None  # invalidate cached remotes
@@ -204,14 +240,14 @@ class RemoteRegistry(object):
             refs = {k: v for k, v in refs.items() if v in new_remotes}
             self._save(new_remotes, refs)
 
-    def _add_update(self, remote_name, remote, verify_ssl, exists_function, insert=None):
+    def _add_update(self, remote_name, url, verify_ssl, exists_function, insert=None):
         self._remotes = None  # invalidate cached remotes
         with fasteners.InterProcessLock(self._filename + ".lock", logger=logger):
             remotes, refs = self._load()
             exists_function(remotes)
             urls = {r[0]: name for name, r in remotes.items() if name != remote_name}
-            if remote in urls:
-                raise ConanException("Remote '%s' already exists with same URL" % urls[remote])
+            if url in urls:
+                raise ConanException("Remote '%s' already exists with same URL" % urls[url])
             if insert is not None:
                 try:
                     insert_index = int(insert)
@@ -219,8 +255,8 @@ class RemoteRegistry(object):
                     raise ConanException("insert argument must be an integer")
                 remotes.pop(remote_name, None)  # Remove if exists (update)
                 remotes_list = list(remotes.items())
-                remotes_list.insert(insert_index, (remote_name, (remote, verify_ssl)))
+                remotes_list.insert(insert_index, (remote_name, (url, verify_ssl)))
                 remotes = OrderedDict(remotes_list)
             else:
-                remotes[remote_name] = (remote, verify_ssl)
+                remotes[remote_name] = (url, verify_ssl)
             self._save(remotes, refs)
diff --git a/conans/model/env_info.py b/conans/model/env_info.py
index f2a133b21..7a5346a75 100644
--- a/conans/model/env_info.py
+++ b/conans/model/env_info.py
@@ -142,7 +142,6 @@ class EnvValues(object):
             # DepsEnvInfo. the OLD values are always kept, never overwrite,
             elif isinstance(env_obj, DepsEnvInfo):
                 for (name, value) in env_obj.vars.items():
-                    name = name.upper() if name.lower() == "path" else name
                     self.add(name, value)
             else:
                 raise ConanException("unknown env type: %s" % env_obj)
@@ -197,10 +196,17 @@ class EnvInfo(object):
     def __init__(self):
         self._values_ = {}
 
+    @staticmethod
+    def _adjust_casing(name):
+        """We don't want to mix "path" with "PATH", actually we don`t want to mix anything
+        with different casing. Furthermore in Windows all is uppercase, but managing all in
+        upper case will be breaking."""
+        return name.upper() if name.lower() == "path" else name
+
     def __getattr__(self, name):
         if name.startswith("_") and name.endswith("_"):
             return super(EnvInfo, self).__getattr__(name)
-
+        name = self._adjust_casing(name)
         attr = self._values_.get(name)
         if not attr:
             self._values_[name] = []
@@ -209,6 +215,7 @@ class EnvInfo(object):
     def __setattr__(self, name, value):
         if name.startswith("_") and name.endswith("_"):
             return super(EnvInfo, self).__setattr__(name, value)
+        name = self._adjust_casing(name)
         self._values_[name] = value
 
     @property


Test patch:
diff --git a/conans/test/command/remote_test.py b/conans/test/command/remote_test.py
index e7cb82f21..d28942a5e 100644
--- a/conans/test/command/remote_test.py
+++ b/conans/test/command/remote_test.py
@@ -41,6 +41,49 @@ class RemoteTest(unittest.TestCase):
         output = str(self.client.user_io.out)
         self.assertIn("remote1: http://", output.splitlines()[0])
 
+    def add_force_test(self):
+        client = TestClient()
+        client.run("remote add r1 https://r1")
+        client.run("remote add r2 https://r2")
+        client.run("remote add r3 https://r3")
+        client.run("remote add_ref Hello/0.1@user/testing r2")
+        client.run("remote add_ref Hello2/0.1@user/testing r1")
+
+        client.run("remote add r4 https://r4 -f")
+        client.run("remote list")
+        lines = str(client.user_io.out).splitlines()
+        self.assertIn("r1: https://r1", lines[0])
+        self.assertIn("r2: https://r2", lines[1])
+        self.assertIn("r3: https://r3", lines[2])
+        self.assertIn("r4: https://r4", lines[3])
+
+        client.run("remote add r2 https://newr2 -f")
+        client.run("remote list")
+        lines = str(client.user_io.out).splitlines()
+        self.assertIn("r1: https://r1", lines[0])
+        self.assertIn("r3: https://r3", lines[1])
+        self.assertIn("r4: https://r4", lines[2])
+        self.assertIn("r2: https://newr2", lines[3])
+
+        client.run("remote add newr1 https://r1 -f")
+        client.run("remote list")
+        lines = str(client.user_io.out).splitlines()
+        self.assertIn("r3: https://r3", lines[0])
+        self.assertIn("r4: https://r4", lines[1])
+        self.assertIn("r2: https://newr2", lines[2])
+        self.assertIn("newr1: https://r1", lines[3])
+        client.run("remote list_ref")
+        self.assertIn("Hello2/0.1@user/testing: newr1", client.out)
+        self.assertIn("Hello/0.1@user/testing: r2", client.out)
+
+        client.run("remote add newr1 https://newr1 -f -i")
+        client.run("remote list")
+        lines = str(client.user_io.out).splitlines()
+        self.assertIn("newr1: https://newr1", lines[0])
+        self.assertIn("r3: https://r3", lines[1])
+        self.assertIn("r4: https://r4", lines[2])
+        self.assertIn("r2: https://newr2", lines[3])
+
     def rename_test(self):
         client = TestClient()
         client.run("remote add r1 https://r1")
diff --git a/conans/test/command/upload_complete_test.py b/conans/test/command/upload_complete_test.py
index 3f05a5a11..bad8447fb 100644
--- a/conans/test/command/upload_complete_test.py
+++ b/conans/test/command/upload_complete_test.py
@@ -1,3 +1,4 @@
+import json
 import unittest
 from conans.test.utils.tools import TestClient, TestServer, TestRequester
 from conans.test.utils.test_files import hello_source_files, temp_folder,\
@@ -7,7 +8,7 @@ import os
 from conans.paths import CONAN_MANIFEST, EXPORT_TGZ_NAME, CONANINFO
 import platform
 import stat
-from conans.util.files import save, mkdir
+from conans.util.files import load, mkdir, save
 from conans.model.ref import ConanFileReference, PackageReference
 from conans.model.manifest import FileTreeManifest
 from conans.test.utils.test_files import uncompress_packaged_files
@@ -336,3 +337,75 @@ class TestConan(ConanFile):
         self.client.run('upload %s --force' % str(self.conan_ref))
         self.assertIn("Uploading %s" % str(self.conan_ref),
                       self.client.user_io.out)
+
+    def upload_json_test(self):
+        conanfile = """
+from conans import ConanFile
+
+class TestConan(ConanFile):
+    name = "test"
+    version = "0.1"
+
+    def package(self):
+        self.copy("mylib.so", dst="lib")
+"""
+
+        client = self._get_client()
+        client.save({"conanfile.py": conanfile,
+                     "mylib.so": ""})
+        client.run("create . danimtb/testing")
+
+        # Test conflict parameter error
+        error = client.run("upload test/0.1@danimtb/* --all -p ewvfw --json upload.json",
+                           ignore_error=True)
+        self.assertTrue(error)
+        json_path = os.path.join(client.current_folder, "upload.json")
+        self.assertTrue(os.path.exists(json_path))
+        json_content = load(json_path)
+        output = json.loads(json_content)
+        self.assertTrue(output["error"])
+        self.assertEqual(0, len(output["uploaded"]))
+
+        # Test invalid reference error
+        error = client.run("upload fake/0.1@danimtb/testing --all --json upload.json",
+                           ignore_error=True)
+        self.assertTrue(error)
+        json_path = os.path.join(client.current_folder, "upload.json")
+        self.assertTrue(os.path.exists(json_path))
+        json_content = load(json_path)
+        output = json.loads(json_content)
+        self.assertTrue(output["error"])
+        self.assertEqual(0, len(output["uploaded"]))
+
+        # Test normal upload
+        client.run("upload test/0.1@danimtb/testing --all --json upload.json")
+        self.assertTrue(os.path.exists(json_path))
+        json_content = load(json_path)
+        output = json.loads(json_content)
+        output_expected = {"error": False,
+                           "uploaded": [
+                               {
+                                   "recipe": {
+                                       "id": "test/0.1@danimtb/testing",
+                                       "remote_url": "unknown",
+                                       "remote_name": "default",
+                                       "time": "unknown"
+                                   },
+                                   "packages": [
+                                       {
+                                           "id": "5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9",
+                                           "time": "unknown"
+                                       }
+                                   ]
+                               }
+                           ]}
+        self.assertEqual(output_expected["error"], output["error"])
+        self.assertEqual(len(output_expected["uploaded"]), len(output["uploaded"]))
+
+        for i, item in enumerate(output["uploaded"]):
+            self.assertEqual(output_expected["uploaded"][i]["recipe"]["id"], item["recipe"]["id"])
+            self.assertEqual(output_expected["uploaded"][i]["recipe"]["remote_name"],
+                             item["recipe"]["remote_name"])
+            for j, subitem in enumerate(item["packages"]):
+                self.assertEqual(output_expected["uploaded"][i]["packages"][j]["id"],
+                                 subitem["id"])
diff --git a/conans/test/download_test.py b/conans/test/download_test.py
index 6b59b17dd..eec9147c2 100644
--- a/conans/test/download_test.py
+++ b/conans/test/download_test.py
@@ -1,6 +1,6 @@
 import unittest
 
-from conans.client.action_recorder import ActionRecorder
+from conans.client.recorder.action_recorder import ActionRecorder
 from conans.client.proxy import ConanProxy
 from conans.errors import NotFoundException, ConanException
 from conans.model.ref import ConanFileReference, PackageReference
diff --git a/conans/test/util/action_recorder_test.py b/conans/test/functional/action_recorder_test.py
similarity index 96%
rename from conans/test/util/action_recorder_test.py
rename to conans/test/functional/action_recorder_test.py
index ac85817b5..8519cab4b 100644
--- a/conans/test/util/action_recorder_test.py
+++ b/conans/test/functional/action_recorder_test.py
@@ -1,7 +1,7 @@
 import unittest
 
-from conans.client.action_recorder import (ActionRecorder, INSTALL_ERROR_MISSING,
-                                           INSTALL_ERROR_NETWORK)
+from conans.client.recorder.action_recorder import (ActionRecorder, INSTALL_ERROR_MISSING,
+                                                    INSTALL_ERROR_NETWORK)
 from conans.model.ref import ConanFileReference, PackageReference
 
 
diff --git a/conans/test/functional/upload_recorder_test.py b/conans/test/functional/upload_recorder_test.py
new file mode 100644
index 000000000..31702ab59
--- /dev/null
+++ b/conans/test/functional/upload_recorder_test.py
@@ -0,0 +1,119 @@
+import unittest
+
+from datetime import datetime
+from conans.client.recorder.upload_recoder import UploadRecoder
+
+
+class UploadRecorderTest(unittest.TestCase):
+
+    def setUp(self):
+        self.recorder = UploadRecoder()
+
+    def empty_test(self):
+        info = self.recorder.get_info()
+        expected_result = {'error': False, 'uploaded': []}
+        self.assertEqual(expected_result, info)
+
+    def sequential_test(self):
+        self.recorder.add_recipe("fake/0.1@user/channel", "my_remote", "https://fake_url.com")
+        self.recorder.add_package("fake/0.1@user/channel", "fake_package_id")
+        self.recorder.add_recipe("fakefake/0.1@user/channel", "my_remote2", "https://fake_url2.com")
+        self.recorder.add_package("fakefake/0.1@user/channel", "fakefake_package_id1")
+        self.recorder.add_package("fakefake/0.1@user/channel", "fakefake_package_id2")
+        info = self.recorder.get_info()
+        expected_result_without_time = {
+                                           "error": False,
+                                           "uploaded": [
+                                               {
+                                                   "recipe": {
+                                                       "id": "fake/0.1@user/channel",
+                                                       "remote_name": "my_remote",
+                                                       "remote_url": "https://fake_url.com"
+                                                       },
+                                                   "packages": [
+                                                       {
+                                                           "id": "fake_package_id"
+                                                       }
+                                                   ]
+                                               },
+                                               {
+                                                   "recipe": {
+                                                       "id": "fakefake/0.1@user/channel",
+                                                       "remote_name": "my_remote2",
+                                                       "remote_url": "https://fake_url2.com"
+                                                   },
+                                                   "packages": [
+                                                       {
+                                                           "id": "fakefake_package_id1"
+                                                       },
+                                                       {
+                                                           "id": "fakefake_package_id2"
+                                                       }
+                                                   ]
+                                               }
+                                           ]
+                                      }
+
+        self._check_result(expected_result_without_time, info)
+
+    def unordered_test(self):
+        self.recorder.add_recipe("fake1/0.1@user/channel", "my_remote1", "https://fake_url1.com")
+        self.recorder.add_recipe("fake2/0.1@user/channel", "my_remote2", "https://fake_url2.com")
+        self.recorder.add_recipe("fake3/0.1@user/channel", "my_remote3", "https://fake_url3.com")
+        self.recorder.add_package("fake1/0.1@user/channel", "fake1_package_id1")
+        self.recorder.add_package("fake2/0.1@user/channel", "fake2_package_id1")
+        self.recorder.add_package("fake2/0.1@user/channel", "fake2_package_id2")
+        info = self.recorder.get_info()
+        expected_result_without_time = {
+            "error": False,
+            "uploaded": [
+                {
+                    "recipe": {
+                        "id": "fake1/0.1@user/channel",
+                        "remote_name": "my_remote1",
+                        "remote_url": "https://fake_url1.com"
+                    },
+                    "packages": [
+                        {
+                            "id": "fake1_package_id1"
+                        }
+                    ]
+                },
+                {
+                    "recipe": {
+                        "id": "fake2/0.1@user/channel",
+                        "remote_name": "my_remote2",
+                        "remote_url": "https://fake_url2.com"
+                    },
+                    "packages": [
+                        {
+                            "id": "fake2_package_id1"
+                        },
+                        {
+                            "id": "fake2_package_id2"
+                        }
+                    ]
+                },
+                {
+                    "recipe": {
+                        "id": "fake3/0.1@user/channel",
+                        "remote_name": "my_remote3",
+                        "remote_url": "https://fake_url3.com"
+                    },
+                    "packages": [
+                    ]
+                }
+            ]
+        }
+
+        self._check_result(expected_result_without_time, info)
+
+    def _check_result(self, expeceted, result):
+        for i, item in enumerate(result["uploaded"]):
+            assert item["recipe"]["time"]
+            del result["uploaded"][i]["recipe"]["time"]
+
+            for j, package in enumerate(item["packages"]):
+                assert package["time"], datetime
+                del result["uploaded"][i]["packages"][j]["time"]
+        self.assertEqual(expeceted, result)
diff --git a/conans/test/integration/conan_env_test.py b/conans/test/integration/conan_env_test.py
index 1409daba1..b09a7caf4 100644
--- a/conans/test/integration/conan_env_test.py
+++ b/conans/test/integration/conan_env_test.py
@@ -626,6 +626,54 @@ class Hello2Conan(ConanFile):
         self.assertInSep("VAR2=>24:23*", client.user_io.out)
         self.assertInSep("VAR3=>bestvalue*", client.user_io.out)
 
+    def mix_path_case_test(self):
+        client = TestClient()
+        conanfile = """
+from conans import ConanFile
+class LibConan(ConanFile):
+    name = "libB"
+    version = "1.0"
+
+    def package_info(self):
+        self.env_info.path = ["path_from_B"]
+"""
+        client.save({"conanfile.py": conanfile})
+        client.run("create . user/channel")
+
+        conanfile = """
+from conans import ConanFile
+class LibConan(ConanFile):
+    name = "libA"
+    version = "1.0"
+    requires = "libB/1.0@user/channel"
+
+    def package_info(self):
+        self.env_info.PATH.extend(["path_from_A"])
+"""
+        client.save({"conanfile.py": conanfile})
+        client.run("create . user/channel")
+
+        conanfile = """
+[requires]
+libA/1.0@user/channel
+[generators]
+virtualenv
+"""
+        client.save({"conanfile.txt": conanfile}, clean_first=True)
+        client.run("install .")
+        info = load(os.path.join(client.current_folder, "conanbuildinfo.txt"))
+        info = info.replace("\r\n", "\n")
+        self.assertIn("""
+[ENV_libA]
+PATH=["path_from_A"]
+[ENV_libB]
+PATH=["path_from_B"]""", info)
+        if platform.system() != "Windows":
+            activate = load(os.path.join(client.current_folder, "activate.sh"))
+            self.assertIn('PATH="path_from_A":"path_from_B":$PATH', activate)
+        else:
+            activate = load(os.path.join(client.current_folder, "activate.bat"))
+            self.assertIn('PATH=path_from_A;path_from_B;%PATH%', activate)
 
     def check_conaninfo_completion_test(self):
         """