Repository: conan-io/conan
Pull request #12884 (2023-01-11 09:29:03)

[AutotoolsToolchain] Added helpers `update_xxxx_args`
Changelog: Feature: `AutotoolsToolchain.configure_args` are adequately initialized.
Changelog: Feature: Added to AutotoolsToolchain helper functions: `update_configure_args`, `update_make_args`, and `update_autoreconf_args` to update/prune flags effortlessly.
Changelog: Bugfix: `AutotoolsToolchain.configure_args` was overwriting Conan's pre-calculated arguments.
Docs: https://github.com/conan-io/docs/pull/XXXX

Closes: https://github.com/conan-io/conan/issues/12431
Closes: https://github.com/conan-io/conan/issues/12642
Closes: https://github.com/conan-io/conan/issues/12705
Closes: https://github.com/conan-io/conan/issues/12546

Taking back the first approach of https://github.com/conan-io/conan/pull/12645



Patch:
diff --git a/conan/tools/gnu/autotoolstoolchain.py b/conan/tools/gnu/autotoolstoolchain.py
index 0a8914df4..61e159ea4 100644
--- a/conan/tools/gnu/autotoolstoolchain.py
+++ b/conan/tools/gnu/autotoolstoolchain.py
@@ -7,7 +7,7 @@ from conan.tools.build.cross_building import cross_building, get_cross_building_
 from conan.tools.env import Environment
 from conan.tools.files.files import save_toolchain_args
 from conan.tools.gnu.get_gnu_triplet import _get_gnu_triplet
-from conan.tools.microsoft import VCVars, is_msvc, msvc_runtime_flag
+from conan.tools.microsoft import VCVars, msvc_runtime_flag
 from conans.errors import ConanException
 from conans.tools import args_to_string
 
@@ -18,10 +18,6 @@ class AutotoolsToolchain:
         self._namespace = namespace
         self._prefix = prefix
 
-        self.configure_args = self._default_configure_shared_flags() + self._default_configure_install_flags()
-        self.autoreconf_args = self._default_autoreconf_flags()
-        self.make_args = []
-
         # Flags
         self.extra_cxxflags = []
         self.extra_cflags = []
@@ -55,12 +51,13 @@ class AutotoolsToolchain:
         self.sysroot_flag = None
 
         if cross_building(self._conanfile):
+            # Host triplet
             os_build, arch_build, os_host, arch_host = get_cross_building_settings(self._conanfile)
             compiler = self._conanfile.settings.get_safe("compiler")
             if not self._host:
                 self._host = _get_gnu_triplet(os_host, arch_host, compiler=compiler)
+            # Build triplet
             self._build = _get_gnu_triplet(os_build, arch_build, compiler=compiler)
-
             # Apple Stuff
             if os_build == "Macos":
                 sdk_path = apple_sdk_path(conanfile)
@@ -74,6 +71,12 @@ class AutotoolsToolchain:
         sysroot = sysroot.replace("\\", "/") if sysroot is not None else None
         self.sysroot_flag = "--sysroot {}".format(sysroot) if sysroot else None
 
+        self.configure_args = self._default_configure_shared_flags() + \
+                              self._default_configure_install_flags() + \
+                              self._get_triplets()
+        self.autoreconf_args = self._default_autoreconf_flags()
+        self.make_args = []
+
         check_using_build_profile(self._conanfile)
 
     def _get_msvc_runtime_flag(self):
@@ -180,19 +183,67 @@ class AutotoolsToolchain:
                                        _get_argument("datarootdir", "resdirs")])
         return [el for el in configure_install_flags if el]
 
-    def _default_autoreconf_flags(self):
+    @staticmethod
+    def _default_autoreconf_flags():
         return ["--force", "--install"]
 
