Repository: mesonbuild/meson

Issue:
Solving the LD issue
I think we (I) made a mistake with LD cross/native ld. We've caused a lot of regressions and our use of LD differs from what autotools does, which is confusing. I wonder if the best solutions is to (if IIRC) match autotools and go to a CCLD, CXXLD, etc environment variable, and remove LD. We could CCLD would only apply the C compiler, CXXLD would only apply to the C++ compiler, etc. If we want to go this route it would be a backwards incompatible change, and I'd like to land it for 0.53.1, with apologies. My plan would be that CCLD would work the same way the LD currently does, that is, if the C compiler is GCC like then the linker would be invoked with `$CC -fuse-ld=$CCLD`, for msvc it would be invoked as a binary, say `lld-link.exe`.

Is my understanding of autotools correct, and does this plan make sense? Are we willing to make a backwards incompatible change after the release?

Related #6413, #6431 

@eli-schwartz, @jpakkane, @textshell  (I think there was someone else on IRC who had opinions about this)


Discussion:
More specifically, autotools defines CCLD=$(CC), and also defines an LD variable... but linking binaries/libraries with gcc/g++ is always performed via libtool, by defining LINK as `libtool --tag=CC $(AM_LIBTOOLFLAGS)  $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) $(AM_LDFLAGS) $(LDFLAGS) -o $@` and then links with that.

libtool doesn't do anything with LD except for reload_cmds (ld -r). autotools expects you to define the linker via *FLAGS.

$CCLD doesn't do anything via the environment, it's overridden and internal to autotools so you cannot modify it AFAICT.
> $CCLD doesn't do anything via the environment, it's overridden and internal to autotools so you cannot modify it AFAICT.

