Repository: conan-io/conan
Pull request #12882 (2023-01-10 21:39:13)

allow skip the generation of conanvcvars.bat
Changelog: Feature: allow skip the generation of conanvcvars.bat with ``tools.microsoft.msbuild:installation_path=disabled``
Docs: https://github.com/conan-io/docs/pull/XXXX

I am proposing the use of ``tools.microsoft.msbuild:installation_path=disabled'`` to skip the generation of the conanvcvars.bat. I am reusing the ``tools.microsoft.msbuild:installation_path`` conf, because it is used exclusively already for the ``VCVars`` functionality, and to avoid the proliferation of tons of ``confs`` for minor functionalities like this one.

Close https://github.com/conan-io/conan/issues/12855


czoido: I think it's good to reuse the conf for this but I was wondering if we could use an empty path value as the indication to skip the file generation, like:

```
tools.microsoft.msbuild:installation_path=""
```

I'm saying that because if we reuse more confs to do a similar thing it's easier to know that you have to set an empty value than using a keyword like "disabled"

WDYT?
memsharded: > Also, did you consider disabling this by setting the conf to an empty string? (Just as https://github.com/conan-io/conan/pull/12868 proves it to be possible) And why did you decide on the disabled way? Less explicit?

yes, I was thinking of making it more explicit, the empty string meaning seems a bit less clear to me.
Some similar "disabling" syntax in Conan:
``--test-folder=None`` reads from cli ``"None"`` as a string, and disables ``test_package``. It has always read dirty to me, ``--test-folder=disabled`` would be a bit nicer (the likelihood of having a subfolder called ``"disabled"`` is a bit higher than ``"None"``, but should be relatively low. In theory, the more correct approach is a new arg ``--disable-test-folder``, I guess this was done to avoid the explosion of command cli args, that are already too many.

Patch:
diff --git a/conan/tools/cmake/toolchain/blocks.py b/conan/tools/cmake/toolchain/blocks.py
index 5846f0097..60d9e5759 100644
--- a/conan/tools/cmake/toolchain/blocks.py
+++ b/conan/tools/cmake/toolchain/blocks.py
@@ -215,6 +215,21 @@ class ArchitectureBlock(Block):
         return {"arch_flag": arch_flag}
 
 
+class LinkerScriptsBlock(Block):
+    template = textwrap.dedent("""
+        string(APPEND CONAN_EXE_LINKER_FLAGS {{ linker_script_flags }})
+        """)
+
+    def context(self):
+        linker_scripts = self._conanfile.conf.get(
+            "tools.build:linker_scripts", check_type=list, default=[])
+        if not linker_scripts:
+            return
+        linker_scripts = [linker_script.replace('\\', '/') for linker_script in linker_scripts]
+        linker_script_flags = ['-T"' + linker_script + '"' for linker_script in linker_scripts]
+        return {"linker_script_flags": " ".join(linker_script_flags)}
+
+
 class CppStdBlock(Block):
     template = textwrap.dedent("""
         message(STATUS "Conan toolchain: C++ Standard {{ cppstd }} with extensions {{ cppstd_extensions }}")
diff --git a/conan/tools/cmake/toolchain/toolchain.py b/conan/tools/cmake/toolchain/toolchain.py
index 2a0e181f8..0bfb04bff 100644
--- a/conan/tools/cmake/toolchain/toolchain.py
+++ b/conan/tools/cmake/toolchain/toolchain.py
@@ -12,7 +12,7 @@ from conan.tools.cmake.toolchain import CONAN_TOOLCHAIN_FILENAME
 from conan.tools.cmake.toolchain.blocks import ToolchainBlocks, UserToolchain, GenericSystemBlock, \
     AndroidSystemBlock, AppleSystemBlock, FPicBlock, ArchitectureBlock, GLibCXXBlock, VSRuntimeBlock, \
     CppStdBlock, ParallelBlock, CMakeFlagsInitBlock, TryCompileBlock, FindFiles, PkgConfigBlock, \
-    SkipRPath, SharedLibBock, OutputDirsBlock, ExtraFlagsBlock, CompilersBlock
+    SkipRPath, SharedLibBock, OutputDirsBlock, ExtraFlagsBlock, CompilersBlock, LinkerScriptsBlock
 from conan.tools.intel import IntelCC
 from conan.tools.microsoft import VCVars
 from conan.tools.microsoft.visual import vs_ide_version
@@ -133,6 +133,7 @@ class CMakeToolchain(object):
                                        ("apple_system", AppleSystemBlock),
                                        ("fpic", FPicBlock),
                                        ("arch_flags", ArchitectureBlock),
+                                       ("linker_scripts", LinkerScriptsBlock),
                                        ("libcxx", GLibCXXBlock),
                                        ("vs_runtime", VSRuntimeBlock),
                                        ("cppstd", CppStdBlock),
diff --git a/conan/tools/gnu/autotoolstoolchain.py b/conan/tools/gnu/autotoolstoolchain.py
index 58115a607..0a8914df4 100644
--- a/conan/tools/gnu/autotoolstoolchain.py
+++ b/conan/tools/gnu/autotoolstoolchain.py
@@ -113,6 +113,8 @@ class AutotoolsToolchain:
                                               check_type=list)
         conf_flags.extend(self._conanfile.conf.get("tools.build:exelinkflags", default=[],
                                                    check_type=list))
+        linker_scripts = self._conanfile.conf.get("tools.build:linker_scripts", default=[], check_type=list)
+        conf_flags.extend(["-T'" + linker_script + "'" for linker_script in linker_scripts])
         ret = ret + apple_flags + conf_flags + self.build_type_link_flags + self.extra_ldflags
         return self._filter_list_empty_fields(ret)
 
diff --git a/conan/tools/meson/toolchain.py b/conan/tools/meson/toolchain.py
index 17f553242..be0a53593 100644
--- a/conan/tools/meson/toolchain.py
+++ b/conan/tools/meson/toolchain.py
@@ -291,10 +291,12 @@ class MesonToolchain(object):
         cflags = self._conanfile.conf.get("tools.build:cflags", default=[], check_type=list)
         sharedlinkflags = self._conanfile.conf.get("tools.build:sharedlinkflags", default=[], check_type=list)
         exelinkflags = self._conanfile.conf.get("tools.build:exelinkflags", default=[], check_type=list)
+        linker_scripts = self._conanfile.conf.get("tools.build:linker_scripts", default=[], check_type=list)
+        linker_script_flags = ['-T"' + linker_script + '"' for linker_script in linker_scripts]
         return {
             "cxxflags": cxxflags,
             "cflags": cflags,
-            "ldflags": sharedlinkflags + exelinkflags
+            "ldflags": sharedlinkflags + exelinkflags + linker_script_flags
         }
 
     @staticmethod
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 62f91da8d..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",
@@ -54,6 +54,7 @@ BUILT_IN_CONFS = {
     "tools.build:sharedlinkflags": "List of extra flags used by CMakeToolchain for CMAKE_SHARED_LINKER_FLAGS_INIT variable",
     "tools.build:exelinkflags": "List of extra flags used by CMakeToolchain for CMAKE_EXE_LINKER_FLAGS_INIT variable",
     "tools.build:compiler_executables": "Defines a Python dict-like with the compilers path to be used. Allowed keys {'c', 'cpp', 'cuda', 'objc', 'objcpp', 'rc', 'fortran', 'asm', 'hip', 'ispc'}",
+    "tools.build:linker_scripts": "List of linker script files to pass to the linker used by different toolchains like CMakeToolchain, AutotoolsToolchain, and MesonToolchain",
     "tools.microsoft.bash:subsystem": "Set subsystem to use for Windows. Possible values: 'msys2', 'msys', 'cygwin', 'wsl' and 'sfu'",
     "tools.microsoft.bash:path": "Path to the shell executable. Default: 'bash'",
     "tools.apple:sdk_path": "Path for the sdk location. This value will be passed as SDKROOT or -isysroot depending on the generator used",


Test patch:
diff --git a/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py b/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py
index dcbc046aa..7f2bac7fb 100644
--- a/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py
+++ b/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py
@@ -902,6 +902,27 @@ def test_cmake_layout_toolchain_folder():
                                        "build/x86-debug/generators/conan_toolchain.cmake"))
 
 
+def test_set_linker_scripts():
+    profile = textwrap.dedent(r"""
+    [settings]
+    os=Windows
+    arch=x86_64
+    compiler=clang
+    compiler.version=15
+    compiler.libcxx=libstdc++11
+    [conf]
+    tools.build:linker_scripts=["/usr/local/src/flash.ld", "C:\\local\\extra_data.ld"]
+    """)
+    client = TestClient(path_with_spaces=False)
+    conanfile = GenConanfile().with_settings("os", "arch", "compiler")\
+        .with_generator("CMakeToolchain")
+    client.save({"conanfile.py": conanfile,
+                "profile": profile})
+    client.run("install . -pr:b profile -pr:h profile")
+    toolchain = client.load("conan_toolchain.cmake")
+    assert 'string(APPEND CONAN_EXE_LINKER_FLAGS -T"/usr/local/src/flash.ld" -T"C:/local/extra_data.ld")' in toolchain
+
+
 def test_test_package_layout():
     """
     test that the ``test_package`` folder also follows the cmake_layout and the
diff --git a/conans/test/integration/toolchains/gnu/test_autotoolstoolchain.py b/conans/test/integration/toolchains/gnu/test_autotoolstoolchain.py
index 9c91d3c8e..4bc28f2fe 100644
--- a/conans/test/integration/toolchains/gnu/test_autotoolstoolchain.py
+++ b/conans/test/integration/toolchains/gnu/test_autotoolstoolchain.py
@@ -45,6 +45,36 @@ def test_extra_flags_via_conf():
         assert 'export LDFLAGS="$LDFLAGS --flag5 --flag6"' in toolchain
 
 
+def test_linker_scripts_via_conf():
+    os_ = platform.system()
+    os_ = "Macos" if os_ == "Darwin" else os_
+
+    profile = textwrap.dedent("""
+        [settings]
+        os=%s
+        compiler=gcc
+        compiler.version=6
+        compiler.libcxx=libstdc++11
+        arch=armv8
+        build_type=Release
+
+        [conf]
+        tools.build:sharedlinkflags+=["--flag5"]
+        tools.build:exelinkflags+=["--flag6"]
+        tools.build:linker_scripts+=["/linker/scripts/flash.ld", "/linker/scripts/extra_data.ld"]
+        """ % os_)
+    client = TestClient()
+    conanfile = GenConanfile().with_settings("os", "arch", "compiler", "build_type")\
+        .with_generator("AutotoolsToolchain")
+    client.save({"conanfile.py": conanfile,
+                "profile": profile})
+    client.run("install . --profile:build=profile --profile:host=profile")
+    toolchain = client.load("conanautotoolstoolchain{}".format('.bat' if os_ == "Windows" else '.sh'))
+    if os_ == "Windows":
+        assert 'set "LDFLAGS=%LDFLAGS% --flag5 --flag6 -T\'/linker/scripts/flash.ld\' -T\'/linker/scripts/extra_data.ld\'"' in toolchain
+    else:
+        assert 'export LDFLAGS="$LDFLAGS --flag5 --flag6 -T\'/linker/scripts/flash.ld\' -T\'/linker/scripts/extra_data.ld\'"' in toolchain
+
 def test_not_none_values():
 
     conanfile = textwrap.dedent("""
diff --git a/conans/test/integration/toolchains/meson/test_mesontoolchain.py b/conans/test/integration/toolchains/meson/test_mesontoolchain.py
index 7a1932fb9..d360c1706 100644
--- a/conans/test/integration/toolchains/meson/test_mesontoolchain.py
+++ b/conans/test/integration/toolchains/meson/test_mesontoolchain.py
@@ -92,6 +92,35 @@ def test_extra_flags_via_conf():
     assert "cpp_link_args = ['-flag0', '-other=val', '-flag5', '-flag6']" in content
 
 
+def test_linker_scripts_via_conf():
+    profile = textwrap.dedent("""
+        [settings]
+        os=Windows
+        arch=x86_64
+        compiler=gcc
+        compiler.version=9
+        compiler.cppstd=17
+        compiler.libcxx=libstdc++
+        build_type=Release
+
+        [buildenv]
+        LDFLAGS=-flag0 -other=val
+
+        [conf]
+        tools.build:sharedlinkflags+=["-flag5"]
+        tools.build:exelinkflags+=["-flag6"]
+        tools.build:linker_scripts+=["/linker/scripts/flash.ld", "/linker/scripts/extra_data.ld"]
+   """)
+    t = TestClient()
+    t.save({"conanfile.txt": "[generators]\nMesonToolchain",
+            "profile": profile})
+
+    t.run("install . -pr=profile")
+    content = t.load(MesonToolchain.native_filename)
+    assert "c_link_args = ['-flag0', '-other=val', '-flag5', '-flag6', '-T\"/linker/scripts/flash.ld\"', '-T\"/linker/scripts/extra_data.ld\"']" in content
+    assert "cpp_link_args = ['-flag0', '-other=val', '-flag5', '-flag6', '-T\"/linker/scripts/flash.ld\"', '-T\"/linker/scripts/extra_data.ld\"']" in content
+
+
 def test_correct_quotes():
     profile = textwrap.dedent("""
        [settings]
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/cmake/test_cmaketoolchain.py b/conans/test/unittests/tools/cmake/test_cmaketoolchain.py
index d66be8732..25776873e 100644
--- a/conans/test/unittests/tools/cmake/test_cmaketoolchain.py
+++ b/conans/test/unittests/tools/cmake/test_cmaketoolchain.py
@@ -505,3 +505,10 @@ def test_compilers_block(conanfile):
     content = toolchain.content
     for compiler, lang in cmake_mapping.items():
         assert f'set(CMAKE_{lang}_COMPILER "path_to_{compiler}")' in content
+
+
+def test_linker_scripts_block(conanfile):
+    conanfile.conf.define("tools.build:linker_scripts", ["path_to_first_linker_script", "path_to_second_linker_script"])
+    toolchain = CMakeToolchain(conanfile)
+    content = toolchain.content
+    assert f'string(APPEND CONAN_EXE_LINKER_FLAGS -T"path_to_first_linker_script" -T"path_to_second_linker_script")' in content
diff --git a/conans/test/unittests/tools/gnu/autotoolschain_test.py b/conans/test/unittests/tools/gnu/autotoolschain_test.py
index 93b7d31ec..b8f8a4d08 100644
--- a/conans/test/unittests/tools/gnu/autotoolschain_test.py
+++ b/conans/test/unittests/tools/gnu/autotoolschain_test.py
@@ -125,3 +125,17 @@ def test_compilers_mapping():
     env = autotoolschain.environment().vars(conanfile)
     for compiler, env_var in autotools_mapping.items():
         assert env[env_var] == f"path_to_{compiler}"
+
+
+def test_linker_scripts():
+    conanfile = ConanFileMock()
+    conanfile.conf = Conf()
+    conanfile.conf.define("tools.build:linker_scripts", ["path_to_first_linker_script", "path_to_second_linker_script"])
+    settings = MockSettings({"build_type": "Release",
+                             "os": "Windows",
+                             "arch": "x86_64"})
+    conanfile.settings = settings
+    autotoolschain = AutotoolsToolchain(conanfile)
+    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"]
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)