Repository: conan-io/conan

Issue:
AutoToolsBuildEnv needs to be more flexible with "host"
To help us debug your issue please explain:

- [ ] I've read the [CONTRIBUTING guide](https://raw.githubusercontent.com/conan-io/conan/develop/.github/CONTRIBUTING.md).
- [ ] I've specified the Conan version, operating system version and any tool that can be relevant.
- [ ] I've explained the steps to reproduce the error or the motivation/use case of the question/suggestion.

I found a couple problems with the `get_gnu_triplet()` method of `oss.py`:
```py
# Calculate the arch
machine = {"x86": "i686" if os != "Linux" else "x86",
           "x86_64": "x86_64",
           "armv6": "arm",
           "armv7": "arm",
           "armv7s": "arm",
           "armv7k": "arm",
           "armv7hf": "arm",
           "armv8": "aarch64"}.get(arch, None)
if machine is None:
    raise ConanException("Unknown '%s' machine, Conan doesn't know how to "
                         "translate it to the GNU triplet, please report at "
                         " https://github.com/conan-io/conan/issues" % arch)
```

First, the exception being raised is swallowed by `AutoBuildToolsBuildEnv._get_host_build_target_flags():

```
try:
    build = get_gnu_triplet(os_detected, arch_detected, self._compiler)
except ConanException:
    build = None
try:
    host = get_gnu_triplet(self._os, self._arch, self._compiler)
except ConanException:
    host = None
```

A warning would be great, instead of just silently building apps very badly.

Second, any way to make that a little more flexible instead of hardcoding architectures in there? I ran into this after adding `armv5te` to my `settings.yml` file and now I have two options: change all of my recipes (and fork open source recipes) to check for `arch==armv5te` and provide the `host` parameter explicitly or wait for this bug to get fixed. Neither solution is good at all.

Maybe a replacement could look something more like this:

```
# Calculate the arch
if 'x86' == arch:
    machine = "i686" if os != "Linux" else "x86"
elif arch == 'armv8':
    machine = 'aarch64'
elif arch.startswith('arm'):
    machine = 'arm'
else:
    machine = arch