+    def _get_triplets(self):
+        triplets = []
+        for flag, value in (("--host=", self._host), ("--build=", self._build),
+                            ("--target=", self._target)):
+            if value:
+                triplets.append(f'{flag}{value}')
+        return triplets
+
+    def update_configure_args(self, updated_flags):
+        """
+        Helper to update/prune flags from ``self.configure_args``.
+
+        :param updated_flags: ``dict`` with arguments as keys and their argument values.
+                              Notice that if argument value is ``None``, this one will be pruned.
+        """
+        self._update_flags("configure_args", updated_flags)
+
+    def update_make_args(self, updated_flags):
+        """
+        Helper to update/prune arguments from ``self.make_args``.
+
+        :param updated_flags: ``dict`` with arguments as keys and their argument values.
+                              Notice that if argument value is ``None``, this one will be pruned.
+        """
+        self._update_flags("make_args", updated_flags)
+
+    def update_autoreconf_args(self, updated_flags):
+        """
+        Helper to update/prune arguments from ``self.autoreconf_args``.
+
+        :param updated_flags: ``dict`` with arguments as keys and their argument values.
+                              Notice that if argument value is ``None``, this one will be pruned.
+        """
+        self._update_flags("autoreconf_args", updated_flags)
+
+    # FIXME: Remove all these update_xxxx whenever xxxx_args are dicts or new ones replace them
+    def _update_flags(self, attr_name, updated_flags):
+        _new_flags = []
+        self_args = getattr(self, attr_name)
+        for index, flag in enumerate(self_args):
+            flag_name = flag.split("=")[0]
+            if flag_name in updated_flags:
+                new_flag_value = updated_flags[flag_name]
+                # if {"build": None} is passed, then "--build=xxxx" will be pruned
+                if new_flag_value is None:
+                    continue
+                elif not new_flag_value:
+                    _new_flags.append(flag_name)
+                else:
+                    _new_flags.append(f"{flag_name}={new_flag_value}")
+            else:
+                _new_flags.append(flag)
+        # Update the current ones
+        setattr(self, attr_name, _new_flags)
+
     def generate_args(self):