So, either we don't need an env variable at all and just do this via config files (and you can do it by adding `-fuse-ld=bfd` to CFLAGS etc) or we can use CCLD without any fear of causing conflicts with autotools?
Are you planning only one variable or several separate variables (for consistency with [this](https://mesonbuild.com/Reference-tables.html#language-arguments-parameter-names) maybe with `_linker` or `_linker_id` suffix (e.g. `c_linker_id`, `cpp_linker_id`, `fortran_linker_id`, `objc_linker_id`, `objcpp_linker_id`))?
A single one, in keeping with the way you set the compiler,

```sh
CC=clang CCLD=lld meson build
```
or
```ini
[binaries]
c = '/usr/bin/clang'
cld = 'lld'
```

EDIT: the cross file originally had `cc` and `ccld`, but that was a typo.
In Gentoo, [`c` (not `cc`) and `cpp` are separately set in cross file](https://gitweb.gentoo.org/repo/gentoo.git/tree/eclass/meson.eclass?id=12688db23547abe05078bb4da81d2ec18a538c7f#n153).

If some project wanted to call linker directly (not through compiler driver and `-fuse-ld=`), what would be the way to select linker in Meson? `LD` environment variable or `ld` variable in cross file?
Sorry for the drive-by comment, but I'm currently traveling and have not had much sleep.

Anyway, an idea that came up is that we could keep the old approach by doing something like the following:

- choose linker based on LD or cross file, it points to an executable
- detect the linker type (`ld --version` etc)
- store that
- add a compiler getter to make that compiler use the given linker
- so for example gcc with bfd would return nothing, whereas gcc with gold would return `-fuse-ld=gold`
- if the compiler is given an impossible combination like VS compiler with Gold, error out

This would make the case where people do `find_program('ld')` to get the cross linker to do their own thing work.
>     * so for example gcc with bfd would return nothing, whereas gcc with gold would return `-fuse-ld=gold`

GNU Binutils can be [configured with `--enable-gold=default`](https://sourceware.org/git/?p=binutils-gdb.git;a=blob;f=configure.ac;h=544fab3d2097cb1e6bc572cc8c060baa9ec108b6;hb=HEAD#l307) which results in using GOLD linker by default, even without `-fuse-ld=gold`.
([Gentoo users can use above feature to decide which linker is used by default.](https://gitweb.gentoo.org/repo/gentoo.git/tree/sys-devel/binutils/binutils-2.33.1-r1.ebuild?id=06a60cd676554f19aac444144f4375d998b1fb50#n186))
@Arfrever, you're right about the cc -> c typo, I've fixed my comment. Sorry for the confusion.

> GNU Binutils can be configured with --enable-gold=default which results in using GOLD linker by default, even without -fuse-ld=gold.

It's actually even more complicated than that, because you can configure gcc to use a linker other than gnu ld (solaris does this)

Resolving patch:
diff --git a/mesonbuild/envconfig.py b/mesonbuild/envconfig.py
--- a/mesonbuild/envconfig.py
+++ b/mesonbuild/envconfig.py
@@ -305,11 +305,19 @@ def __init__(
         'rust': 'RUSTC',
         'vala': 'VALAC',
 
+        # Linkers
+        'c_ld': 'CC_LD',
+        'cpp_ld': 'CXX_LD',
+        'd_ld': 'D_LD',
+        'fortran_ld': 'F_LD',
+        'objc_ld': 'OBJC_LD',
+        'objcpp_ld': 'OBJCPP_LD',
+        'rust_ld': 'RUST_LD',
+
         # Binutils
         'strip': 'STRIP',
         'ar': 'AR',
         'windres': 'WINDRES',
-        'ld': 'LD',
 
         # Other tools
         'cmake': 'CMAKE',
diff --git a/mesonbuild/environment.py b/mesonbuild/environment.py
--- a/mesonbuild/environment.py
+++ b/mesonbuild/environment.py
@@ -749,7 +749,7 @@ def _guess_win_linker(self, compiler: T.List[str], comp_class: Compiler,
         check_args += self.coredata.compiler_options[for_machine][comp_class.language + '_args'].value
 
         override = []  # type: T.List[str]
-        value = self.binaries[for_machine].lookup_entry('ld')
+        value = self.binaries[for_machine].lookup_entry(comp_class.language + '_ld')
         if value is not None:
             override = comp_class.use_linker_args(value[0])
             check_args += override
@@ -812,7 +812,7 @@ def _guess_nix_linker(self, compiler: T.List[str], comp_class: T.Type[Compiler],
             check_args = comp_class.LINKER_PREFIX + ['--version'] + extra_args
 
         override = []  # type: T.List[str]
-        value = self.binaries[for_machine].lookup_entry('ld')
+        value = self.binaries[for_machine].lookup_entry(comp_class.language + '_ld')
         if value is not None:
             override = comp_class.use_linker_args(value[0])
             check_args += override
@@ -1355,6 +1355,7 @@ def detect_rust_compiler(self, for_machine):
 
         cc = self.detect_c_compiler(for_machine)
         is_link_exe = isinstance(cc.linker, VisualStudioLikeLinkerMixin)
+        override = self.binaries[for_machine].lookup_entry('rust_ld')
 
         for compiler in compilers:
             if isinstance(compiler, str):
@@ -1378,32 +1379,45 @@ def detect_rust_compiler(self, for_machine):
                 # the default use that, and second add the necessary arguments
                 # to rust to use -fuse-ld
 
-                extra_args = {}
-                always_args = []
-                if is_link_exe:
-                    compiler.extend(['-C', 'linker={}'.format(cc.linker.exelist[0])])
-                    extra_args['direct'] = True
-                    extra_args['machine'] = cc.linker.machine
-                elif not ((info.is_darwin() and isinstance(cc, AppleClangCCompiler)) or
-                          isinstance(cc, GnuCCompiler)):
+                if override is None:
+                    extra_args = {}
+                    always_args = []
+                    if is_link_exe:
+                        compiler.extend(['-C', 'linker={}'.format(cc.linker.exelist[0])])
+                        extra_args['direct'] = True
+                        extra_args['machine'] = cc.linker.machine
+                    elif not ((info.is_darwin() and isinstance(cc, AppleClangCCompiler)) or
+                              isinstance(cc, GnuCCompiler)):
+                        c = cc.exelist[1] if cc.exelist[0].endswith('ccache') else cc.exelist[0]
+                        compiler.extend(['-C', 'linker={}'.format(c)])
+
+                    # This trickery with type() gets us the class of the linker
+                    # so we can initialize a new copy for the Rust Compiler
+                    if is_link_exe:
+                        linker = type(cc.linker)(for_machine, always_args, exelist=cc.linker.exelist,
+                                                 version=cc.linker.version, **extra_args)
+                    else:
+                        linker = type(cc.linker)(compiler, for_machine, cc.linker.id, cc.LINKER_PREFIX,
+                                                 always_args=always_args, version=cc.linker.version,
+                                                 **extra_args)
+                elif 'link' in override[0]:
+                    linker = self._guess_win_linker(
+                        override, RustCompiler, for_machine, use_linker_prefix=False)
+                    linker.direct = True
+                else:
+                    # We're creating a new type of "C" compiler, that has rust
+                    # as it's language. This is gross, but I can't figure out
+                    # another way to handle this, because rustc is actually
+                    # invoking the c compiler as it's linker.
+                    b = type('b', (type(cc), ), {})
+                    b.language = RustCompiler.language
+                    linker = self._guess_nix_linker(cc.exelist, b, for_machine)
+
+                    # Of course, we're not going to use any of that, we just
+                    # need it to get the proper arguments to pass to rustc
                     c = cc.exelist[1] if cc.exelist[0].endswith('ccache') else cc.exelist[0]
                     compiler.extend(['-C', 'linker={}'.format(c)])
-
-                value = self.binaries[for_machine].lookup_entry('ld')
-                if value is not None:
-                    for a in cc.use_linker_args(value[0]):
-                        always_args.extend(['-C', 'link-arg={}'.format(a)])
-
-                # This trickery with type() gets us the class of the linker
-                # so we can initialize a new copy for the Rust Compiler
-
-                if is_link_exe:
-                    linker = type(cc.linker)(for_machine, always_args, exelist=cc.linker.exelist,
-                                             version=cc.linker.version, **extra_args)
-                else:
-                    linker = type(cc.linker)(compiler, for_machine, cc.linker.id, cc.LINKER_PREFIX,
-                                             always_args=always_args, version=cc.linker.version,
-                                             **extra_args)
+                    compiler.extend(['-C', 'link-args={}'.format(' '.join(cc.use_linker_args(override[0])))])
 
                 return RustCompiler(
                     compiler, version, for_machine, is_cross, info, exe_wrap,
diff --git a/run_unittests.py b/run_unittests.py
--- a/run_unittests.py
+++ b/run_unittests.py
@@ -43,6 +43,7 @@
 import mesonbuild.mlog
 import mesonbuild.depfile
 import mesonbuild.compilers
+import mesonbuild.envconfig
 import mesonbuild.environment
 import mesonbuild.mesonlib
 import mesonbuild.coredata
@@ -4626,7 +4627,8 @@ def test_install_pdb_introspection(self):
     def _check_ld(self, name: str, lang: str, expected: str) -> None:
         if not shutil.which(name):
             raise unittest.SkipTest('Could not find {}.'.format(name))
-        with mock.patch.dict(os.environ, {'LD': name}):
+        envvar = mesonbuild.envconfig.BinaryTable.evarMap['{}_ld'.format(lang)]
+        with mock.patch.dict(os.environ, {envvar: name}):
             env = get_fake_env()
             try:
                 comp = getattr(env, 'detect_{}_compiler'.format(lang))(MachineChoice.HOST)
@@ -5878,7 +5880,8 @@ def _check_ld(self, check: str, name: str, lang: str, expected: str) -> None:
             raise unittest.SkipTest('Solaris currently cannot override the linker.')
         if not shutil.which(check):
             raise unittest.SkipTest('Could not find {}.'.format(check))
-        with mock.patch.dict(os.environ, {'LD': name}):
+        envvar = mesonbuild.envconfig.BinaryTable.evarMap['{}_ld'.format(lang)]
+        with mock.patch.dict(os.environ, {envvar: name}):
             env = get_fake_env()
             comp = getattr(env, 'detect_{}_compiler'.format(lang))(MachineChoice.HOST)
             if lang != 'rust' and comp.use_linker_args('foo') == []:


Test patch:
diff --git a/test cases/linuxlike/15 ld binary/meson.build b/test cases/linuxlike/15 ld binary/meson.build
new file mode 100644
--- /dev/null
+++ b/test cases/linuxlike/15 ld binary/meson.build	
@@ -0,0 +1,4 @@
+project('ld binary')
+
+ld = find_program('ld')
+assert(run_command(ld, '--version').returncode() == 0)