Repository: conan-io/conan

Issue:
Improve tools.vs_installation_path with VSXX.0COMNTOOLS env variable
To help us debug your issue please explain:

- [x] I've read the [CONTRIBUTING guide](https://raw.githubusercontent.com/conan-io/conan/develop/.github/CONTRIBUTING.md).
- [x] I've specified the Conan version, operating system version and any tool that can be relevant.
Conan version 1.0.0-beta.5
- [x] I've explained the steps to reproduce the error or the motivation/use case of the question/suggestion.

Now `vs_installation_path()` tool uses `vswhere` to locate VS installations. However, `vswhere` is not always installed (in case of versions **prior to** Visual Studio 2017 or Build Tools 2017).
My suggestion is to search for  `VSXX.0COMNTOOLS` variable to locate the installation path of old VS versions.

**Note**: `vswhere` is able to locate old versions of both VS and BuildTools installations (`vswhere -legacy`, `vswhere -products *`). This issue focuses in the case when there is no vswhere in the system (vswhere is only installed with 2017 installers).

Maybe add a `vs_comntools` helper method, as `os.getenv("vs15comntools")` and similar is used in other places of the codebase.




Discussion:

Resolving patch:
diff --git a/conans/client/conf/__init__.py b/conans/client/conf/__init__.py
--- a/conans/client/conf/__init__.py
+++ b/conans/client/conf/__init__.py
@@ -83,6 +83,7 @@
 default_profile = %s
 compression_level = 9                 # environment CONAN_COMPRESSION_LEVEL
 sysrequires_sudo = True               # environment CONAN_SYSREQUIRES_SUDO
+# 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)
 # recipe_linter = False               # environment CONAN_RECIPE_LINTER
@@ -149,6 +150,7 @@ def env_vars(self):
                "CONAN_PYLINTRC": self._env_c("general.pylintrc", "CONAN_PYLINTRC", None),
                "CONAN_PYLINT_WERR": self._env_c("general.pylint_werr", "CONAN_PYLINT_WERR", None),
                "CONAN_SYSREQUIRES_SUDO": self._env_c("general.sysrequires_sudo", "CONAN_SYSREQUIRES_SUDO", "False"),
+               "CONAN_VS_INSTALLATION_PREFERENCE": self._env_c("general.vs_installation_preference", "CONAN_VS_INSTALLATION_PREFERENCE", None),
                "CONAN_RECIPE_LINTER": self._env_c("general.recipe_linter", "CONAN_RECIPE_LINTER", "True"),
                "CONAN_CPU_COUNT": self._env_c("general.cpu_count", "CONAN_CPU_COUNT", None),
                "CONAN_READ_ONLY_CACHE": self._env_c("general.read_only_cache", "CONAN_READ_ONLY_CACHE", None),
diff --git a/conans/client/tools/win.py b/conans/client/tools/win.py
--- a/conans/client/tools/win.py
+++ b/conans/client/tools/win.py
@@ -5,11 +5,13 @@
 import re
 
 import subprocess
+from collections import OrderedDict
 from contextlib import contextmanager
 
 from conans.client.tools.env import environment_append
 from conans.client.tools.oss import cpu_count, detected_architecture, os_info
 from conans.errors import ConanException
+from conans.util.env_reader import get_env
 from conans.util.files import decode_text
 
 _global_output = None