-        configure_args = []
-        configure_args.extend(self.configure_args)
-        user_args_str = args_to_string(self.configure_args)
-        for flag, var in (("host", self._host), ("build", self._build), ("target", self._target)):
-            if var and flag not in user_args_str:
-                configure_args.append('--{}={}'.format(flag, var))
-
-        args = {"configure_args": args_to_string(configure_args),
+        args = {"configure_args": args_to_string(self.configure_args),
                 "make_args":  args_to_string(self.make_args),
                 "autoreconf_args": args_to_string(self.autoreconf_args)}
-
         save_toolchain_args(args, namespace=self._namespace)
diff --git a/conan/tools/gnu/get_gnu_triplet.py b/conan/tools/gnu/get_gnu_triplet.py
index c082f3007..d25e8c506 100644
--- a/conan/tools/gnu/get_gnu_triplet.py
+++ b/conan/tools/gnu/get_gnu_triplet.py
@@ -68,7 +68,7 @@ def _get_gnu_triplet(os_, arch, compiler=None):
     if compiler == "gcc":
         windows_op = "w64-mingw32"
     elif compiler == "Visual Studio":
-        windows_op = "windows-msvc"
+        windows_op = "unknown-windows"
     else:
         windows_op = "windows"
 
diff --git a/conan/tools/microsoft/visual.py b/conan/tools/microsoft/visual.py
index 882b867e5..5050ee8b8 100644
--- a/conan/tools/microsoft/visual.py
+++ b/conan/tools/microsoft/visual.py
@@ -8,7 +8,7 @@ from conans.errors import ConanException, ConanInvalidConfiguration
 CONAN_VCVARS_FILE = "conanvcvars.bat"
 
 
-def check_min_vs(conanfile, version):
+def check_min_vs(conanfile, version, raise_invalid=True):
     """ this is a helper method to allow the migration of 1.X->2.0 and VisualStudio->msvc settings
     withoug breaking recipes
     The legacy "Visual Studio" with different toolset is not managed, not worth the complexity
@@ -30,9 +30,13 @@ def check_min_vs(conanfile, version):
             compiler_version += ".{}".format(compiler_update)
 
     if compiler_version and Version(compiler_version) < version:
-        msg = "This package doesn't work with VS compiler version '{}'" \
-              ", it requires at least '{}'".format(compiler_version, version)
-        raise ConanInvalidConfiguration(msg)
+        if raise_invalid:
+            msg = f"This package doesn't work with VS compiler version '{compiler_version}'" \
+                  f", it requires at least '{version}'"
+            raise ConanInvalidConfiguration(msg)
+        else:
+            return False
+    return True
 
 
 def msvc_version_to_vs_ide_version(version):
@@ -72,6 +76,10 @@ class VCVars:
         if compiler not in ("Visual Studio", "msvc", "clang"):
             return
 
+        vs_install_path = conanfile.conf.get("tools.microsoft.msbuild:installation_path")
+        if vs_install_path == "":  # Empty string means "disable"
+            return
+
         if compiler == "clang":
             # The vcvars only needed for LLVM/Clang and VS ClangCL, who define runtime
             if not conanfile.settings.get_safe("compiler.runtime"):
@@ -93,7 +101,6 @@ class VCVars:
             vcvars_ver = _vcvars_vers(conanfile, compiler, vs_version)
         vcvarsarch = vcvars_arch(conanfile)
 
-        vs_install_path = conanfile.conf.get("tools.microsoft.msbuild:installation_path")
         # The vs_install_path is like
         # C:\Program Files (x86)\Microsoft Visual Studio\2019\Community
         # C:\Program Files (x86)\Microsoft Visual Studio\2017\Community
diff --git a/conans/model/conf.py b/conans/model/conf.py
index 4ed113b61..fcad44d96 100644
--- a/conans/model/conf.py
+++ b/conans/model/conf.py
@@ -35,7 +35,7 @@ BUILT_IN_CONFS = {
     "tools.microsoft.msbuild:verbosity": "Verbosity level for MSBuild: 'Quiet', 'Minimal', 'Normal', 'Detailed', 'Diagnostic'",
     "tools.microsoft.msbuild:vs_version": "Defines the IDE version when using the new msvc compiler",
     "tools.microsoft.msbuild:max_cpu_count": "Argument for the /m when running msvc to build parallel projects",
-    "tools.microsoft.msbuild:installation_path": "VS install path, to avoid auto-detect via vswhere, like C:/Program Files (x86)/Microsoft Visual Studio/2019/Community",
+    "tools.microsoft.msbuild:installation_path": "VS install path, to avoid auto-detect via vswhere, like C:/Program Files (x86)/Microsoft Visual Studio/2019/Community. Use empty string to disable",
     "tools.microsoft.msbuilddeps:exclude_code_analysis": "Suppress MSBuild code analysis for patterns",
     "tools.microsoft.msbuildtoolchain:compile_options": "Dictionary with MSBuild compiler options",
     "tools.intel:installation_path": "Defines the Intel oneAPI installation root path",


Test patch:
diff --git a/conans/test/integration/toolchains/microsoft/vcvars_test.py b/conans/test/integration/toolchains/microsoft/vcvars_test.py
index 27db87bbd..fb6779ab8 100644
--- a/conans/test/integration/toolchains/microsoft/vcvars_test.py
+++ b/conans/test/integration/toolchains/microsoft/vcvars_test.py
@@ -4,6 +4,7 @@ import os
 
 import pytest
 
+from conans.test.assets.genconanfile import GenConanfile
 from conans.test.utils.tools import TestClient
 
 
@@ -36,6 +37,23 @@ def test_vcvars_generator(scope):
         assert not os.path.exists(os.path.join(client.current_folder, "conanbuild.bat"))
 
 
+@pytest.mark.skipif(platform.system() not in ["Windows"], reason="Requires Windows")
+def test_vcvars_generator_skip():
+    """
+    tools.microsoft.msbuild:installation_path=disabled avoids creation of conanvcvars.bat
+    """
+    client = TestClient()
+    client.save({"conanfile.py": GenConanfile().with_generator("VCVars")
+                                               .with_settings("os", "compiler",
+                                                              "arch", "build_type"),
+                 "profile": 'include(default)\n[conf]\ntools.microsoft.msbuild:installation_path='})
+
+    client.run('install . -c tools.microsoft.msbuild:installation_path=""')
+    assert not os.path.exists(os.path.join(client.current_folder, "conanvcvars.bat"))
+    client.run('install . -pr=profile')
+    assert not os.path.exists(os.path.join(client.current_folder, "conanvcvars.bat"))
+
+
 @pytest.mark.skipif(platform.system() not in ["Windows"], reason="Requires Windows")
 def test_vcvars_generator_string():
     client = TestClient(path_with_spaces=False)
diff --git a/conans/test/unittests/tools/gnu/autotoolschain_test.py b/conans/test/unittests/tools/gnu/autotoolschain_test.py
index b8f8a4d08..026acc933 100644
--- a/conans/test/unittests/tools/gnu/autotoolschain_test.py
+++ b/conans/test/unittests/tools/gnu/autotoolschain_test.py
@@ -1,9 +1,29 @@
+from unittest.mock import patch
+
 import pytest
 
 from conan.tools.gnu import AutotoolsToolchain
 from conans.errors import ConanException
 from conans.model.conf import Conf
 from conans.test.utils.mocks import ConanFileMock, MockSettings
+from conans.tools import args_to_string
+
+
+@pytest.fixture()
+def cross_building_conanfile():
+    settings_build = MockSettings({"os": "Linux",
+                                   "arch": "x86_64",
+                                   "compiler": "gcc",
+                                   "compiler.version": "11",
+                                   "compiler.libcxx": "libstdc++",
+                                   "build_type": "Release"})
+    settings_target = MockSettings({"os": "Android", "arch": "armv8"})
+    settings = MockSettings({"os": "Emscripten", "arch": "wasm"})
+    conanfile = ConanFileMock()
+    conanfile.settings = settings
+    conanfile.settings_build = settings_build
+    conanfile.settings_target = settings_target
+    return conanfile
 
 
 def test_get_gnu_triplet_for_cross_building():
@@ -139,3 +159,50 @@ def test_linker_scripts():
     env = autotoolschain.environment().vars(conanfile)
     assert "-T'path_to_first_linker_script'" in env["LDFLAGS"]
     assert "-T'path_to_second_linker_script'" in env["LDFLAGS"]
+
+
+@patch("conan.tools.gnu.autotoolstoolchain.save_toolchain_args")
+def test_check_configure_args_overwriting_and_deletion(save_args, cross_building_conanfile):
+    # Issue: https://github.com/conan-io/conan/issues/12642
+    at = AutotoolsToolchain(cross_building_conanfile)
+    at.configure_args.extend([
+        "--with-cross-build=my_path",
+        "--something-host=my_host"
+    ])
+    at.generate_args()
+    configure_args = save_args.call_args[0][0]['configure_args']
+    assert "--build=x86_64-linux-gnu" in configure_args
+    assert "--host=wasm32-local-emscripten" in configure_args
+    assert "--with-cross-build=my_path" in configure_args
+    assert "--something-host=my_host" in configure_args
+    # https://github.com/conan-io/conan/issues/12431
+    at.configure_args.remove("--build=x86_64-linux-gnu")
+    at.configure_args.remove("--host=wasm32-local-emscripten")
+    at.generate_args()
+    configure_args = save_args.call_args[0][0]['configure_args']
+    assert "--build=x86_64-linux-gnu" not in configure_args  # removed
+    assert "--host=wasm32-local-emscripten" not in configure_args  # removed
+    assert "--with-cross-build=my_path" in configure_args
+    assert "--something-host=my_host" in configure_args
+
+
+def test_update_or_prune_any_args(cross_building_conanfile):
+    at = AutotoolsToolchain(cross_building_conanfile)
+    at.configure_args.append("--enable-flag1=false")
+    at.make_args.append("--complex-flag=complex-value")
+    # Update configure_args
+    at.update_configure_args({"--prefix": "/my/other/prefix",
+                              "--build": None,  # prune value
+                              "--enable-flag1": ""})
+    new_configure_args = args_to_string(at.configure_args)
+    assert "--prefix=/my/other/prefix" in new_configure_args
+    assert "--build=" not in new_configure_args  # pruned
+    assert "--enable-flag1" in new_configure_args  # flag without value
+    # Update autoreconf_args
+    at.update_autoreconf_args({"--force": None})
+    new_autoreconf_args = args_to_string(at.autoreconf_args)
+    assert "'--force" not in new_autoreconf_args
+    # Update make_args
+    at.update_make_args({"--complex-flag": "new-value"})
+    new_make_args = args_to_string(at.make_args)
+    assert "--complex-flag=new-value" in new_make_args
diff --git a/conans/test/unittests/tools/microsoft/test_check_min_vs.py b/conans/test/unittests/tools/microsoft/test_check_min_vs.py
index bca089093..4810a6a81 100644
--- a/conans/test/unittests/tools/microsoft/test_check_min_vs.py
+++ b/conans/test/unittests/tools/microsoft/test_check_min_vs.py
@@ -7,6 +7,23 @@ from conans.test.utils.mocks import MockConanfile, MockSettings
 
 class TestCheckMinVS:
 
+    parametrize_vars = "compiler,version,update,minimum"
+    valid_parametrize_values = [
+        ("Visual Studio", "15", None, "191"),
+        ("Visual Studio", "16", None, "192"),
+        ("msvc", "193", None, "193"),
+        ("msvc", "193", None, "192"),
+        ("msvc", "193", "2", "193.1"),
+    ]
+
+    invalid_parametrize_values = [
+        ("Visual Studio", "15", None, "192"),
+        ("Visual Studio", "16", None, "193.1"),
+        ("msvc", "192", None, "193"),
+        ("msvc", "193", None, "193.1"),
+        ("msvc", "193", "1", "193.2"),
+    ]
+
     @staticmethod
     def _create_conanfile(compiler, version, update=None):
         settings = MockSettings({"compiler": compiler,
@@ -15,25 +32,23 @@ class TestCheckMinVS:
         conanfile = MockConanfile(settings)
         return conanfile
 
-    @pytest.mark.parametrize("compiler,version,update,minimum", [
-        ("Visual Studio", "15", None, "191"),
-        ("Visual Studio", "16", None, "192"),
-        ("msvc", "193", None, "193"),
-        ("msvc", "193", None, "192"),
-        ("msvc", "193", "2", "193.1"),
-    ])
+    @pytest.mark.parametrize(parametrize_vars, valid_parametrize_values)
     def test_valid(self, compiler, version, update, minimum):
         conanfile = self._create_conanfile(compiler, version, update)
         check_min_vs(conanfile, minimum)
 
-    @pytest.mark.parametrize("compiler,version,update,minimum", [
-        ("Visual Studio", "15", None, "192"),
-        ("Visual Studio", "16", None, "193.1"),
-        ("msvc", "192", None, "193"),
-        ("msvc", "193", None, "193.1"),
-        ("msvc", "193", "1", "193.2"),
-    ])
+    @pytest.mark.parametrize(parametrize_vars, valid_parametrize_values)
+    def test_valid_nothrows(self, compiler, version, update, minimum):
+        conanfile = self._create_conanfile(compiler, version, update)
+        assert check_min_vs(conanfile, minimum, raise_invalid=False)
+
+    @pytest.mark.parametrize(parametrize_vars, invalid_parametrize_values)
     def test_invalid(self, compiler, version, update, minimum):
         conanfile = self._create_conanfile(compiler, version, update)
         with pytest.raises(ConanInvalidConfiguration):
             check_min_vs(conanfile, minimum)
+
+    @pytest.mark.parametrize(parametrize_vars, invalid_parametrize_values)
+    def test_invalid_nothrows(self, compiler, version, update, minimum):
+        conanfile = self._create_conanfile(compiler, version, update)
+        assert not check_min_vs(conanfile, minimum, raise_invalid=False)