```

This should work reasonably well so long as you stay on top of adding new ARM architectures in the future.


Discussion:

Patch:
diff --git a/conans/client/build/autotools_environment.py b/conans/client/build/autotools_environment.py
index 12f66ae69..ccb7ab655 100644
--- a/conans/client/build/autotools_environment.py
+++ b/conans/client/build/autotools_environment.py
@@ -84,11 +84,13 @@ class AutoToolsBuildEnvironment(object):
 
         try:
             build = get_gnu_triplet(os_detected, arch_detected, self._compiler)
-        except ConanException:
+        except ConanException as exc:
+            self._conanfile.output.warn(str(exc))
             build = None
         try:
             host = get_gnu_triplet(self._os, self._arch, self._compiler)
-        except ConanException:
+        except ConanException as exc:
+            self._conanfile.output.warn(str(exc))
             host = None
         return build, host, None
 
diff --git a/conans/client/client_cache.py b/conans/client/client_cache.py
index ea4344d6b..cc73646f4 100644
--- a/conans/client/client_cache.py
+++ b/conans/client/client_cache.py
@@ -24,6 +24,7 @@ CONAN_SETTINGS = "settings.yml"
 LOCALDB = ".conan.db"
 REGISTRY = "registry.txt"
 PROFILES_FOLDER = "profiles"
+PLUGINS_FOLDER = "plugins"
 
 # Client certificates
 CLIENT_CERT = "client.crt"
@@ -141,6 +142,12 @@ class ClientCache(SimplePaths):
         else:
             return join(self.conan_folder, PROFILES_FOLDER,
                         self.conan_config.default_profile)
+    @property
+    def plugins_path(self):
+        """
+        :return: Plugins folder in client cache
+        """
+        return join(self.conan_folder, PLUGINS_FOLDER)
 
     @property
     def default_profile(self):
@@ -189,6 +196,15 @@ class ClientCache(SimplePaths):
             self._settings = settings
         return self._settings
 
+    @property
+    def plugins(self):
+        """Returns a list of plugins inside the plugins folder"""
+        plugins = []
+        for plugin_name in os.listdir(self.plugins_path):
+            if os.path.isfile(plugin_name) and plugin_name.endswith(".py"):
+                plugins.append(plugin_name[:-3])
+        return plugins
+
     def conan_packages(self, conan_reference):
         """ Returns a list of package_id from a local cache package folder """
         assert isinstance(conan_reference, ConanFileReference)
diff --git a/conans/client/cmd/download.py b/conans/client/cmd/download.py
index 732de1c3d..381294fb1 100644
--- a/conans/client/cmd/download.py
+++ b/conans/client/cmd/download.py
@@ -7,7 +7,7 @@ from conans.client.source import complete_recipe_sources
 
 
 def download(reference, package_ids, remote_name, recipe, registry, remote_manager,
-             client_cache, out, recorder, loader):
+             client_cache, out, recorder, loader, plugin_manager):
 
     assert(isinstance(reference, ConanFileReference))
     output = ScopedOutput(str(reference), out)
@@ -16,32 +16,33 @@ def download(reference, package_ids, remote_name, recipe, registry, remote_manag
     if not package:  # Search the reference first, and raise if it doesn't exist
         raise ConanException("'%s' not found in remote" % str(reference))
 
+    plugin_manager.execute("pre_download", reference=reference, remote=remote)
     # First of all download package recipe
     remote_manager.get_recipe(reference, remote)
     registry.set_ref(reference, remote.name)
-
-    if recipe:
-        return
-
-    # Download the sources too, don't be lazy
     conan_file_path = client_cache.conanfile(reference)
     conanfile = loader.load_class(conan_file_path)
-    complete_recipe_sources(remote_manager, client_cache, registry,
-                            conanfile, reference)
-
-    if package_ids:
-        _download_binaries(reference, package_ids, client_cache, remote_manager,
-                           remote, output, recorder, loader)
-    else:
-        output.info("Getting the complete package list "
-                    "from '%s'..." % str(reference))
-        packages_props = remote_manager.search_packages(remote, reference, None)
-        if not packages_props:
-            output = ScopedOutput(str(reference), out)
-            output.warn("No remote binary packages found in remote")
+
+    if not recipe:
+        # Download the sources too, don't be lazy
+        complete_recipe_sources(remote_manager, client_cache, registry,
+                                conanfile, reference)
+
+        if package_ids:
+            _download_binaries(reference, package_ids, client_cache, remote_manager,
+                               remote, output, recorder, loader)
         else:
-            _download_binaries(reference, list(packages_props.keys()), client_cache,
-                               remote_manager, remote, output, recorder, loader)
+            output.info("Getting the complete package list "
+                        "from '%s'..." % str(reference))
+            packages_props = remote_manager.search_packages(remote, reference, None)
+            if not packages_props:
+                output = ScopedOutput(str(reference), out)
+                output.warn("No remote binary packages found in remote")
+            else:
+                _download_binaries(reference, list(packages_props.keys()), client_cache,
+                                   remote_manager, remote, output, recorder, loader)
+    plugin_manager.execute("post_download", conanfile_path=conan_file_path, reference=reference,
+                           remote=remote)
 
 
 def _download_binaries(reference, package_ids, client_cache, remote_manager, remote, output,
diff --git a/conans/client/cmd/export.py b/conans/client/cmd/export.py
index 2d9852b00..410d171c5 100644
--- a/conans/client/cmd/export.py
+++ b/conans/client/cmd/export.py
@@ -13,6 +13,7 @@ from conans.paths import CONAN_MANIFEST, CONANFILE
 from conans.util.files import save, rmdir, is_dirty, set_dirty, mkdir, load
 from conans.util.log import logger
 from conans.search.search import search_recipes
+from conans.client.plugin_manager import PluginManager
 
 
 def export_alias(reference, target_reference, client_cache):
@@ -33,23 +34,20 @@ class AliasConanfile(ConanFile):
     digest.save(export_path)
 
 
-def cmd_export(conanfile_path, conanfile, reference, keep_source, output, client_cache):
+def cmd_export(conanfile_path, conanfile, reference, keep_source, output, client_cache,
+               plugin_manager):
     """ Export the recipe
     param conanfile_path: the original source directory of the user containing a
                        conanfile.py
     """
+    plugin_manager.execute("pre_export", conanfile=conanfile, conanfile_path=conanfile_path,
+                           reference=reference)
     logger.debug("Exporting %s" % conanfile_path)
     output.highlight("Exporting package recipe")
 
     conan_linter(conanfile_path, output)
-    for field in ["url", "license", "description"]:
-        field_value = getattr(conanfile, field, None)
-        if not field_value:
-            output.warn("Conanfile doesn't have '%s'.\n"
-                        "It is recommended to add it as attribute" % field)
-
-    conan_ref_str = str(reference)
     # Maybe a platform check could be added, but depends on disk partition
+    conan_ref_str = str(reference)
     refs = search_recipes(client_cache, conan_ref_str, ignorecase=True)
     if refs and reference not in refs:
         raise ConanException("Cannot export package with same name but different case\n"
@@ -59,6 +57,9 @@ def cmd_export(conanfile_path, conanfile, reference, keep_source, output, client
     with client_cache.conanfile_write_lock(reference):
         _export_conanfile(conanfile_path, conanfile.output, client_cache, conanfile, reference,
                           keep_source)
+    conanfile_cache_path = client_cache.conanfile(reference)
+    plugin_manager.execute("post_export", conanfile=conanfile, conanfile_path=conanfile_cache_path,
+                           reference=reference)
 
 
 def _capture_export_scm_data(conanfile, conanfile_dir, destination_folder, output, paths, conan_ref):
diff --git a/conans/client/cmd/uploader.py b/conans/client/cmd/uploader.py
index 2bb858fe2..0ebf746f3 100644
--- a/conans/client/cmd/uploader.py
+++ b/conans/client/cmd/uploader.py
@@ -1,5 +1,6 @@
 import time
 
+from conans import load
 from conans.errors import ConanException, NotFoundException
 from conans.model.ref import PackageReference, ConanFileReference
 from conans.util.log import logger
@@ -24,12 +25,13 @@ UPLOAD_POLICY_SKIP = "skip-upload"
 
 class CmdUpload(object):
 
-    def __init__(self, client_cache, user_io, remote_manager, registry, loader):
+    def __init__(self, client_cache, user_io, remote_manager, registry, loader, plugin_manager):
         self._client_cache = client_cache
         self._user_io = user_io
         self._remote_manager = remote_manager
         self._registry = registry
         self._loader = loader
+        self._plugin_manager = plugin_manager
 
     def upload(self, recorder, reference_or_pattern, package_id=None, all_packages=None,
                confirm=False, retry=0, retry_wait=0, integrity_check=False, policy=None,
@@ -88,6 +90,10 @@ class CmdUpload(object):
         else:  # Or use the default otherwise
             upload_remote = self._registry.default_remote
 
+        conanfile_path = self._client_cache.conanfile(conan_ref)
+        self._plugin_manager.execute("pre_upload", conanfile_path=conanfile_path,
+                                     reference=conan_ref, remote=upload_remote)
+
         if policy != UPLOAD_POLICY_FORCE:
             self._check_recipe_date(conan_ref, upload_remote)
 
@@ -112,6 +118,9 @@ class CmdUpload(object):
 
         if not defined_remote and policy != UPLOAD_POLICY_SKIP:
             self._registry.set_ref(conan_ref, upload_remote.name)
+        
+        self._plugin_manager.execute("post_upload", conanfile_path=conanfile_path,
+                                     reference=conan_ref, remote=upload_remote)
 
     def _upload_recipe(self, conan_reference, retry, retry_wait, policy, remote):
         conan_file_path = self._client_cache.conanfile(conan_reference)
diff --git a/conans/client/command.py b/conans/client/command.py
index 6556d0353..3154542e9 100644
--- a/conans/client/command.py
+++ b/conans/client/command.py
@@ -401,7 +401,10 @@ class Command(object):
             try:
                 key, value = args.item.split("=", 1)
             except ValueError:
-                raise ConanException("Please specify 'key=value'")
+                if "plugins." in args.item:
+                    key, value = args.item.split("=", 1)[0], None
+                else:
+                    raise ConanException("Please specify 'key=value'")
             return self._conan.config_set(key, value)
         elif args.subcommand == "get":
             return self._conan.config_get(args.item)
diff --git a/conans/client/conan_api.py b/conans/client/conan_api.py
index d96725350..68a15c0e3 100644
--- a/conans/client/conan_api.py
+++ b/conans/client/conan_api.py
@@ -6,6 +6,7 @@ import requests
 import conans
 from conans import __version__ as client_version, tools
 from conans.client.cmd.create import create
+from conans.client.plugin_manager import PluginManager
 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
@@ -89,7 +90,6 @@ def api_method(f):
             raise
         finally:
             os.chdir(curdir)
-
     return wrapper
 
 
@@ -138,7 +138,7 @@ class ConanAPIV1(object):
 
     @staticmethod
     def instance_remote_manager(requester, client_cache, user_io, _client_version,
-                                min_server_compatible_version):
+                                min_server_compatible_version, plugin_manager):
 
         # Verify client version against remotes
         version_checker_req = VersionCheckerRequester(requester, _client_version,
@@ -154,7 +154,7 @@ class ConanAPIV1(object):
         # Wraps RestApiClient to add authentication support (same interface)
         auth_manager = ConanApiAuthManager(rest_api_client, user_io, localdb)
         # Handle remote connections
-        remote_manager = RemoteManager(client_cache, auth_manager, user_io.out)
+        remote_manager = RemoteManager(client_cache, auth_manager, user_io.out, plugin_manager)
         return localdb, rest_api_client, remote_manager
 
     @staticmethod
@@ -189,13 +189,18 @@ class ConanAPIV1(object):
             # Adjust CONAN_LOGGING_LEVEL with the env readed
             conans.util.log.logger = configure_logger()
 
+            # Create Plugin Manager
+            plugin_manager = PluginManager(client_cache.plugins_path,
+                                           get_env("CONAN_PLUGINS", list()), user_io.out)
+
             # Get the new command instance after migrations have been done
             requester = get_basic_requester(client_cache)
             _, _, remote_manager = ConanAPIV1.instance_remote_manager(
                 requester,
                 client_cache, user_io,
                 Version(client_version),
-                Version(MIN_SERVER_COMPATIBLE_VERSION))
+                Version(MIN_SERVER_COMPATIBLE_VERSION),
+                plugin_manager)
 
             # Adjust global tool variables
             set_global_instances(out, requester)
@@ -204,11 +209,12 @@ class ConanAPIV1(object):
             if interactive is None:
                 interactive = not get_env("CONAN_NON_INTERACTIVE", False)
             conan = ConanAPIV1(client_cache, user_io, get_conan_runner(), remote_manager,
-                               interactive=interactive)
+                               plugin_manager, interactive=interactive)
 
         return conan, client_cache, user_io
 
-    def __init__(self, client_cache, user_io, runner, remote_manager, interactive=True):
+    def __init__(self, client_cache, user_io, runner, remote_manager, plugin_manager,
+                 interactive=True):
         assert isinstance(user_io, UserIO)
         assert isinstance(client_cache, ClientCache)
         self._client_cache = client_cache
@@ -225,12 +231,13 @@ class ConanAPIV1(object):
         self._loader = ConanFileLoader(self._runner, self._user_io.out, python_requires)
         self._graph_manager = GraphManager(self._user_io.out, self._client_cache, self._registry,
                                            self._remote_manager, self._loader, self._proxy, resolver)
+        self._plugin_manager = plugin_manager
 
     def _init_manager(self, action_recorder):
         """Every api call gets a new recorder and new manager"""
         return ConanManager(self._client_cache, self._user_io, self._runner,
                             self._remote_manager, action_recorder, self._registry,
-                            self._graph_manager)
+                            self._graph_manager, self._plugin_manager)
 
     @api_method
     def new(self, name, header=False, pure_c=False, test=False, exports_sources=False, bare=False,
@@ -308,7 +315,7 @@ class ConanAPIV1(object):
             # Forcing an export!
             if not not_export:
                 cmd_export(conanfile_path, conanfile, reference, keep_source, self._user_io.out,
-                           self._client_cache)
+                           self._client_cache, self._plugin_manager)
 
             if build_modes is None:  # Not specified, force build the tested library
                 build_modes = [conanfile.name]
@@ -373,7 +380,8 @@ class ConanAPIV1(object):
             profile = read_conaninfo_profile(install_folder)
 
         reference, conanfile = self._loader.load_export(conanfile_path, name, version, user, channel)
-        cmd_export(conanfile_path, conanfile, reference, False, self._user_io.out, self._client_cache)
+        cmd_export(conanfile_path, conanfile, reference, False, self._user_io.out,
+                   self._client_cache, self._plugin_manager)
 
         recorder = ActionRecorder()
         manager = self._init_manager(recorder)
@@ -389,7 +397,8 @@ class ConanAPIV1(object):
         conan_ref = ConanFileReference.loads(reference)
         recorder = ActionRecorder()
         download(conan_ref, package, remote_name, recipe, self._registry, self._remote_manager,
-                 self._client_cache, self._user_io.out, recorder, self._loader)
+                 self._client_cache, self._user_io.out, recorder, self._loader,
+                 self._plugin_manager)
 
     @api_method
     def install_reference(self, reference, settings=None, options=None, env=None,
@@ -628,7 +637,8 @@ class ConanAPIV1(object):
     def export(self, path, name, version, user, channel, keep_source=False, cwd=None):
         conanfile_path = _get_conanfile_path(path, cwd, py=True)
         reference, conanfile = self._loader.load_export(conanfile_path, name, version, user, channel)
-        cmd_export(conanfile_path, conanfile, reference, keep_source, self._user_io.out, self._client_cache)
+        cmd_export(conanfile_path, conanfile, reference, keep_source, self._user_io.out,
+                   self._client_cache, self._plugin_manager)
 
     @api_method
     def remove(self, pattern, query=None, packages=None, builds=None, src=False, force=False,
@@ -727,7 +737,7 @@ class ConanAPIV1(object):
 
         recorder = UploadRecorder()
         uploader = CmdUpload(self._client_cache, self._user_io, self._remote_manager,
-                             self._registry, self._loader)
+                             self._registry, self._loader, self._plugin_manager)
         try:
             uploader.upload(recorder, pattern, package, all_packages, confirm, retry,
                             retry_wait, integrity_check, policy, remote_name, query=query)
diff --git a/conans/client/conf/__init__.py b/conans/client/conf/__init__.py
index b760b0471..f6477a3ae 100644
--- a/conans/client/conf/__init__.py
+++ b/conans/client/conf/__init__.py
@@ -90,7 +90,7 @@ default_profile = %s
 compression_level = 9                 # environment CONAN_COMPRESSION_LEVEL
 sysrequires_sudo = True               # environment CONAN_SYSREQUIRES_SUDO
 request_timeout = 60                  # environment CONAN_REQUEST_TIMEOUT (seconds)
-# sysrequires_mode = enabled            # environment CONAN_SYSREQUIRES_MODE (allowed modes enabled/verify/disabled)
+# sysrequires_mode = enabled          # environment CONAN_SYSREQUIRES_MODE (allowed modes enabled/verify/disabled)
 # vs_installation_preference = Enterprise, Professional, Community, BuildTools # environment CONAN_VS_INSTALLATION_PREFERENCE
 # verbose_traceback = False           # environment CONAN_VERBOSE_TRACEBACK
 # bash_path = ""                      # environment CONAN_BASH_PATH (only windows)
@@ -137,6 +137,8 @@ path = ~/.conan/data
 # You can skip the proxy for the matching (fnmatch) urls (comma-separated)
 # no_proxy_match = *bintray.com*, https://myserver.*
 
+[plugins]  # environment CONAN_PLUGINS
+attribute_checker
 
 # Default settings now declared in the default profile
 
@@ -147,7 +149,7 @@ path = ~/.conan/data
 class ConanClientConfigParser(ConfigParser, object):
 
     def __init__(self, filename):
-        ConfigParser.__init__(self)
+        ConfigParser.__init__(self, allow_no_value=True)
         self.read(filename)
         self.filename = filename
 
@@ -198,7 +200,8 @@ class ConanClientConfigParser(ConfigParser, object):
                "CONAN_BASH_PATH": self._env_c("general.bash_path", "CONAN_BASH_PATH", None),
                "CONAN_MAKE_PROGRAM": self._env_c("general.conan_make_program", "CONAN_MAKE_PROGRAM", None),
                "CONAN_TEMP_TEST_FOLDER": self._env_c("general.temp_test_folder", "CONAN_TEMP_TEST_FOLDER", "False"),
-               "CONAN_SKIP_VS_PROJECTS_UPGRADE": self._env_c("general.skip_vs_projects_upgrade", "CONAN_SKIP_VS_PROJECTS_UPGRADE", "False")
+               "CONAN_SKIP_VS_PROJECTS_UPGRADE": self._env_c("general.skip_vs_projects_upgrade", "CONAN_SKIP_VS_PROJECTS_UPGRADE", "False"),
+               "CONAN_PLUGINS": self._env_c("plugins", "CONAN_PLUGINS", None)
                }
 
         # Filter None values
@@ -225,9 +228,14 @@ class ConanClientConfigParser(ConfigParser, object):
             raise ConanException("'%s' is not a section of conan.conf" % section_name)
         if len(tokens) == 1:
             result = []
-            for item in section:
-                result.append(" = ".join(item))
-            return "\n".join(result)
+            if section_name == "plugins":
+                for key, _ in section:
+                    result.append(key)
+                return ",".join(result)
+            else:
+                for section_item in section:
+                    result.append(" = ".join(section_item))
+                return "\n".join(result)
         else:
             key = tokens[1]
             try:
diff --git a/conans/client/installer.py b/conans/client/installer.py
index 119df2cdd..7f92f69fb 100644
--- a/conans/client/installer.py
+++ b/conans/client/installer.py
@@ -48,13 +48,14 @@ def build_id(conan_file):
 class _ConanPackageBuilder(object):
     """Builds and packages a single conan_file binary package"""
 
-    def __init__(self, conan_file, package_reference, client_cache, output):
+    def __init__(self, conan_file, package_reference, client_cache, output, plugin_manager):
         self._client_cache = client_cache
         self._conan_file = conan_file
         self._out = output
         self._package_reference = package_reference
         self._conan_ref = self._package_reference.conan
         self._skip_build = False  # If build_id()
+        self._plugin_manager = plugin_manager
 
         new_id = build_id(self._conan_file)
         self.build_reference = PackageReference(self._conan_ref, new_id) if new_id else package_reference
@@ -77,6 +78,7 @@ class _ConanPackageBuilder(object):
         export_folder = self._client_cache.export(self._conan_ref)
         export_source_folder = self._client_cache.export_sources(self._conan_ref,
                                                                  self._conan_file.short_paths)
+        conanfile_path = self._client_cache.conanfile(self._conan_ref)
 
         try:
             rmdir(self.build_folder)
@@ -89,7 +91,8 @@ class _ConanPackageBuilder(object):
         sources_pointer = self._client_cache.scm_folder(self._conan_ref)
         local_sources_path = load(sources_pointer) if os.path.exists(sources_pointer) else None
         config_source(export_folder, export_source_folder, local_sources_path, self.source_folder,
-                      self._conan_file, self._out)
+                      self._conan_file, self._out, conanfile_path, self._conan_ref,
+                      self._plugin_manager)
         self._out.info('Copying sources to build folder')
 
         if getattr(self._conan_file, 'no_copy_source', False):
@@ -137,8 +140,11 @@ class _ConanPackageBuilder(object):
         with get_env_context_manager(self._conan_file):
             install_folder = self.build_folder  # While installing, the infos goes to build folder
             pkg_id = self._conan_file.info.package_id()
+            conanfile_path = self._client_cache.conanfile(self._conan_ref)
+
             create_package(self._conan_file, pkg_id, source_folder, self.build_folder,
-                           self.package_folder, install_folder, self._out)
+                           self.package_folder, install_folder, self._out, self._plugin_manager,
+                           conanfile_path, self._conan_ref)
 
         if get_env("CONAN_READ_ONLY_CACHE", False):
             make_read_only(self.package_folder)
@@ -165,6 +171,9 @@ class _ConanPackageBuilder(object):
         try:
             # This is necessary because it is different for user projects
             # than for packages
+            self._plugin_manager.execute("pre_build", conanfile=self._conan_file,
+                                         reference=self._conan_ref,
+                                         package_id=self._package_reference.package_id)
             logger.debug("Call conanfile.build() with files in build folder: %s",
                          os.listdir(self.build_folder))
             self._out.highlight("Calling build()")
@@ -173,6 +182,9 @@ class _ConanPackageBuilder(object):
 
             self._out.success("Package '%s' built" % self._conan_file.info.package_id())
             self._out.info("Build folder %s" % self.build_folder)
+            self._plugin_manager.execute("post_build", conanfile=self._conan_file,
+                                         reference=self._conan_ref,
+                                         package_id=self._package_reference.package_id)
         except Exception as exc:
             self._out.writeln("")
             self._out.error("Package '%s' build failed" % self._conan_file.info.package_id())
@@ -244,13 +256,15 @@ 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_manager, registry, recorder, workspace):
+    def __init__(self, client_cache, output, remote_manager, registry, recorder, workspace,
+                 plugin_manager):
         self._client_cache = client_cache
         self._out = output
         self._remote_manager = remote_manager
         self._registry = registry
         self._recorder = recorder
         self._workspace = workspace
+        self._plugin_manager = plugin_manager
 
     def install(self, deps_graph, keep_build=False):
         # order by levels and separate the root node (conan_ref=None) from the rest
@@ -354,7 +368,8 @@ class ConanInstaller(object):
             output.info("Won't be built as specified by --keep-build")
 
         t1 = time.time()
-        builder = _ConanPackageBuilder(conan_file, package_ref, self._client_cache, output)
+        builder = _ConanPackageBuilder(conan_file, package_ref, self._client_cache, output,
+                                       self._plugin_manager)
 
         if skip_build:
             if not os.path.exists(builder.build_folder):
diff --git a/conans/client/manager.py b/conans/client/manager.py
index 6cedd66f2..7e28396ed 100644
--- a/conans/client/manager.py
+++ b/conans/client/manager.py
@@ -26,7 +26,7 @@ class ConanManager(object):
     business logic
     """
     def __init__(self, client_cache, user_io, runner, remote_manager,
-                 recorder, registry, graph_manager):
+                 recorder, registry, graph_manager, plugin_manager):
         assert isinstance(user_io, UserIO)
         assert isinstance(client_cache, ClientCache)
         self._client_cache = client_cache
@@ -36,6 +36,7 @@ class ConanManager(object):
         self._recorder = recorder
         self._registry = registry
         self._graph_manager = graph_manager
+        self._plugin_manager = plugin_manager
 
     def export_pkg(self, reference, source_folder, build_folder, package_folder, install_folder, profile, force):
 
@@ -70,10 +71,11 @@ class ConanManager(object):
         package_output = ScopedOutput(str(reference), self._user_io.out)
         if package_folder:
             packager.export_pkg(conanfile, pkg_id, package_folder, dest_package_folder,
-                                package_output)
+                                package_output, self._plugin_manager, conan_file_path, reference)
         else:
             packager.create_package(conanfile, pkg_id, source_folder, build_folder,
-                                    dest_package_folder, install_folder, package_output, local=True)
+                                    dest_package_folder, install_folder, package_output,
+                                    self._plugin_manager, conan_file_path, reference, local=True)
 
     def install_workspace(self, profile, workspace, remote_name, build_modes, update):
         references = [ConanFileReference(v, "root", "project", "develop") for v in workspace.root]
@@ -85,7 +87,8 @@ class ConanManager(object):
         print_graph(deps_graph, self._user_io.out)
 
         installer = ConanInstaller(self._client_cache, output, self._remote_manager,
-                                   self._registry, recorder=self._recorder, workspace=workspace)
+                                   self._registry, recorder=self._recorder, workspace=workspace,
+                                   plugin_manager=self._plugin_manager)
         installer.install(deps_graph, keep_build=False)
         workspace.generate()
 
@@ -139,7 +142,8 @@ class ConanManager(object):
             pass
 
         installer = ConanInstaller(self._client_cache, output, self._remote_manager,
-                                   self._registry, recorder=self._recorder, workspace=None)
+                                   self._registry, recorder=self._recorder, workspace=None,
+                                   plugin_manager=self._plugin_manager)
         installer.install(deps_graph, keep_build)
 
         if manifest_folder:
@@ -193,7 +197,8 @@ class ConanManager(object):
         if conanfile_folder != source_folder:
             output.info("Executing exports to: %s" % source_folder)
             _execute_export(conanfile_path, conanfile, source_folder, source_folder, output)
-        config_source_local(source_folder, conanfile, conanfile_folder, output)
+        config_source_local(source_folder, conanfile, conanfile_folder, output, conanfile_path,
+                            self._plugin_manager)
 
     def imports(self, conan_file_path, dest_folder, info_folder):
         """
@@ -214,10 +219,11 @@ class ConanManager(object):
             raise ConanException("Cannot 'conan package' to the build folder. "
                                  "--build-folder and package folder can't be the same")
         output = ScopedOutput("PROJECT", self._user_io.out)
-        conanfile = self._graph_manager.load_consumer_conanfile(conanfile_path, install_folder, output,
-                                                                deps_info_required=True)
+        conanfile = self._graph_manager.load_consumer_conanfile(conanfile_path, install_folder,
+                                                                output, deps_info_required=True)
         packager.create_package(conanfile, None, source_folder, build_folder, package_folder,
-                                install_folder, output, local=True, copy_info=True)
+                                install_folder, output, self._plugin_manager, conanfile_path, None,
+                                local=True, copy_info=True)
 
     def build(self, conanfile_path, source_folder, build_folder, package_folder, install_folder,
               test=False, should_configure=True, should_build=True, should_install=True,
@@ -259,10 +265,14 @@ class ConanManager(object):
             conan_file.source_folder = source_folder
             conan_file.package_folder = package_folder
             conan_file.install_folder = install_folder
+            self._plugin_manager.execute("pre_build", conanfile=conan_file,
+                                         conanfile_path=conanfile_path)
             with get_env_context_manager(conan_file):
                 output.highlight("Running build()")
                 with conanfile_exception_formatter(str(conan_file), "build"):
                     conan_file.build()
+                self._plugin_manager.execute("post_build", conanfile=conan_file,
+                                             conanfile_path=conanfile_path)
                 if test:
                     output.highlight("Running test()")
                     with conanfile_exception_formatter(str(conan_file), "test"):
diff --git a/conans/client/packager.py b/conans/client/packager.py
index 45bc3900f..b439afd63 100644
--- a/conans/client/packager.py
+++ b/conans/client/packager.py
@@ -11,11 +11,15 @@ from conans.client.output import ScopedOutput
 from conans.client.file_copier import FileCopier
 
 
-def export_pkg(conanfile, pkg_id, src_package_folder, package_folder, output):
+def export_pkg(conanfile, pkg_id, src_package_folder, package_folder, output, plugin_manager,
+               conanfile_path, reference):
     mkdir(package_folder)
-
+    conanfile.package_folder = src_package_folder
     output.info("Exporting to cache existing package from user folder")
     output.info("Package folder %s" % package_folder)
+    print("export_pkg", type(reference))
+    plugin_manager.execute("pre_package", conanfile=conanfile, conanfile_path=conanfile_path,
+                           reference=reference, package_id=pkg_id)
 
     copier = FileCopier(src_package_folder, package_folder)
     copier("*", symlinks=True)
@@ -28,10 +32,13 @@ def export_pkg(conanfile, pkg_id, src_package_folder, package_folder, output):
     digest = FileTreeManifest.create(package_folder)
     digest.save(package_folder)
     output.success("Package '%s' created" % pkg_id)
+    conanfile.package_folder = package_folder
+    plugin_manager.execute("post_package", conanfile=conanfile, conanfile_path=conanfile_path,
+                           reference=reference, package_id=pkg_id)
 
 
 def create_package(conanfile, pkg_id, source_folder, build_folder, package_folder, install_folder,
-                   output, local=False, copy_info=False):
+                   output, plugin_manager, conanfile_path, reference, local=False, copy_info=False):
     """ copies built artifacts, libs, headers, data, etc. from build_folder to
     package folder
     """
@@ -42,13 +49,17 @@ def create_package(conanfile, pkg_id, source_folder, build_folder, package_folde
     output.info("Package folder %s" % package_folder)
 
     try:
-        package_output = ScopedOutput("%s package()" % output.scope, output)
-        output.highlight("Calling package()")
         conanfile.package_folder = package_folder
         conanfile.source_folder = source_folder
         conanfile.install_folder = install_folder
         conanfile.build_folder = build_folder
 
+        plugin_manager.execute("pre_package", conanfile=conanfile, conanfile_path=conanfile_path,
+                               reference=reference, package_id=pkg_id)
+
+        package_output = ScopedOutput("%s package()" % output.scope, output)
+        output.highlight("Calling package()")
+
         def recipe_has(attribute):
             return attribute in conanfile.__class__.__dict__
 
@@ -84,6 +95,8 @@ def create_package(conanfile, pkg_id, source_folder, build_folder, package_folde
     _create_aux_files(install_folder, package_folder, conanfile, copy_info)
     pkg_id = pkg_id or os.path.basename(package_folder)
     output.success("Package '%s' created" % pkg_id)
+    plugin_manager.execute("post_package", conanfile=conanfile, conanfile_path=conanfile_path,
+                           reference=reference, package_id=pkg_id)
 
 
 def _create_aux_files(install_folder, package_folder, conanfile, copy_info):
diff --git a/conans/client/plugin_manager.py b/conans/client/plugin_manager.py
new file mode 100644
index 000000000..5b0ac98ae
--- /dev/null
+++ b/conans/client/plugin_manager.py
@@ -0,0 +1,120 @@
+import traceback
+import os
+import sys
+import uuid
+from collections import defaultdict
+
+from conans.client.output import ScopedOutput
+from conans.errors import ConanException, NotFoundException
+from conans.tools import chdir
+from conans.util.files import save
+
+
+attribute_checker_plugin = """
+def pre_export(output, conanfile, conanfile_path, reference, **kwargs):
+    # Check basic meta-data
+    for field in ["url", "license", "description"]:
+        field_value = getattr(conanfile, field, None)
+        if not field_value:
+            output.warn("Conanfile doesn't have '%s'. It is recommended to add it as attribute"
+                        % field)
+"""
+
+valid_plugin_methods = ["pre_export", "post_export",
+                        "pre_source", "post_source",
+                        "pre_build", "post_build",
+                        "pre_package", "post_package",
+                        "pre_upload", "post_upload",
+                        "pre_upload_recipe", "post_upload_recipe",
+                        "pre_upload_package", "post_upload_package",
+                        "pre_download", "post_download",
+                        "pre_download_recipe", "post_download_recipe",
+                        "pre_download_package", "post_download_package"
+                        ]
+
+
+class PluginManager(object):
+
+    def __init__(self, plugins_folder, plugin_names, output):
+        self._plugins_folder = plugins_folder
+        self._plugin_names = plugin_names
+        self.plugins = defaultdict(list)
+        self.output = output
+
+    def create_default_plugins(self):
+        attribute_checker_path = os.path.join(self._plugins_folder, "attribute_checker.py")
+        save(attribute_checker_path, attribute_checker_plugin)
+
+    def execute(self, method_name, **kwargs):
+        if not os.path.exists(os.path.join(self._plugins_folder, "attribute_checker.py")):
+            self.create_default_plugins()
+        if not self.plugins:
+            self.load_plugins()
+
+        assert method_name in valid_plugin_methods
+        for name, method in self.plugins[method_name]:
+            try:
+                output = ScopedOutput("[PLUGIN - %s] %s()" % (name, method_name), self.output)
+                method(output, **kwargs)
+            except Exception as e:
+                raise ConanException("[PLUGIN - %s] %s(): %s\n%s" % (name, method_name, str(e),
+                                                                     traceback.format_exc()))
+
+    def load_plugins(self):
+        for name in self._plugin_names:
+            self.load_plugin(name)
+
+    def load_plugin(self, plugin_name):
+        filename = "%s.py" % plugin_name
+        plugin_path = os.path.join(self._plugins_folder, filename)
+        try:
+            plugin = PluginManager._load_module_from_file(plugin_path)
+            for method in valid_plugin_methods:
+                plugin_method = getattr(plugin, method, None)
+                if plugin_method:
+                    self.plugins[method].append((plugin_name, plugin_method))
+        except NotFoundException:
+            self.output.warn("Plugin '%s' not found in %s folder. Please remove plugin "
+                             "from conan.conf or include it inside the plugins folder."
+                             % (filename, self._plugins_folder))
+        except Exception as e:
+            raise ConanException("Error loading plugin '%s': %s" % (plugin_path, str(e)))
+
+    @staticmethod
+    def _load_module_from_file(plugin_path):
+        """ From a given path, obtain the in memory python import module
+        """
+        if not os.path.exists(plugin_path):
+            raise NotFoundException
+        filename = os.path.splitext(os.path.basename(plugin_path))[0]
+        current_dir = os.path.dirname(plugin_path)
+
+        try:
+            sys.path.append(current_dir)
+            old_modules = list(sys.modules.keys())
+            with chdir(current_dir):
+                sys.dont_write_bytecode = True
+                loaded = __import__(filename)
+            # Put all imported files under a new package name
+            module_id = uuid.uuid1()
+            added_modules = set(sys.modules).difference(old_modules)
+            for added in added_modules:
+                module = sys.modules[added]
+                if module:
+                    try:
+                        folder = os.path.dirname(module.__file__)
+                    except AttributeError:  # some module doesn't have __file__
+                        pass
+                    else:
+                        if folder.startswith(current_dir):
+                            module = sys.modules.pop(added)
+                            sys.modules["%s.%s" % (module_id, added)] = module
+        except Exception:
+            trace = traceback.format_exc().split('\n')
+            raise ConanException("Unable to load Plugin in %s\n%s" % (plugin_path,
+                                                                      '\n'.join(trace[3:])))
+        finally:
+            sys.dont_write_bytecode = False
+            sys.path.pop()
+
+        return loaded
diff --git a/conans/client/remote_manager.py b/conans/client/remote_manager.py
index a5cde0def..51c0f896c 100644
--- a/conans/client/remote_manager.py
+++ b/conans/client/remote_manager.py
@@ -34,12 +34,16 @@ from conans.client.cmd.uploader import UPLOAD_POLICY_SKIP
 class RemoteManager(object):
     """ Will handle the remotes to get recipes, packages etc """
 
-    def __init__(self, client_cache, auth_manager, output):
+    def __init__(self, client_cache, auth_manager, output, plugin_manager):
         self._client_cache = client_cache
         self._output = output
         self._auth_manager = auth_manager
+        self._plugin_manager = plugin_manager
 
     def upload_recipe(self, conan_reference, remote, retry, retry_wait, policy=None):
+        conanfile_path = self._client_cache.conanfile(conan_reference)
+        self._plugin_manager.execute("pre_upload_recipe", conanfile_path=conanfile_path,
+                                     reference=conan_reference, remote=remote)
         t1 = time.time()
         export_folder = self._client_cache.export(conan_reference)
 
@@ -71,6 +75,8 @@ class RemoteManager(object):
         else:
             msg = "Recipe is up to date, upload skipped"
         self._output.info(msg)
+        self._plugin_manager.execute("post_upload_recipe", conanfile_path=conanfile_path,
+                                     reference=conan_reference, remote=remote)
         return new_ref
 
     def _package_integrity_check(self, package_reference, files, package_folder):
@@ -102,6 +108,11 @@ class RemoteManager(object):
     def upload_package(self, package_reference, remote, retry, retry_wait,
                        integrity_check=False, policy=None):
         """Will upload the package to the first remote"""
+        conanfile_path = self._client_cache.conanfile(package_reference.conan)
+        self._plugin_manager.execute("pre_upload_package", conanfile_path=conanfile_path,
+                                     reference=package_reference.conan,
+                                     package_id=package_reference.package_id,
+                                     remote=remote)
         t1 = time.time()
         # existing package, will use short paths if defined
         package_folder = self._client_cache.package(package_reference, short_paths=None)
@@ -143,6 +154,9 @@ class RemoteManager(object):
             self._output.rewrite_line("Package is up to date, upload skipped")
             self._output.writeln("")
 
+        self._plugin_manager.execute("post_upload_package", conanfile_path=conanfile_path,
+                                     reference=package_reference.conan,
+                                     package_id=package_reference.package_id, remote=remote)
         return tmp
 
     def get_conan_manifest(self, conan_reference, remote):
@@ -175,6 +189,7 @@ class RemoteManager(object):
         Will iterate the remotes to find the conans unless remote was specified
 
         returns (dict relative_filepath:abs_path , remote_name)"""
+        self._plugin_manager.execute("pre_download_recipe", reference=conan_reference, remote=remote)
         dest_folder = self._client_cache.export(conan_reference)
         rmdir(dest_folder)
 
@@ -188,6 +203,9 @@ class RemoteManager(object):
         # Make sure that the source dir is deleted
         rm_conandir(self._client_cache.source(conan_reference))
         touch_folder(dest_folder)
+        conanfile_path = self._client_cache.conanfile(conan_reference)
+        self._plugin_manager.execute("post_download_recipe", conanfile_path=conanfile_path,
+                                     reference=conan_reference, remote=remote)
         return conan_reference
 
     def get_recipe_sources(self, conan_reference, export_folder, export_sources_folder, remote):
@@ -212,6 +230,10 @@ class RemoteManager(object):
 
     def get_package(self, package_reference, dest_folder, remote, output, recorder):
         package_id = package_reference.package_id
+        conanfile_path = self._client_cache.conanfile(package_reference.conan)
+        self._plugin_manager.execute("pre_download_package", conanfile_path=conanfile_path,
+                                     reference=package_reference.conan, package_id=package_id,
+                                     remote=remote)
         output.info("Retrieving package %s from remote '%s' " % (package_id, remote.name))
         rm_conandir(dest_folder)  # Remove first the destination folder
         t1 = time.time()
@@ -238,6 +260,9 @@ class RemoteManager(object):
                 raise ConanException("%s\n\nCouldn't remove folder '%s', might be busy or open. Close any app "
                                      "using it, and retry" % (str(e), dest_folder))
             raise
+        self._plugin_manager.execute("post_download_package", conanfile_path=conanfile_path,
+                                     reference=package_reference.conan, package_id=package_id,
+                                     remote=remote)
 
     def search_recipes(self, remote, pattern=None, ignorecase=True):
         """
diff --git a/conans/client/source.py b/conans/client/source.py
index bf2a7e0ce..2d58d438d 100644
--- a/conans/client/source.py
+++ b/conans/client/source.py
@@ -7,6 +7,7 @@ from conans import tools
 from conans.errors import ConanException, conanfile_exception_formatter, \
     ConanExceptionInUserConanfileMethod
 from conans.model.conan_file import get_env_context_manager
+from conans.model.ref import ConanFileReference
 from conans.model.scm import SCM
 from conans.paths import EXPORT_TGZ_NAME, EXPORT_SOURCES_TGZ_NAME, CONANFILE, CONAN_MANIFEST
 from conans.util.files import rmdir, set_dirty, is_dirty, clean_dirty, mkdir
@@ -85,7 +86,7 @@ def get_scm_data(conanfile):
 
 
 def config_source(export_folder, export_source_folder, local_sources_path, src_folder,
-                  conanfile, output, force=False):
+                  conanfile, output, conanfile_path, reference, plugin_manager, force=False):
     """ creates src folder and retrieve, calling source() from conanfile
     the necessary source code
     """
@@ -118,7 +119,6 @@ def config_source(export_folder, export_source_folder, local_sources_path, src_f
         remove_source()
 
     if not os.path.exists(src_folder):
-        output.info('Configuring sources in %s' % src_folder)
         set_dirty(src_folder)
         mkdir(src_folder)
         os.chdir(src_folder)
@@ -128,7 +128,9 @@ def config_source(export_folder, export_source_folder, local_sources_path, src_f
                 with get_env_context_manager(conanfile):
                     conanfile.build_folder = None
                     conanfile.package_folder = None
-
+                    plugin_manager.execute("pre_source", conanfile=conanfile,
+                                           conanfile_path=conanfile_path, reference=reference)
+                    output.info('Configuring sources in %s' % src_folder)
                     scm_data = get_scm_data(conanfile)
                     if scm_data:
                         dest_dir = os.path.normpath(os.path.join(src_folder, scm_data.subfolder))
@@ -146,7 +148,8 @@ def config_source(export_folder, export_source_folder, local_sources_path, src_f
                         pass
 
                     conanfile.source()
-
+                    plugin_manager.execute("post_source", conanfile=conanfile,
+                                           conanfile_path=conanfile_path,  reference=reference)
             clean_dirty(src_folder)  # Everything went well, remove DIRTY flag
         except Exception as e:
             os.chdir(export_folder)
@@ -159,15 +162,18 @@ def config_source(export_folder, export_source_folder, local_sources_path, src_f
             raise ConanException(e)
 
 
-def config_source_local(dest_dir, conanfile, conanfile_folder, output):
-    output.info('Configuring sources in %s' % dest_dir)
+def config_source_local(dest_dir, conanfile, conanfile_folder, output, conanfile_path,
+                        plugin_manager):
     conanfile.source_folder = dest_dir
+    conanfile.build_folder = None
+    conanfile.package_folder = None
     with tools.chdir(dest_dir):
         try:
             with conanfile_exception_formatter(str(conanfile), "source"):
                 with get_env_context_manager(conanfile):
-                    conanfile.build_folder = None
-                    conanfile.package_folder = None
+                    plugin_manager.execute("pre_source", conanfile=conanfile,
+                                           conanfile_path=conanfile_path)
+                    output.info('Configuring sources in %s' % dest_dir)
                     scm_data = get_scm_data(conanfile)
                     if scm_data:
                         dest_dir = os.path.join(dest_dir, scm_data.subfolder)
@@ -176,6 +182,8 @@ def config_source_local(dest_dir, conanfile, conanfile_folder, output):
                         _fetch_scm(scm_data, dest_dir, local_sources_path, output)
 
                     conanfile.source()
+                    plugin_manager.execute("post_source", conanfile=conanfile,
+                                           conanfile_path=conanfile_path)
         except ConanExceptionInUserConanfileMethod:
             raise
         except Exception as e:
diff --git a/conans/client/tools/oss.py b/conans/client/tools/oss.py
index f66a3226b..c3598042b 100644
--- a/conans/client/tools/oss.py
+++ b/conans/client/tools/oss.py
@@ -327,28 +327,43 @@ def get_cross_building_settings(settings, self_os=None, self_arch=None):
     return build_os, build_arch, host_os, host_arch
 
 
-def get_gnu_triplet(os, arch, compiler=None):
+def get_gnu_triplet(os_, arch, compiler=None):
     """
     Returns string with <machine>-<vendor>-<op_system> triplet (<vendor> can be omitted in practice)
 
-    :param os: os to be used to create the triplet
+    :param os_: os to be used to create the triplet
     :param arch: arch to be used to create the triplet
     :param compiler: compiler used to create the triplet (only needed fo windows)
     """
 
-    if os == "Windows" and compiler is None:
+    if os_ == "Windows" and compiler is None:
         raise ConanException("'compiler' parameter for 'get_gnu_triplet()' is not specified and "
                              "needed for os=Windows")
 
     # Calculate the arch
-    machine = {"x86": "i686" if os != "Linux" else "x86",
+    machine = {"x86": "i686" if os_ != "Linux" else "x86",
                "x86_64": "x86_64",
-               "armv6": "arm",
-               "armv7": "arm",
-               "armv7s": "arm",
-               "armv7k": "arm",
-               "armv7hf": "arm",
                "armv8": "aarch64"}.get(arch, None)
+
+    if not machine:
+        # https://wiki.debian.org/Multiarch/Tuples
+        if "arm" in arch:
+            machine = "arm"
+        elif "ppc64le" in arch:
+            machine = "powerpc64le"
+        elif "ppc64" in arch:
+            machine = "powerpc64"
+        elif "powerpc" in arch:
+            machine = "powerpc"
+        elif "mips64" in arch:
+            machine = "mips64"
+        elif "mips" in arch:
+            machine = "mips"
+        elif "sparcv9" in arch:
+            machine = "sparc64"
+        elif "sparc" in arch:
+            machine = "sparc"
+
     if machine is None:
         raise ConanException("Unknown '%s' machine, Conan doesn't know how to "
                              "translate it to the GNU triplet, please report at "
@@ -369,13 +384,13 @@ def get_gnu_triplet(os, arch, compiler=None):
                  "Macos": "apple-darwin",
                  "iOS": "apple-darwin",
                  "watchOS": "apple-darwin",
-                 "tvOS": "apple-darwin"}.get(os, os.lower())
+                 "tvOS": "apple-darwin"}.get(os_, os_.lower())
 
-    if os in ("Linux", "Android"):
+    if os_ in ("Linux", "Android"):
         if "arm" in arch and arch != "armv8":
             op_system += "eabi"
 
-        if arch == "armv7hf" and os == "Linux":
+        if arch == "armv7hf" and os_ == "Linux":
             op_system += "hf"
 
     return "%s-%s" % (machine, op_system)


Test patch:
diff --git a/conans/test/build_helpers/autotools_configure_test.py b/conans/test/build_helpers/autotools_configure_test.py
index 07ccab794..b8b8ac53d 100644
--- a/conans/test/build_helpers/autotools_configure_test.py
+++ b/conans/test/build_helpers/autotools_configure_test.py
@@ -47,6 +47,16 @@ class AutoToolsConfigureTest(unittest.TestCase):
         be.make()
         self.assertIsNone(conan_file.command)
 
+    def warn_when_no_triplet_test(self):
+        conan_file = ConanFileMock()
+        deps_cpp_info = namedtuple("Deps", "libs, include_paths, lib_paths, defines, cflags, "
+                                   "cppflags, sharedlinkflags, exelinkflags, sysroot")
+        conan_file.deps_cpp_info = deps_cpp_info([], [], [], [], [], [], [], [], "")
+        conan_file.settings = MockSettings({"arch": "UNKNOWN_ARCH", "os": "Linux"})
+        AutoToolsBuildEnvironment(conan_file)
+        self.assertIn("Unknown 'UNKNOWN_ARCH' machine, Conan doesn't know "
+                      "how to translate it to the GNU triplet", conan_file.output)
+
     def test_cppstd(self):
         options = MockOptions({})
         # Valid one for GCC
diff --git a/conans/test/command/info_test.py b/conans/test/command/info_test.py
index 6dbf919ed..db07f5e02 100644
--- a/conans/test/command/info_test.py
+++ b/conans/test/command/info_test.py
@@ -38,12 +38,10 @@ class Pkg(ConanFile):
             self.client.run("export . lasote/stable")
             expected_output = textwrap.dedent(
                 """\
-                WARN: Conanfile doesn't have 'url'.
-                It is recommended to add it as attribute
-                WARN: Conanfile doesn't have 'license'.
-                It is recommended to add it as attribute
-                WARN: Conanfile doesn't have 'description'.
-                It is recommended to add it as attribute""")
+                [PLUGIN - attribute_checker] pre_export(): WARN: Conanfile doesn't have 'url'. It is recommended to add it as attribute
+                [PLUGIN - attribute_checker] pre_export(): WARN: Conanfile doesn't have 'license'. It is recommended to add it as attribute
+                [PLUGIN - attribute_checker] pre_export(): WARN: Conanfile doesn't have 'description'. It is recommended to add it as attribute
+                """)
             self.assertIn(expected_output, self.client.user_io.out)
 
         if number != "Hello2":
diff --git a/conans/test/functional/create_package_test.py b/conans/test/functional/create_package_test.py
index c279198c0..6c5e14a36 100644
--- a/conans/test/functional/create_package_test.py
+++ b/conans/test/functional/create_package_test.py
@@ -86,7 +86,7 @@ class ExporterTest(unittest.TestCase):
         conanfile = loader.load_conanfile(conanfile_path, None, ProcessedProfile())
 
         create_package(conanfile, None, build_folder, build_folder, package_folder, install_folder,
-                       output, copy_info=True)
+                       output, client.plugin_manager, conanfile_path, conan_ref, copy_info=True)
 
         # test build folder
         self.assertTrue(os.path.exists(build_folder))
diff --git a/conans/test/functional/plugin_manager_test.py b/conans/test/functional/plugin_manager_test.py
new file mode 100644
index 000000000..53efeac77
--- /dev/null
+++ b/conans/test/functional/plugin_manager_test.py
@@ -0,0 +1,113 @@
+import os
+import unittest
+
+from conans import load
+from conans.client.plugin_manager import PluginManager
+from conans.errors import ConanException
+from conans.test.utils.test_files import temp_folder
+from conans.test.utils.tools import TestBufferConanOutput
+from conans.util.files import save
+
+my_plugin = """
+def pre_export(output, **kwargs):
+    output.info("pre_export()")
+
+def post_export(output, **kwargs):
+    output.info("post_export()")
+
+def pre_source(output, **kwargs):
+    output.info("pre_source()")
+
+def post_source(output, **kwargs):
+    output.info("post_source()")
+
+def pre_build(output, **kwargs):
+    output.info("pre_build()")
+
+def post_build(output, **kwargs):
+    output.info("post_build()")
+
+def pre_package(output, **kwargs):
+    output.info("pre_package()")
+
+def post_package(output, **kwargs):
+    output.info("post_package()")
+
+def pre_upload(output, **kwargs):
+    output.info("pre_upload()")
+
+def post_upload(output, **kwargs):
+    output.info("post_upload()")
+
+def pre_upload_package(output, **kwargs):
+    output.info("pre_upload_package()")
+
+def post_upload_package(output, **kwargs):
+    output.info("post_upload_package()")
+
+def pre_download(output, **kwargs):
+    output.info("pre_download()")
+
+def post_download(output, **kwargs):
+    output.info("post_download()")
+
+def pre_download_package(output, **kwargs):
+    output.info("pre_download_package()")
+
+def post_download_package(output, **kwargs):
+    output.info("post_download_package()")
+"""
+
+
+class PluginManagerTest(unittest.TestCase):
+
+    def _init(self):
+        temp_dir = temp_folder()
+        plugin_path = os.path.join(temp_dir, "my_plugin.py")
+        save(os.path.join(temp_dir, "my_plugin.py"), my_plugin)
+        output = TestBufferConanOutput()
+        plugin_manager = PluginManager(temp_dir, ["my_plugin"], output)
+        return plugin_manager, output, plugin_path
+
+    def load_test(self):
+        plugin_manager, output, _ = self._init()
+        self.assertEqual({}, plugin_manager.plugins)
+        self.assertEqual(["my_plugin"], plugin_manager._plugin_names)
+        plugin_manager.load_plugins()
+        self.assertEqual(16, len(plugin_manager.plugins))  # Checks number of methods loaded
+
+    def check_output_test(self):
+        plugin_manager, output, _ = self._init()
+        plugin_manager.load_plugins()
+        methods = plugin_manager.plugins.keys()
+        for method in methods:
+            plugin_manager.execute(method)
+            self.assertIn("[PLUGIN - my_plugin] %s(): %s()" % (method, method), output)
+
+    def no_error_with_no_method_test(self):
+        plugin_manager, output, plugin_path = self._init()
+        other_plugin = """
+def my_custom_function():
+    pass
+"""
+        save(plugin_path, other_plugin)
+        self.assertEqual(other_plugin, load(plugin_path))
+        plugin_manager.execute("pre_source")
+        self.assertEqual("", output)
+
+    def exception_in_method_test(self):
+        plugin_manager, output, plugin_path = self._init()
+        my_plugin = """
+from conans.errors import ConanException
+
+def pre_build(output, **kwargs):
+    raise Exception("My custom exception")
+"""
+        save(plugin_path, my_plugin)
+        with self.assertRaisesRegexp(ConanException, "My custom exception"):
+            plugin_manager.execute("pre_build")
+        # Check traceback output
+        try:
+            plugin_manager.execute("pre_build")
+        except ConanException as e:
+            self.assertIn("[PLUGIN - my_plugin] pre_build(): My custom exception", str(e))
diff --git a/conans/test/integration/plugin_test.py b/conans/test/integration/plugin_test.py
new file mode 100644
index 000000000..86eb16387
--- /dev/null
+++ b/conans/test/integration/plugin_test.py
@@ -0,0 +1,337 @@
+import os
+import unittest
+
+from conans.model.ref import ConanFileReference
+from conans.test.utils.tools import TestClient, TestServer
+
+conanfile_basic = """
+from conans import ConanFile
+
+class AConan(ConanFile):
+    name = "basic"
+    version = "0.1"
+"""
+
+complete_plugin = """
+from conans.model.ref import ConanFileReference
+
+
+def pre_export(output, conanfile, conanfile_path, reference, **kwargs):
+    assert conanfile
+    output.info("conanfile_path=%s" % conanfile_path)
+    output.info("reference=%s" % reference.full_repr())
+
+def post_export(output, conanfile, conanfile_path, reference, **kwargs):
+    assert conanfile
+    output.info("conanfile_path=%s" % conanfile_path)
+    output.info("reference=%s" % reference.full_repr())
+
+def pre_source(output, conanfile, conanfile_path, **kwargs):
+    assert conanfile
+    output.info("conanfile_path=%s" % conanfile_path)
+    if conanfile.in_local_cache:
+        output.info("reference=%s" % kwargs["reference"].full_repr())
+
+def post_source(output, conanfile, conanfile_path, **kwargs):
+    assert conanfile
+    output.info("conanfile_path=%s" % conanfile_path)
+    if conanfile.in_local_cache:
+        output.info("reference=%s" % kwargs["reference"].full_repr())
+
+def pre_build(output, conanfile, **kwargs):
+    assert conanfile
+    if conanfile.in_local_cache:
+        output.info("reference=%s" % kwargs["reference"].full_repr())
+        output.info("package_id=%s" % kwargs["package_id"])
+    else:
+        output.info("conanfile_path=%s" % kwargs["conanfile_path"])
+
+def post_build(output, conanfile, **kwargs):
+    assert conanfile
+    if conanfile.in_local_cache:
+        output.info("reference=%s" % kwargs["reference"].full_repr())
+        output.info("package_id=%s" % kwargs["package_id"])
+    else:
+        output.info("conanfile_path=%s" % kwargs["conanfile_path"])
+
+def pre_package(output, conanfile, conanfile_path, **kwargs):
+    assert conanfile
+    output.info("conanfile_path=%s" % conanfile_path)
+    if conanfile.in_local_cache:
+        output.info("reference=%s" % kwargs["reference"].full_repr())
+        output.info("package_id=%s" % kwargs["package_id"])
+
+def post_package(output, conanfile, conanfile_path, **kwargs):
+    assert conanfile
+    output.info("conanfile_path=%s" % conanfile_path)
+    if conanfile.in_local_cache:
+        output.info("reference=%s" % kwargs["reference"].full_repr())
+        output.info("package_id=%s" % kwargs["package_id"])
+
+def pre_upload(output, conanfile_path, reference, remote, **kwargs):
+    output.info("conanfile_path=%s" % conanfile_path)
+    output.info("reference=%s" % reference.full_repr())
+    output.info("remote.name=%s" % remote.name)
+
+def post_upload(output, conanfile_path, reference, remote, **kwargs):
+    output.info("conanfile_path=%s" % conanfile_path)
+    output.info("reference=%s" % reference.full_repr())
+    output.info("remote.name=%s" % remote.name)
+
+def pre_upload_recipe(output, conanfile_path, reference, remote, **kwargs):
+    output.info("conanfile_path=%s" % conanfile_path)
+    output.info("reference=%s" % reference.full_repr())
+    output.info("remote.name=%s" % remote.name)
+
+def post_upload_recipe(output, conanfile_path, reference, remote, **kwargs):
+    output.info("conanfile_path=%s" % conanfile_path)
+    output.info("reference=%s" % reference.full_repr())
+    output.info("remote.name=%s" % remote.name)
+
+def pre_upload_package(output, conanfile_path, reference, package_id, remote, **kwargs):
+    output.info("conanfile_path=%s" % conanfile_path)
+    output.info("reference=%s" % reference.full_repr())
+    output.info("package_id=%s" % package_id)
+    output.info("remote.name=%s" % remote.name)
+
+def post_upload_package(output, conanfile_path, reference, package_id, remote, **kwargs):
+    output.info("conanfile_path=%s" % conanfile_path)
+    output.info("reference=%s" % reference.full_repr())
+    output.info("package_id=%s" % package_id)
+    output.info("remote.name=%s" % remote.name)
+
+def pre_download(output, reference, remote, **kwargs):
+    output.info("reference=%s" % reference.full_repr())
+    output.info("remote.name=%s" % remote.name)
+
+def post_download(output, conanfile_path, reference, remote, **kwargs):
+    output.info("conanfile_path=%s" % conanfile_path)
+    output.info("reference=%s" % reference.full_repr())
+    output.info("remote.name=%s" % remote.name)
+
+def pre_download_recipe(output, reference, remote, **kwargs):
+    output.info("reference=%s" % reference.full_repr())
+    output.info("remote.name=%s" % remote.name)
+
+def post_download_recipe(output, conanfile_path, reference, remote, **kwargs):
+    output.info("conanfile_path=%s" % conanfile_path)
+    output.info("reference=%s" % reference.full_repr())
+    output.info("remote.name=%s" % remote.name)
+
+def pre_download_package(output, conanfile_path, reference, package_id, remote, **kwargs):
+    output.info("conanfile_path=%s" % conanfile_path)
+    output.info("reference=%s" % reference.full_repr())
+    output.info("package_id=%s" % package_id)
+    output.info("remote.name=%s" % remote.name)
+
+def post_download_package(output, conanfile_path, reference, package_id, remote, **kwargs):
+    output.info("conanfile_path=%s" % conanfile_path)
+    output.info("reference=%s" % reference.full_repr())
+    output.info("package_id=%s" % package_id)
+    output.info("remote.name=%s" % remote.name)
+"""
+
+HEADER = "[PLUGIN - complete_plugin] {method_name}():"
+REFERENCE_LOCAL = "basic/0.1@PROJECT"
+REFERENCE_CACHE = "basic/0.1@danimtb/testing"
+PACKAGE_ID = "5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9"
+
+custom_module = """
+def my_printer(output):
+    output.info("my_printer(): CUSTOM MODULE")
+"""
+
+my_plugin = """
+from custom_module.custom import my_printer
+
+def pre_export(output, conanfile_path, reference, **kwargs):
+    my_printer(output)
+"""
+
+
+class PluginTest(unittest.TestCase):
+
+    def default_plugin_test(self):
+        client = TestClient()
+        self.assertTrue(client.client_cache.plugins_path.endswith("plugins"))
+        client.save({"conanfile.py": conanfile_basic})
+        client.run("export . danimtb/testing")
+        self.assertIn("[PLUGIN - attribute_checker] pre_export(): WARN: Conanfile doesn't have 'url'",
+                      client.out)
+        self.assertIn("[PLUGIN - attribute_checker] pre_export(): WARN: Conanfile doesn't have "
+                      "'description'", client.out)
+        self.assertIn("[PLUGIN - attribute_checker] pre_export(): WARN: Conanfile doesn't have "
+                      "'license'", client.out)
+
+    def complete_plugin_test(self):
+        server = TestServer([], users={"danimtb": "pass"})
+        client = TestClient(servers={"default": server}, users={"default": [("danimtb", "pass")]})
+        plugin_path = os.path.join(client.client_cache.plugins_path, "complete_plugin.py")
+        client.save({plugin_path: complete_plugin, "conanfile.py": conanfile_basic})
+        conanfile_path = os.path.join(client.current_folder, "conanfile.py")
+        conanfile_cache_path = client.client_cache.conanfile(
+            ConanFileReference("basic", "0.1", "danimtb", "testing"))
+        client.run("config set plugins.complete_plugin")
+
+        client.run("source .")
+        self._check_source(conanfile_path, client.out)
+
+        client.run("install .")
+        client.run("build .")
+        self._check_build(conanfile_path, client.out)
+
+        client.run("package .")
+        self._check_package(conanfile_path, client.out)
+
+        client.run("export . danimtb/testing")
+        self._check_export(conanfile_path, conanfile_cache_path, client.out)
+
+        client.run("export-pkg . danimtb/testing")
+        self._check_export(conanfile_path, conanfile_cache_path, client.out)
+        self._check_export_pkg(conanfile_cache_path, client.out)
+
+        client.run("remove * --force")
+        client.run("export-pkg . -pf . danimtb/testing")
+        self._check_export(conanfile_path, conanfile_cache_path, client.out)
+        self._check_export_pkg(conanfile_cache_path, client.out)
+
+        client.run("remove * --force")
+        client.run("create . danimtb/testing")
+        self._check_export(conanfile_path, conanfile_cache_path, client.out) # Export gets
+        self._check_source(conanfile_cache_path, client.out, in_cache=True)
+        self._check_build(conanfile_cache_path, client.out, in_cache=True)
+        self._check_package(conanfile_cache_path, client.out, in_cache=True)
+
+        client.run("upload basic/0.1@danimtb/testing -r default")
+        self._check_upload(conanfile_cache_path, client.out)
+        self._check_upload_recipe(conanfile_cache_path, client.out)
+        client.run("upload basic/0.1@danimtb/testing -r default --all")
+        self._check_upload(conanfile_cache_path, client.out)
+        self._check_upload_recipe(conanfile_cache_path, client.out)
+        self._check_upload_package(conanfile_cache_path, client.out)
+
+        client.run("remove * --force")
+        client.run("download basic/0.1@danimtb/testing --recipe")
+        self._check_download(conanfile_cache_path, client.out)
+        self._check_download_recipe(conanfile_cache_path, client.out)
+        client.run("remove * --force")
+        client.run("download basic/0.1@danimtb/testing")
+        self._check_download(conanfile_cache_path, client.out)
+        self._check_download_recipe(conanfile_cache_path, client.out)
+        self._check_download_package(conanfile_cache_path, client.out)
+
+        client.run("remove * --force")
+        client.run("install basic/0.1@danimtb/testing")
+        self._check_download_recipe(conanfile_cache_path, client.out)
+        self._check_download_package(conanfile_cache_path, client.out)
+
+    def _check_source(self, conanfile_path, out, in_cache=False):
+        reference = REFERENCE_CACHE if in_cache else REFERENCE_LOCAL
+        self.assertIn("[PLUGIN - complete_plugin] pre_source(): conanfile_path=%s" % conanfile_path, out)
+        self.assertIn("[PLUGIN - complete_plugin] post_source(): conanfile_path=%s" % conanfile_path, out)
+        if in_cache:
+            self.assertIn("[PLUGIN - complete_plugin] pre_source(): reference=%s" % reference, out)
+            self.assertIn("[PLUGIN - complete_plugin] post_source(): reference=%s" % reference, out)
+
+    def _check_build(self, conanfile_path,  out, in_cache=False):
+        reference = REFERENCE_CACHE if in_cache else REFERENCE_LOCAL
+        if in_cache:
+            self.assertIn("[PLUGIN - complete_plugin] pre_build(): reference=%s" % reference, out)
+            self.assertIn("[PLUGIN - complete_plugin] pre_build(): package_id=%s" % PACKAGE_ID, out)
+            self.assertIn("[PLUGIN - complete_plugin] post_build(): reference=%s" % reference, out)
+            self.assertIn("[PLUGIN - complete_plugin] post_build(): package_id=%s" % PACKAGE_ID, out)
+        else:
+            self.assertIn("[PLUGIN - complete_plugin] pre_build(): conanfile_path=%s" % conanfile_path,
+                          out)
+            self.assertIn("[PLUGIN - complete_plugin] post_build(): conanfile_path=%s" % conanfile_path,
+                          out)
+
+    def _check_package(self, conanfile_path, out, in_cache=False):
+        reference = REFERENCE_CACHE if in_cache else REFERENCE_LOCAL
+        self.assertIn("[PLUGIN - complete_plugin] pre_package(): conanfile_path=%s" % conanfile_path,
+                      out)
+        self.assertIn("[PLUGIN - complete_plugin] post_package(): conanfile_path=%s" % conanfile_path,
+                      out)
+        if in_cache:
+            self.assertIn("[PLUGIN - complete_plugin] pre_package(): reference=%s" % reference, out)
+            self.assertIn("[PLUGIN - complete_plugin] pre_package(): package_id=%s" % PACKAGE_ID, out)
+            self.assertIn("[PLUGIN - complete_plugin] post_package(): reference=%s" % reference, out)
+            self.assertIn("[PLUGIN - complete_plugin] post_package(): package_id=%s" % PACKAGE_ID, out)
+
+    def _check_export(self, conanfile_path, conanfile_cache_path, out):
+        self.assertIn("[PLUGIN - complete_plugin] pre_export(): conanfile_path=%s" % conanfile_path, out)
+        self.assertIn("[PLUGIN - complete_plugin] pre_export(): reference=%s" % REFERENCE_CACHE, out)
+        self.assertIn("[PLUGIN - complete_plugin] post_export(): conanfile_path=%s" % conanfile_cache_path, out)
+        self.assertIn("[PLUGIN - complete_plugin] post_export(): reference=%s" % REFERENCE_CACHE, out)
+
+    def _check_export_pkg(self, conanfile_cache_path, out):
+        self.assertIn("[PLUGIN - complete_plugin] pre_package(): conanfile_path=%s" % conanfile_cache_path, out)
+        self.assertIn("[PLUGIN - complete_plugin] pre_package(): reference=%s" % REFERENCE_CACHE, out)
+        self.assertIn("[PLUGIN - complete_plugin] pre_package(): package_id=%s" % PACKAGE_ID, out)
+        self.assertIn("[PLUGIN - complete_plugin] post_package(): conanfile_path=%s" % conanfile_cache_path, out)
+        self.assertIn("[PLUGIN - complete_plugin] post_package(): reference=%s" % REFERENCE_CACHE, out)
+        self.assertIn("[PLUGIN - complete_plugin] post_package(): package_id=%s" % PACKAGE_ID, out)
+
+    def _check_upload(self, conanfile_cache_path, out):
+        self.assertIn("[PLUGIN - complete_plugin] pre_upload(): conanfile_path=%s" % conanfile_cache_path, out)
+        self.assertIn("[PLUGIN - complete_plugin] pre_upload(): reference=%s" % REFERENCE_CACHE, out)
+        self.assertIn("[PLUGIN - complete_plugin] pre_upload(): remote.name=default", out)
+        self.assertIn("[PLUGIN - complete_plugin] post_upload(): conanfile_path=%s" % conanfile_cache_path, out)
+        self.assertIn("[PLUGIN - complete_plugin] post_upload(): reference=%s" % REFERENCE_CACHE, out)
+        self.assertIn("[PLUGIN - complete_plugin] post_upload(): remote.name=default", out)
+
+    def _check_upload_recipe(self, conanfile_cache_path, out):
+        self.assertIn("[PLUGIN - complete_plugin] pre_upload_recipe(): conanfile_path=%s" % conanfile_cache_path, out)
+        self.assertIn("[PLUGIN - complete_plugin] pre_upload_recipe(): reference=%s" % REFERENCE_CACHE, out)
+        self.assertIn("[PLUGIN - complete_plugin] pre_upload_recipe(): remote.name=default", out)
+        self.assertIn("[PLUGIN - complete_plugin] post_upload_recipe(): conanfile_path=%s" % conanfile_cache_path, out)
+        self.assertIn("[PLUGIN - complete_plugin] post_upload_recipe(): reference=%s" % REFERENCE_CACHE, out)
+        self.assertIn("[PLUGIN - complete_plugin] post_upload_recipe(): remote.name=default", out)
+
+    def _check_upload_package(self, conanfile_cache_path, out):
+        self.assertIn("[PLUGIN - complete_plugin] pre_upload_package(): conanfile_path=%s" % conanfile_cache_path, out)
+        self.assertIn("[PLUGIN - complete_plugin] pre_upload_package(): reference=%s" % REFERENCE_CACHE, out)
+        self.assertIn("[PLUGIN - complete_plugin] pre_upload_package(): package_id=%s" % PACKAGE_ID, out)
+        self.assertIn("[PLUGIN - complete_plugin] pre_upload_package(): remote.name=default", out)
+        self.assertIn("[PLUGIN - complete_plugin] post_upload_package(): conanfile_path=%s" % conanfile_cache_path, out)
+        self.assertIn("[PLUGIN - complete_plugin] post_upload_package(): reference=%s" % REFERENCE_CACHE, out)
+        self.assertIn("[PLUGIN - complete_plugin] post_upload_package(): package_id=%s" % PACKAGE_ID, out)
+        self.assertIn("[PLUGIN - complete_plugin] post_upload_package(): remote.name=default", out)
+
+    def _check_download(self, conanfile_cache_path, out):
+        self.assertIn("[PLUGIN - complete_plugin] pre_download(): reference=%s" % REFERENCE_CACHE, out)
+        self.assertIn("[PLUGIN - complete_plugin] pre_download(): remote.name=default", out)
+        self.assertIn("[PLUGIN - complete_plugin] post_download(): conanfile_path=%s" % conanfile_cache_path, out)
+        self.assertIn("[PLUGIN - complete_plugin] post_download(): reference=%s" % REFERENCE_CACHE, out)
+        self.assertIn("[PLUGIN - complete_plugin] post_download(): remote.name=default", out)
+
+    def _check_download_recipe(self, conanfile_cache_path, out):
+        self.assertIn("[PLUGIN - complete_plugin] pre_download_recipe(): reference=%s" % REFERENCE_CACHE, out)
+        self.assertIn("[PLUGIN - complete_plugin] pre_download_recipe(): remote.name=default", out)
+        self.assertIn("[PLUGIN - complete_plugin] post_download_recipe(): conanfile_path=%s" % conanfile_cache_path, out)
+        self.assertIn("[PLUGIN - complete_plugin] post_download_recipe(): reference=%s" % REFERENCE_CACHE, out)
+        self.assertIn("[PLUGIN - complete_plugin] post_download_recipe(): remote.name=default", out)
+
+    def _check_download_package(self, conanfile_cache_path, out):
+        self.assertIn("[PLUGIN - complete_plugin] pre_download_package(): conanfile_path=%s" % conanfile_cache_path, out)
+        self.assertIn("[PLUGIN - complete_plugin] pre_download_package(): reference=%s" % REFERENCE_CACHE, out)
+        self.assertIn("[PLUGIN - complete_plugin] pre_download_package(): package_id=%s" % PACKAGE_ID, out)
+        self.assertIn("[PLUGIN - complete_plugin] pre_download_package(): remote.name=default", out)
+        self.assertIn("[PLUGIN - complete_plugin] post_download_package(): conanfile_path=%s" % conanfile_cache_path, out)
+        self.assertIn("[PLUGIN - complete_plugin] post_download_package(): reference=%s" % REFERENCE_CACHE, out)
+        self.assertIn("[PLUGIN - complete_plugin] post_download_package(): package_id=%s" % PACKAGE_ID, out)
+        self.assertIn("[PLUGIN - complete_plugin] post_download_package(): remote.name=default", out)
+
+    def import_plugin_test(self):
+        client = TestClient()
+        plugin_path = os.path.join(client.client_cache.plugins_path, "my_plugin.py")
+        init_path = os.path.join(client.client_cache.plugins_path, "custom_module", "__init__.py")
+        custom_path = os.path.join(client.client_cache.plugins_path, "custom_module", "custom.py")
+        client.save({plugin_path: complete_plugin,
+                     init_path: "",
+                     custom_path: custom_module,
+                     plugin_path: my_plugin,
+                     "conanfile.py": conanfile_basic})
+        client.run("config set plugins.my_plugin")
+        client.run("export . danimtb/testing")
+        self.assertIn("[PLUGIN - my_plugin] pre_export(): my_printer(): CUSTOM MODULE", client.out)
diff --git a/conans/test/util/tools_test.py b/conans/test/util/tools_test.py
index dde6a8efe..d2b6c4cca 100644
--- a/conans/test/util/tools_test.py
+++ b/conans/test/util/tools_test.py
@@ -982,6 +982,26 @@ ProgramFiles(x86)=C:\Program Files (x86)
             host = tools.get_gnu_triplet(setting_os, setting_arch, compiler)
             return build, host
 
+        build, host = get_values("Linux", "armv6", "Linux", "armv6")
+        self.assertEquals(build, "arm-linux-gnueabi")
+        self.assertEquals(host, "arm-linux-gnueabi")
+
+        build, host = get_values("Linux", "sparc", "Linux", "sparcv9")
+        self.assertEquals(build, "sparc-linux-gnu")
+        self.assertEquals(host, "sparc64-linux-gnu")
+
+        build, host = get_values("Linux", "mips", "Linux", "mips64")
+        self.assertEquals(build, "mips-linux-gnu")
+        self.assertEquals(host, "mips64-linux-gnu")
+
+        build, host = get_values("Linux", "ppc64le", "Linux", "ppc64")
+        self.assertEquals(build, "powerpc64le-linux-gnu")
+        self.assertEquals(host, "powerpc64-linux-gnu")
+
+        build, host = get_values("Linux", "armv5te", "Linux", "arm_whatever")
+        self.assertEquals(build, "arm-linux-gnueabi")
+        self.assertEquals(host, "arm-linux-gnueabi")
+
         build, host = get_values("Linux", "x86_64", "Linux", "armv7hf")
         self.assertEquals(build, "x86_64-linux-gnu")
         self.assertEquals(host, "arm-linux-gnueabihf")
@@ -1074,18 +1094,18 @@ ProgramFiles(x86)=C:\Program Files (x86)
         self.assertEquals(build, "x86_64-apple-darwin")
         self.assertEquals(host, "aarch64-apple-darwin")
 
-        for os in ["Windows", "Linux"]:
+        for _os in ["Windows", "Linux"]:
             for arch in ["x86_64", "x86"]:
-                triplet = tools.get_gnu_triplet(os, arch, "gcc")
+                triplet = tools.get_gnu_triplet(_os, arch, "gcc")
 
                 output = ""
                 if arch == "x86_64":
                     output += "x86_64"
                 else:
-                    output += "i686" if os != "Linux" else "x86"
+                    output += "i686" if _os != "Linux" else "x86"
 
                 output += "-"
-                if os == "Windows":
+                if _os == "Windows":
                     output += "w64-mingw32"
                 else:
                     output += "linux-gnu"
diff --git a/conans/test/utils/tools.py b/conans/test/utils/tools.py
index 1e676b33a..df7b77c08 100644
--- a/conans/test/utils/tools.py
+++ b/conans/test/utils/tools.py
@@ -24,6 +24,7 @@ from conans.client.conan_api import migrate_and_get_client_cache, Conan, get_req
 from conans.client.conan_command_output import CommandOutputer
 from conans.client.conf import MIN_SERVER_COMPATIBLE_VERSION
 from conans.client.output import ConanOutput
+from conans.client.plugin_manager import PluginManager
 from conans.client.remote_registry import RemoteRegistry
 from conans.client.rest.conan_requester import ConanRequester
 from conans.client.rest.uploader_downloader import IterableToFileAdapter
@@ -461,10 +462,15 @@ class TestClient(object):
             self.requester = ConanRequester(requester, self.client_cache,
                                             get_request_timeout())
 
+            self.plugin_manager = PluginManager(self.client_cache.plugins_path,
+                                                get_env("CONAN_PLUGINS", list()),
+                                                self.user_io.out)
+
             self.localdb, self.rest_api_client, self.remote_manager = Conan.instance_remote_manager(
                                                             self.requester, self.client_cache,
                                                             self.user_io, self.client_version,
-                                                            self.min_server_compatible_version)
+                                                            self.min_server_compatible_version,
+                                                            self.plugin_manager)
             set_global_instances(output, self.requester)
 
     def init_dynamic_vars(self, user_io=None):
@@ -485,7 +491,7 @@ class TestClient(object):
             # Settings preprocessor
             interactive = not get_env("CONAN_NON_INTERACTIVE", False)
             conan = Conan(self.client_cache, self.user_io, self.runner, self.remote_manager,
-                          interactive=interactive)
+                          self.plugin_manager, interactive=interactive)
         outputer = CommandOutputer(self.user_io, self.client_cache)
         command = Command(conan, self.client_cache, self.user_io, outputer)
         args = shlex.split(command_line)