Repository: conan-io/conan

Issue:
[feature] CMakeToolchain support for linker scripts
When building an ELF executable for bare-metal targets it is normal to include a linker script to define the memory layout of the specific target microcontroller.
It is possible to use multiple linker scripts, too.
Each one is passed with the `-T` flag when using GCC.
I don't think this actually needs to be applied to dependencies, only the final executable that will be loaded on the microcontroller.
It would be great if there was an easy way to configure this for the CMakeToolchain generator.
If it only needs to be applied to the consumer, perhaps the CMakeToolchain generator could define a dedicated block for this.
I'm not sure if there should be additional ways to configure linker scripts, but there might be some good ideas out there.

- [X] I've read the [CONTRIBUTING guide](https://github.com/conan-io/conan/blob/develop/.github/CONTRIBUTING.md).



Discussion:
I'd like to +1 this. It would be great if the linker scripts can be exposed in some way. It would be even better if you can select out of a set of possible linker scripts. For example, my library for LPC40XX mcu has 5 different variants that I'd like to somehow choose at configuration time for my project.
@kammce If it helps, I currently use a `python_requires` package that provides the linker script as a file which the consumer can than add to their CMakeToolchain. My rough implementation included below adds a new block for the CMakeToolchain generator and copies the linker script directly in to the build directory for easy access.

Linker Script `python_requires` `conanfile.py`:

```python
import os
from pathlib import Path
from shutil import copy2
import textwrap
from conans import ConanFile
from conan.tools.cmake import CMakeToolchain
from conan.tools.cmake.toolchain import Block


LINKER_SCRIPT_FILENAME = "MyLinkerScript.ld"


class LinkerScriptBlock(Block):
    template = textwrap.dedent(
        """
        set(CMAKE_EXE_LINKER_FLAGS_INIT "${CMAKE_EXE_LINKER_FLAGS_INIT} -T '${CMAKE_CURRENT_LIST_DIR}/MyLinkerScript.ld'")
        """
    )


def add_linker_script(
    tc: CMakeToolchain, module_directory: Path, destination_directory: Path
) -> CMakeToolchain:
    copy2(os.path.join(module_directory, LINKER_SCRIPT_FILENAME), destination_directory)
    tc.blocks["linker_script"] = LinkerScriptBlock
    return tc


class MyLinkerScript(ConanFile):
    name = "my-linker-script"
    description = "My very descriptive description."
    exports = LINKER_SCRIPT_FILENAME
```

Consumer's `conanfile.py`:

```python
import os
from conan.tools.cmake import CMakeToolchain

class MyEmbeddedApp(ConanFile):
    python_requires = 'my-linker-script/1.0.0'

    def generate(self):
        tc = CMakeToolchain(self)
        if self.settings.os == "baremetal":
            linker_script_package_path = self.python_requires["'my-linker-script"].path
            tc = self.python_requires[
                "my-linker-script"
            ].module.add_linker_script(tc, linker_script_package_path, os.getcwd())
        tc.generate()
```
I see, yes, maybe a built-in "LinkerScripts" block in ``CMakeToolchain`` can simplify a bit the above:

- This block would be empty by default
- Only users adding the path to a linker script would activate it
- It should probably define better CONAN_ var than CMAKE_ var
- The definition of the linker script could come from different places, from the current recipe itself, or it could be provided by the python_requires, but for the toolchain, it should be agnostic.

Sounds a relatively easy feature to contribute if anyone wants to give it a try.

In future works we could add some [conf] maybe to define this from profiles too, but probably better develop first this part.
Just a note that allowing multiple linker scripts would be fantastic.

Patch:
diff --git a/conan/tools/cmake/toolchain/blocks.py b/conan/tools/cmake/toolchain/blocks.py
--- a/conan/tools/cmake/toolchain/blocks.py
+++ b/conan/tools/cmake/toolchain/blocks.py
@@ -215,6 +215,21 @@ def context(self):
         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
--- a/conan/tools/cmake/toolchain/toolchain.py
+++ b/conan/tools/cmake/toolchain/toolchain.py
@@ -12,7 +12,7 @@
 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 @@ def __init__(self, conanfile, generator=None):
                                        ("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
--- a/conan/tools/gnu/autotoolstoolchain.py
+++ b/conan/tools/gnu/autotoolstoolchain.py
@@ -113,6 +113,8 @@ def ldflags(self):
                                               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
--- a/conan/tools/meson/toolchain.py
+++ b/conan/tools/meson/toolchain.py
@@ -291,10 +291,12 @@ def _get_extra_flags(self):
         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/conans/model/conf.py b/conans/model/conf.py
--- a/conans/model/conf.py
+++ b/conans/model/conf.py
@@ -54,6 +54,7 @@
     "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
--- a/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py
+++ b/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py
@@ -902,6 +902,27 @@ def layout(self):
                                        "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
--- 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
--- 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/unittests/tools/cmake/test_cmaketoolchain.py b/conans/test/unittests/tools/cmake/test_cmaketoolchain.py
--- 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
--- 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"]