diff options
Diffstat (limited to 'mesonbuild/linkers.py')
-rw-r--r-- | mesonbuild/linkers.py | 698 |
1 files changed, 696 insertions, 2 deletions
diff --git a/mesonbuild/linkers.py b/mesonbuild/linkers.py index dc9a825..a641cd0 100644 --- a/mesonbuild/linkers.py +++ b/mesonbuild/linkers.py @@ -12,6 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +import abc +import os +import shlex import typing from . import mesonlib @@ -51,8 +54,9 @@ class StaticLinker: def get_coverage_link_args(self) -> typing.List[str]: return [] - def build_rpath_args(self, build_dir: str, from_dir: str, rpath_paths: str, - build_rpath: str, install_rpath: str) -> typing.List[str]: + def build_rpath_args(self, env: 'Environment', build_dir: str, from_dir: str, + rpath_paths: str, build_rpath: str, + install_rpath: str) -> typing.List[str]: return [] def thread_link_flags(self, env: 'Environment') -> typing.List[str]: @@ -189,3 +193,693 @@ class CcrxLinker(StaticLinker): def get_linker_always_args(self) -> typing.List[str]: return ['-nologo', '-form=library'] + + +def prepare_rpaths(raw_rpaths: str, build_dir: str, from_dir: str) -> typing.List[str]: + # The rpaths we write must be relative if they point to the build dir, + # because otherwise they have different length depending on the build + # directory. This breaks reproducible builds. + internal_format_rpaths = [evaluate_rpath(p, build_dir, from_dir) for p in raw_rpaths] + ordered_rpaths = order_rpaths(internal_format_rpaths) + return ordered_rpaths + + +def order_rpaths(rpath_list: typing.List[str]) -> typing.List[str]: + # We want rpaths that point inside our build dir to always override + # those pointing to other places in the file system. This is so built + # binaries prefer our libraries to the ones that may lie somewhere + # in the file system, such as /lib/x86_64-linux-gnu. + # + # The correct thing to do here would be C++'s std::stable_partition. + # Python standard library does not have it, so replicate it with + # sort, which is guaranteed to be stable. + return sorted(rpath_list, key=os.path.isabs) + + +def evaluate_rpath(p: str, build_dir: str, from_dir: str) -> str: + if p == from_dir: + return '' # relpath errors out in this case + elif os.path.isabs(p): + return p # These can be outside of build dir. + else: + return os.path.relpath(os.path.join(build_dir, p), os.path.join(build_dir, from_dir)) + + +class DynamicLinker(metaclass=abc.ABCMeta): + + """Base class for dynamic linkers.""" + + _BUILDTYPE_ARGS = { + 'plain': [], + 'debug': [], + 'debugoptimized': [], + 'release': [], + 'minsize': [], + 'custom': [], + } # type: typing.Dict[str, typing.List[str]] + + def __init__(self, exelist: typing.List[str], for_machine: mesonlib.MachineChoice, + id_: str, *, version: str = 'unknown version'): + self.exelist = exelist + self.for_machine = for_machine + self.version = version + self.id = id_ + + def __repr__(self) -> str: + return '<{}: v{} `{}`>'.format(type(self).__name__, self.version, ' '.join(self.exelist)) + + def get_id(self) -> str: + return self.id + + def get_version_string(self) -> str: + return '({} {})'.format(self.id, self.version) + + def get_exelist(self) -> typing.List[str]: + return self.exelist.copy() + + def get_accepts_rsp(self) -> bool: + # TODO: is it really a matter of is_windows or is it for_windows? + return mesonlib.is_windows() + + def get_always_args(self) -> typing.List[str]: + return [] + + def get_lib_prefix(self) -> str: + return '' + + # XXX: is use_ldflags a compiler or a linker attribute? + + def get_args_from_envvars(self) -> typing.List[str]: + flags = os.environ.get('LDFLAGS') + if not flags: + return [] + return shlex.split(flags) + + def get_option_args(self, options: 'OptionDictType') -> typing.List[str]: + return [] + + def has_multi_arguments(self, args: typing.List[str], env: 'Environment') -> typing.Tuple[bool, bool]: + m = 'Language {} does not support has_multi_link_arguments.' + raise mesonlib.EnvironmentException(m.format(self.id)) + + def get_debugfile_args(self, targetfile: str) -> typing.List[str]: + """Some compilers (MSVC) write debug into a separate file. + + This method takes the target object path and returns a list of + commands to append to the linker invocation to control where that + file is written. + """ + return [] + + def get_std_shared_lib_args(self) -> typing.List[str]: + return [] + + def get_std_shared_module_args(self, options: 'OptionDictType') -> typing.List[str]: + return self.get_std_shared_lib_args() + + def get_pie_args(self) -> typing.List[str]: + # TODO: this really needs to take a boolean and return the args to + # disable pie, otherwise it only acts to enable pie if pie *isn't* the + # default. + m = 'Linker {} does not support position-independent executable' + raise mesonlib.EnvironmentException(m.format(self.id)) + + def get_lto_args(self) -> typing.List[str]: + return [] + + def sanitizer_args(self, value: str) -> typing.List[str]: + return [] + + def get_buildtype_args(self, buildtype: str) -> typing.List[str]: + # We can override these in children by just overriding the + # _BUILDTYPE_ARGS value. + return self._BUILDTYPE_ARGS[buildtype] + + def get_asneeded_args(self) -> typing.List[str]: + return [] + + def get_link_whole_for(self, args: typing.List[str]) -> typing.List[str]: + raise mesonlib.EnvironmentException( + 'Linker {} does not support link_whole'.format(self.id)) + + def get_allow_undefined_args(self) -> typing.List[str]: + raise mesonlib.EnvironmentException( + 'Linker {} does not support allow undefined'.format(self.id)) + + def invoked_by_compiler(self) -> bool: + """True if meson uses the compiler to invoke the linker.""" + return True + + @abc.abstractmethod + def get_output_args(self, outname: str) -> typing.List[str]: + pass + + def get_coverage_args(self) -> typing.List[str]: + m = "Linker {} doesn't implement coverage data generation.".format(self.id) + raise mesonlib.EnvironmentException(m) + + @abc.abstractmethod + def get_search_args(self, dirname: str) -> typing.List[str]: + pass + + def export_dynamic_args(self, env: 'Environment') -> typing.List[str]: + return [] + + def import_library_args(self, implibname: str) -> typing.List[str]: + """The name of the outputted import library. + + This implementation is used only on Windows by compilers that use GNU ld + """ + return [] + + def thread_flags(self, env: 'Environment') -> typing.List[str]: + return [] + + def no_undefined_args(self) -> typing.List[str]: + """Arguments to error if there are any undefined symbols at link time. + + This is the inverse of get_allow_undefined_args(). + + TODO: A future cleanup might merge this and + get_allow_undefined_args() into a single method taking a + boolean + """ + return [] + + def fatal_warnings(self) -> typing.List[str]: + """Arguments to make all warnings errors.""" + return [] + + def bitcode_args(self) -> typing.List[str]: + raise mesonlib.MesonException('This linker does not support bitcode bundles') + + def get_debug_crt_args(self) -> typing.List[str]: + return [] + + def build_rpath_args(self, env: 'Environment', build_dir: str, from_dir: str, + rpath_paths: str, build_rpath: str, + install_rpath: str) -> typing.List[str]: + return [] + + +class PosixDynamicLinkerMixin: + + """Mixin class for POSIX-ish linkers. + + This is obviously a pretty small subset of the linker interface, but + enough dynamic linkers that meson supports are POSIX-like but not + GNU-like that it makes sense to split this out. + """ + + def get_output_args(self, outname: str) -> typing.List[str]: + return ['-o', outname] + + def get_std_shared_lib_args(self) -> typing.List[str]: + return ['-shared'] + + def get_search_args(self, dirname: str) -> typing.List[str]: + return ['-L', dirname] + + +class GnuLikeDynamicLinkerMixin: + + """Mixin class for dynamic linkers that provides gnu-like interface. + + This acts as a base for the GNU linkers (bfd and gold), the Intel Xild + (which comes with ICC), LLVM's lld, and other linkers like GNU-ld. + """ + + _BUILDTYPE_ARGS = { + 'plain': [], + 'debug': [], + 'debugoptimized': [], + 'release': ['-Wl,-O1'], + 'minsize': [], + 'custom': [], + } # type: typing.Dict[str, typing.List[str]] + + def get_pie_args(self) -> typing.List[str]: + return ['-pie'] + + def get_asneeded_args(self) -> typing.List[str]: + return ['-Wl,--as-needed'] + + def get_link_whole_for(self, args: typing.List[str]) -> typing.List[str]: + if not args: + return args + return ['-Wl,--whole-archive'] + args + ['-Wl,--no-whole-archive'] + + def get_allow_undefined_args(self) -> typing.List[str]: + return ['-Wl,--allow-shlib-undefined'] + + def get_lto_args(self) -> typing.List[str]: + return ['-flto'] + + def sanitizer_args(self, value: str) -> typing.List[str]: + if value == 'none': + return [] + return ['-fsanitize=' + value] + + def invoked_by_compiler(self) -> bool: + """True if meson uses the compiler to invoke the linker.""" + return True + + def get_coverage_args(self) -> typing.List[str]: + return ['--coverage'] + + def export_dynamic_args(self, env: 'Environment') -> typing.List[str]: + m = env.machines[self.for_machine] + if m.is_windows() or m.is_cygwin(): + return ['-Wl,--export-all-symbols'] + return ['-Wl,-export-dynamic'] + + def import_library_args(self, implibname: str) -> typing.List[str]: + return ['-Wl,--out-implib=' + implibname] + + def thread_flags(self, env: 'Environment') -> typing.List[str]: + if env.machines[self.for_machine].is_haiku(): + return [] + return ['-pthread'] + + def no_undefined_args(self) -> typing.List[str]: + return ['-Wl,--no-undefined'] + + def fatal_warnings(self) -> typing.List[str]: + return ['-Wl,--fatal-warnings'] + + def get_soname_args(self, env: 'Environment', prefix: str, shlib_name: str, + suffix: str, soversion: str, darwin_versions: typing.Tuple[str, str], + is_shared_module: bool) -> typing.List[str]: + m = env.machines[self.for_machine] + if m.is_windows() or m.is_cygwin(): + # For PE/COFF the soname argument has no effect + return [] + sostr = '' if soversion is None else '.' + soversion + return ['-Wl,-soname,{}{}.{}{}'.format(prefix, shlib_name, suffix, sostr)] + + def build_rpath_args(self, env: 'Environment', build_dir: str, from_dir: str, + rpath_paths: str, build_rpath: str, + install_rpath: str) -> typing.List[str]: + m = env.machines[self.for_machine] + if m.is_windows() or m.is_cygwin(): + return [] + if not rpath_paths and not install_rpath and not build_rpath: + return [] + args = [] + origin_placeholder = '$ORIGIN' + processed_rpaths = prepare_rpaths(rpath_paths, build_dir, from_dir) + # Need to deduplicate rpaths, as macOS's install_name_tool + # is *very* allergic to duplicate -delete_rpath arguments + # when calling depfixer on installation. + all_paths = mesonlib.OrderedSet([os.path.join(origin_placeholder, p) for p in processed_rpaths]) + # Build_rpath is used as-is (it is usually absolute). + if build_rpath != '': + all_paths.add(build_rpath) + + # TODO: should this actually be "for (dragonfly|open)bsd"? + if mesonlib.is_dragonflybsd() or mesonlib.is_openbsd(): + # This argument instructs the compiler to record the value of + # ORIGIN in the .dynamic section of the elf. On Linux this is done + # by default, but is not on dragonfly/openbsd for some reason. Without this + # $ORIGIN in the runtime path will be undefined and any binaries + # linked against local libraries will fail to resolve them. + args.append('-Wl,-z,origin') + + # In order to avoid relinking for RPATH removal, the binary needs to contain just + # enough space in the ELF header to hold the final installation RPATH. + paths = ':'.join(all_paths) + if len(paths) < len(install_rpath): + padding = 'X' * (len(install_rpath) - len(paths)) + if not paths: + paths = padding + else: + paths = paths + ':' + padding + args.append('-Wl,-rpath,' + paths) + + # TODO: should this actually be "for solaris/sunos"? + if mesonlib.is_sunos(): + return args + + # Rpaths to use while linking must be absolute. These are not + # written to the binary. Needed only with GNU ld: + # https://sourceware.org/bugzilla/show_bug.cgi?id=16936 + # Not needed on Windows or other platforms that don't use RPATH + # https://github.com/mesonbuild/meson/issues/1897 + # + # In addition, this linker option tends to be quite long and some + # compilers have trouble dealing with it. That's why we will include + # one option per folder, like this: + # + # -Wl,-rpath-link,/path/to/folder1 -Wl,-rpath,/path/to/folder2 ... + # + # ...instead of just one single looooong option, like this: + # + # -Wl,-rpath-link,/path/to/folder1:/path/to/folder2:... + args.extend(['-Wl,-rpath-link,' + os.path.join(build_dir, p) for p in rpath_paths]) + + return args + + +class AppleDynamicLinker(PosixDynamicLinkerMixin, DynamicLinker): + + """Apple's ld implementation.""" + + def get_asneeded_args(self) -> typing.List[str]: + return ['-Wl,-dead_strip_dylibs'] + + def get_allow_undefined_args(self) -> typing.List[str]: + return ['-Wl,-undefined,dynamic_lookup'] + + def get_std_shared_module_args(self, options: 'OptionDictType') -> typing.List[str]: + return ['-bundle', '-Wl,-undefined,dynamic_lookup'] + + def get_link_whole_for(self, args: typing.List[str]) -> typing.List[str]: + result = [] # type: typing.List[str] + for a in args: + result.extend(['-Wl,-force_load', a]) + return result + + def no_undefined_args(self) -> typing.List[str]: + return ['-Wl,-undefined,error'] + + def get_always_args(self) -> typing.List[str]: + return ['-Wl,-headerpad_max_install_names'] + + def bitcode_args(self) -> typing.List[str]: + return ['-Wl,-bitcode_bundle'] + + def fatal_warnings(self) -> typing.List[str]: + return ['-Wl,-fatal_warnings'] + + def get_soname_args(self, env: 'Environment', prefix: str, shlib_name: str, + suffix: str, soversion: str, darwin_versions: typing.Tuple[str, str], + is_shared_module: bool) -> typing.List[str]: + if is_shared_module: + return [] + install_name = ['@rpath/', prefix, shlib_name] + if soversion is not None: + install_name.append('.' + soversion) + install_name.append('.dylib') + args = ['-install_name', ''.join(install_name)] + if darwin_versions: + args.extend(['-compatibility_version', darwin_versions[0], + '-current_version', darwin_versions[1]]) + return args + + def build_rpath_args(self, env: 'Environment', build_dir: str, from_dir: str, + rpath_paths: str, build_rpath: str, + install_rpath: str) -> typing.List[str]: + if not rpath_paths and not install_rpath and not build_rpath: + return [] + # Ensure that there is enough space for install_name_tool in-place + # editing of large RPATHs + args = ['-Wl,-headerpad_max_install_names'] + # @loader_path is the equivalent of $ORIGIN on macOS + # https://stackoverflow.com/q/26280738 + origin_placeholder = '@loader_path' + processed_rpaths = prepare_rpaths(rpath_paths, build_dir, from_dir) + all_paths = mesonlib.OrderedSet([os.path.join(origin_placeholder, p) for p in processed_rpaths]) + if build_rpath != '': + all_paths.add(build_rpath) + args.extend(['-Wl,-rpath,' + rp for rp in all_paths]) + + return args + + +class GnuDynamicLinker(GnuLikeDynamicLinkerMixin, PosixDynamicLinkerMixin, DynamicLinker): + + """Representation of GNU ld.bfd and ld.gold.""" + + pass + + +class LLVMDynamicLinker(GnuLikeDynamicLinkerMixin, PosixDynamicLinkerMixin, DynamicLinker): + + """Representation of LLVM's lld (not lld-link) linker. + + This is only the posix-like linker. + """ + + pass + + +class XildLinuxDynamicLinker(GnuLikeDynamicLinkerMixin, PosixDynamicLinkerMixin, DynamicLinker): + + """Representation of Intel's Xild linker. + + This is only the linux-like linker which dispatches to Gnu ld. + """ + + pass + + +class XildAppleDynamicLinker(AppleDynamicLinker): + + """Representation of Intel's Xild linker. + + This is the apple linker, which dispatches to Apple's ld. + """ + + pass + + +class CcrxDynamicLinker(DynamicLinker): + + """Linker for Renesis CCrx compiler.""" + + def __init__(self, for_machine: mesonlib.MachineChoice, + *, version: str = 'unknown version'): + super().__init__(['rlink.exe'], for_machine, 'rlink', + version=version) + + def get_accepts_rsp(self) -> bool: + return False + + def get_lib_prefix(self) -> str: + return '-lib=' + + def get_std_shared_lib_args(self) -> typing.List[str]: + return [] + + def get_output_args(self, outputname: str) -> typing.List[str]: + return ['-output=%s' % outputname] + + def get_search_args(self, dirname: str) -> typing.NoReturn: + raise EnvironmentError('rlink.exe does not have a search dir argument') + + def get_allow_undefined_args(self) -> typing.List[str]: + return [] + + def get_soname_args(self, env: 'Environment', prefix: str, shlib_name: str, + suffix: str, soversion: str, darwin_versions: typing.Tuple[str, str], + is_shared_module: bool) -> typing.List[str]: + return [] + + +class ArmDynamicLinker(PosixDynamicLinkerMixin, DynamicLinker): + + """Linker for the ARM compiler.""" + + def __init__(self, for_machine: mesonlib.MachineChoice, + *, version: str = 'unknown version'): + super().__init__(['armlink'], for_machine, 'armlink', + version=version) + + def get_accepts_rsp(self) -> bool: + return False + + def get_std_shared_lib_args(self) -> typing.NoReturn: + raise mesonlib.MesonException('The Arm Linkers do not support shared libraries') + + def get_allow_undefined_args(self) -> typing.List[str]: + return [] + + +class ArmClangDynamicLinker(ArmDynamicLinker): + + """Linker used with ARM's clang fork. + + The interface is similar enough to the old ARM ld that it inherits and + extends a few things as needed. + """ + + def export_dynamic_args(self, env: 'Environment') -> typing.List[str]: + return ['--export_dynamic'] + + def import_library_args(self, implibname: str) -> typing.List[str]: + return ['--symdefs=' + implibname] + + +class PGIDynamicLinker(PosixDynamicLinkerMixin, DynamicLinker): + + """PGI linker.""" + + def get_allow_undefined_args(self) -> typing.List[str]: + return [] + + def get_soname_args(self, env: 'Environment', prefix: str, shlib_name: str, + suffix: str, soversion: str, darwin_versions: typing.Tuple[str, str], + is_shared_module: bool) -> typing.List[str]: + return [] + + def get_std_shared_lib_args(self) -> typing.List[str]: + # PGI -shared is Linux only. + if mesonlib.is_windows(): + return ['-Bdynamic', '-Mmakedll'] + elif mesonlib.is_linux: + return ['-shared'] + return [] + + def build_rpath_args(self, env: 'Environment', build_dir: str, from_dir: str, + rpath_paths: str, build_rpath: str, + install_rpath: str) -> typing.List[str]: + if env.machines[self.for_machine].is_windows(): + return ['-R' + os.path.join(build_dir, p) for p in rpath_paths] + return [] + + +class VisualStudioLikeLinkerMixin: + + _BUILDTYPE_ARGS = { + 'plain': [], + 'debug': [], + 'debugoptimized': [], + # The otherwise implicit REF and ICF linker optimisations are disabled by + # /DEBUG. REF implies ICF. + 'release': ['/OPT:REF'], + 'minsize': ['/INCREMENTAL:NO', '/OPT:REF'], + 'custom': [], + } # type: typing.Dict[str, typing.List[str]] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.machine = 'x86' + + def get_debug_crt_args(self) -> typing.List[str]: + """Arguments needed to select a debug crt for the linker. + + Sometimes we need to manually select the CRT (C runtime) to use with + MSVC. One example is when trying to link with static libraries since + MSVC won't auto-select a CRT for us in that case and will error out + asking us to select one. + """ + return ['/MDd'] + + def get_output_args(self, outputname: str) -> typing.List[str]: + return ['/MACHINE:' + self.machine, '/OUT:' + outputname] + + def get_always_args(self) -> typing.List[str]: + return ['/nologo'] + + def get_search_args(self, dirname: str) -> typing.List[str]: + return ['/LIBPATH:' + dirname] + + def get_std_shared_lib_args(self) -> typing.List[str]: + return ['/DLL'] + + def get_debugfile_args(self, targetfile: str) -> typing.List[str]: + pdbarr = targetfile.split('.')[:-1] + pdbarr += ['pdb'] + return ['/DEBUG', '/PDB:' + '.'.join(pdbarr)] + + def get_link_whole_for(self, args: typing.List[str]) -> typing.List[str]: + # Only since VS2015 + args = mesonlib.listify(args) + return ['/WHOLEARCHIVE:' + x for x in args] + + def get_allow_undefined_args(self) -> typing.List[str]: + # link.exe + return ['/FORCE:UNRESOLVED'] + + def get_soname_args(self, env: 'Environment', prefix: str, shlib_name: str, + suffix: str, soversion: str, darwin_versions: typing.Tuple[str, str], + is_shared_module: bool) -> typing.List[str]: + return [] + + +class MSVCDynamicLinker(VisualStudioLikeLinkerMixin, DynamicLinker): + + """Microsoft's Link.exe.""" + + def __init__(self, for_machine: mesonlib.MachineChoice, + *, version: str = 'unknown version'): + super().__init__(['link.exe'], for_machine, 'link', version=version) + + +class ClangClDynamicLinker(VisualStudioLikeLinkerMixin, DynamicLinker): + + """Clang's lld-link.exe.""" + + def __init__(self, for_machine: mesonlib.MachineChoice, + *, version: str = 'unknown version'): + super().__init__(['lld-link.exe'], for_machine, 'lld-link', + version=version) + + +class XilinkDynamicLinker(VisualStudioLikeLinkerMixin, DynamicLinker): + + """Intel's Xilink.exe.""" + + def __init__(self, for_machine: mesonlib.MachineChoice, + *, version: str = 'unknown version'): + super().__init__(['xilink.exe'], for_machine, 'xilink', version=version) + + +class SolarisDynamicLinker(PosixDynamicLinkerMixin, DynamicLinker): + + """Sys-V derived linker used on Solaris and OpenSolaris.""" + + def get_link_whole_for(self, args: typing.List[str]) -> typing.List[str]: + if not args: + return args + return ['-Wl,--whole-archive'] + args + ['-Wl,--no-whole-archive'] + + def no_undefined_args(self) -> typing.List[str]: + return ['-z', 'defs'] + + def get_allow_undefined_args(self) -> typing.List[str]: + return ['-z', 'nodefs'] + + def fatal_warnings(self) -> typing.List[str]: + return ['-z', 'fatal-warnings'] + + def build_rpath_args(self, env: 'Environment', build_dir: str, from_dir: str, + rpath_paths: str, build_rpath: str, + install_rpath: str) -> typing.List[str]: + if not rpath_paths and not install_rpath and not build_rpath: + return [] + processed_rpaths = prepare_rpaths(rpath_paths, build_dir, from_dir) + all_paths = mesonlib.OrderedSet([os.path.join('$ORIGIN', p) for p in processed_rpaths]) + if build_rpath != '': + all_paths.add(build_rpath) + + # In order to avoid relinking for RPATH removal, the binary needs to contain just + # enough space in the ELF header to hold the final installation RPATH. + paths = ':'.join(all_paths) + if len(paths) < len(install_rpath): + padding = 'X' * (len(install_rpath) - len(paths)) + if not paths: + paths = padding + else: + paths = paths + ':' + padding + return ['-Wl,-rpath,{}'.format(paths)] + + def get_soname_args(self, env: 'Environment', prefix: str, shlib_name: str, + suffix: str, soversion: str, darwin_versions: typing.Tuple[str, str], + is_shared_module: bool) -> typing.List[str]: + sostr = '' if soversion is None else '.' + soversion + return ['-Wl,-soname,{}{}.{}{}'.format(prefix, shlib_name, suffix, sostr)] + + +class OptlinkDynamicLinker(VisualStudioLikeLinkerMixin, DynamicLinker): + + """Digital Mars dynamic linker for windows.""" + + def __init__(self, for_machine: mesonlib.MachineChoice, + *, version: str = 'unknown version'): + # Use optlink instead of link so we don't interfer with other link.exe + # implementations. + super().__init__(['optlink.exe'], for_machine, 'optlink', version=version) + + def get_allow_undefined_args(self) -> typing.List[str]: + return [] |