@@ -63,24 +65,64 @@ def build_sln_command(settings, sln_path, targets=None, upgrade_project=True, bu
     return command
 
 
-def vs_installation_path(version):
-    if not hasattr(vs_installation_path, "_cached"):
-        vs_installation_path._cached = dict()
+def vs_installation_path(version, preference=None):
 
-    if version not in vs_installation_path._cached:
-        vs_path = None
+    vs_installation_path = None
+
+    if not preference:
+        env_prefs = get_env("CONAN_VS_INSTALLATION_PREFERENCE", list())
+
+        if env_prefs:
+            preference = env_prefs
+        else:  # default values
+            preference = ["Enterprise", "Professional", "Community", "BuildTools"]
+
+    # Try with vswhere()
+    try:
         legacy_products = vswhere(legacy=True)
         all_products = vswhere(products=["*"])
         products = legacy_products + all_products
+    except ConanException:
+        products = None
 
-        for product in products:
-            if product["installationVersion"].startswith(("%d." % int(version))):
-                vs_path = product["installationPath"]
+    vs_paths = []
 
-        # Remember to cache result
-        vs_installation_path._cached[version] = vs_path
+    if products:
+        # remove repeated products
+        seen_products = []
+        for product in products:
+            if product not in seen_products:
+                seen_products.append(product)
+
+        # Append products with "productId" by order of preference
+        for product_type in preference:
+            for product in seen_products:
+                product = dict(product)
+                if product["installationVersion"].startswith(("%d." % int(version))) and "productId" in product:
+                    if product_type in product["productId"]:
+                        vs_paths.append(product["installationPath"])
+
+        # Append products without "productId" (Legacy installations)
+        for product in seen_products:
+            product = dict(product)
+            if product["installationVersion"].startswith(("%d." % int(version))) and "productId" not in product:
+                vs_paths.append(product["installationPath"])
+
+    # If vswhere does not find anything or not available, try with vs_comntools()
+    if not vs_paths:
+        vs_path = vs_comntools(version)
+
+        if vs_path:
+            sub_path_to_remove = os.path.join("", "Common7", "Tools", "")
+            # Remove '\\Common7\\Tools\\' to get same output as vswhere
+            if vs_path.endswith(sub_path_to_remove):
+                vs_path = vs_path[:-(len(sub_path_to_remove)+1)]
+
+        vs_installation_path = vs_path
+    else:
+        vs_installation_path = vs_paths[0]
 
-    return vs_installation_path._cached[version]
+    return vs_installation_path
 
 
 def vswhere(all_=False, prerelease=False, products=None, requires=None, version="", latest=False,
@@ -155,6 +197,12 @@ def vswhere(all_=False, prerelease=False, products=None, requires=None, version=
     return json.loads(vswhere_out)
 
 
+def vs_comntools(compiler_version):
+    env_var = "vs%s0comntools" % compiler_version
+    vs_path = os.getenv(env_var)
+    return vs_path
+
+
 def find_windows_10_sdk():
     """finds valid Windows 10 SDK version which can be passed to vcvarsall.bat (vcvars_command)"""
     # uses the same method as VCVarsQueryRegistry.bat
@@ -204,7 +252,10 @@ def vcvars_command(settings, arch=None, compiler_version=None, force=False):
                        'armv8': 'x86_arm64'}.get(arch_setting)
     if not vcvars_arch:
         raise ConanException('unsupported architecture %s' % arch_setting)
+
+    command = ""
     existing_version = os.environ.get("VisualStudioVersion")
+
     if existing_version:
         command = "echo Conan:vcvars already set"
         existing_version = existing_version.split(".")[0]
@@ -216,33 +267,19 @@ def vcvars_command(settings, arch=None, compiler_version=None, force=False):
             else:
                 _global_output.warn(message)
     else:
-        env_var = "vs%s0comntools" % compiler_version
-
-        if env_var == 'vs150comntools':
-            vs_path = os.getenv(env_var)
-            if not vs_path:  # Try to locate with vswhere
-                vs_root = vs_installation_path("15")
-                if vs_root:
-                    vs_path = os.path.join(vs_root, "Common7", "Tools")
-                else:
-                    raise ConanException("VS2017 '%s' variable not defined, "
-                                         "and vswhere didn't find it" % env_var)
-            if not os.path.isdir(vs_path):
-                _global_output.warn('VS variable %s points to the non-existing path "%s",'
-                                    'please check that you have set it correctly' % (env_var, vs_path))
-            vcvars_path = os.path.join(vs_path, "../../VC/Auxiliary/Build/vcvarsall.bat")
-            command = ('set "VSCMD_START_DIR=%%CD%%" && '
-                       'call "%s" %s' % (vcvars_path, vcvars_arch))
+        vs_path = vs_installation_path(str(compiler_version))
+
+        if not vs_path or not os.path.isdir(vs_path):
+            _global_output.warn("VS non-existing installation")
         else:
-            try:
-                vs_path = os.environ[env_var]
-            except KeyError:
-                raise ConanException("VS '%s' variable not defined. Please install VS" % env_var)
-            if not os.path.isdir(vs_path):
-                _global_output.warn('VS variable %s points to the non-existing path "%s",'
-                                    'please check that you have set it correctly' % (env_var, vs_path))
-            vcvars_path = os.path.join(vs_path, "../../VC/vcvarsall.bat")
-            command = ('call "%s" %s' % (vcvars_path, vcvars_arch))
+            vcvars_path = ""
+            if int(compiler_version) > 14:
+                vcvars_path = os.path.join(vs_path, "VC/Auxiliary/Build/vcvarsall.bat")
+                command = ('set "VSCMD_START_DIR=%%CD%%" && '
+                           'call "%s" %s' % (vcvars_path, vcvars_arch))
+            else:
+                vcvars_path = os.path.join(vs_path, "VC/vcvarsall.bat")
+                command = ('call "%s" %s' % (vcvars_path, vcvars_arch))
 
     if os_setting == 'WindowsStore':
         os_version_setting = settings.get_safe("os.version")
diff --git a/conans/util/env_reader.py b/conans/util/env_reader.py
--- a/conans/util/env_reader.py
+++ b/conans/util/env_reader.py
@@ -25,5 +25,5 @@ def get_env(env_key, default=None, environment=None):
         elif isinstance(default, float):
             return float(env_var)
         elif isinstance(default, list):
-            return env_var.split(",")
+            return [var.strip() for var in env_var.split(",")]
     return env_var


Test patch:
diff --git a/conans/test/util/tools_test.py b/conans/test/util/tools_test.py
--- a/conans/test/util/tools_test.py
+++ b/conans/test/util/tools_test.py
@@ -3,12 +3,14 @@
 import unittest
 
 from collections import namedtuple
+from six import StringIO
 
 from conans.client.client_cache import CONAN_CONF
 
 from conans import tools
 from conans.client.conan_api import ConanAPIV1
 from conans.client.conf import default_settings_yml, default_client_conf
+from conans.client.output import ConanOutput
 
 from conans.errors import ConanException, NotFoundException
 from conans.model.settings import Settings
@@ -29,7 +31,7 @@ def __init__(self, return_ok=True):
         self.command_called = None
         self.return_ok = return_ok
 
-    def __call__(self, command, output, win_bash=False, subsystem=None):  # @UnusedVariable
+    def __call__(self, command, output, win_bash=False, subsystem=None): # @UnusedVariable
         self.command_called = command
         self.win_bash = win_bash
         self.subsystem = subsystem
@@ -124,7 +126,7 @@ def get_env_unit_test(self):
         )
         self.assertEqual(
             tools.get_env("TO_LIST_NOT_TRIMMED", default=[], environment={"TO_LIST_NOT_TRIMMED": " 1 , 2 , 3 "}),
-            [" 1 ", " 2 ", " 3 "]
+            ["1", "2", "3"]
         )
 
     def test_get_env_in_conanfile(self):
@@ -476,7 +478,6 @@ def vcvars_echo_test(self):
             self.assertIn("VS140COMNTOOLS=", str(output))
 
     def vcvars_constrained_test(self):
-
         text = """os: [Windows]
 compiler:
     Visual Studio:
@@ -488,10 +489,15 @@ def vcvars_constrained_test(self):
         with self.assertRaisesRegexp(ConanException,
                                      "compiler.version setting required for vcvars not defined"):
             tools.vcvars_command(settings)
+
+        new_out = StringIO()
+        tools.set_global_instances(ConanOutput(new_out), None)
         settings.compiler.version = "14"
         with tools.environment_append({"vs140comntools": "path/to/fake"}):
-            cmd = tools.vcvars_command(settings)
-            self.assertIn("vcvarsall.bat", cmd)
+            tools.vcvars_command(settings)
+            if platform.system() != "Windows":
+                self.assertIn("VS non-existing installation", new_out.getvalue())
+
             with tools.environment_append({"VisualStudioVersion": "12"}):
                 with self.assertRaisesRegexp(ConanException,
                                              "Error, Visual environment already set to 12"):