diff --git a/mesonbuild/backend/backends.py b/mesonbuild/backend/backends.py index 3b8762343119..46e931b4fbff 100644 --- a/mesonbuild/backend/backends.py +++ b/mesonbuild/backend/backends.py @@ -150,6 +150,7 @@ class TargetInstallData: optional: bool = False tag: T.Optional[str] = None can_strip: bool = False + install_fname: T.Optional[str] = None def __post_init__(self, outdir_name: T.Optional[str]) -> None: if outdir_name is None: @@ -1714,7 +1715,7 @@ def generate_target_install(self, d: InstallData) -> None: first_outdir_name, should_strip, mappings, t.rpath_dirs_to_remove, t.install_rpath, install_mode, t.subproject, - tag=tag, can_strip=can_strip) + tag=tag, can_strip=can_strip, install_fname=t.get_install_fname()) d.targets.append(i) for alias, to, tag in t.get_aliases(): diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py index 108aa729558e..11ed17d38140 100644 --- a/mesonbuild/backend/ninjabackend.py +++ b/mesonbuild/backend/ninjabackend.py @@ -3251,10 +3251,13 @@ def get_target_type_link_args(self, target, linker): commands += linker.get_std_shared_lib_link_args() # All shared libraries are PIC commands += linker.get_pic_args() - if not isinstance(target, build.SharedModule) or target.force_soname: + soname = target.get_install_name() + if not isinstance(target, build.SharedModule) or target.force_soname or soname: + if not soname: + soname = target.name # Add -Wl,-soname arguments on Linux, -install_name on OS X commands += linker.get_soname_args( - self.environment, target.prefix, target.name, target.suffix, + self.environment, target.prefix, soname, target.suffix, target.soversion, target.darwin_versions) # This is only visited when building for Windows using either GCC or Visual Studio if target.vs_module_defs and hasattr(linker, 'gen_vs_module_defs_args'): diff --git a/mesonbuild/build.py b/mesonbuild/build.py index d9e04804bd7b..eb7fba8b9ed8 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -37,6 +37,7 @@ get_filenames_templates_dict, substitute_values, has_path_sep, OptionKey, PerMachineDefaultable, OptionOverrideProxy, MesonBugException, EnvironmentVariables, pickle_load, + flatten_path, ) from .compilers import ( is_object, clink_langs, sort_clink, all_languages, @@ -92,6 +93,7 @@ 'install_rpath', 'install_dir', 'install_mode', + 'install_name', 'install_tag', 'name_prefix', 'name_suffix', @@ -519,6 +521,7 @@ class Target(HoldableObject, metaclass=abc.ABCMeta): build_always_stale: bool = False extra_files: T.List[File] = field(default_factory=list) override_options: InitVar[T.Optional[T.Dict[OptionKey, str]]] = None + install_name: T.Optional[str] = None @abc.abstractproperty def typename(self) -> str: @@ -536,12 +539,14 @@ def __post_init__(self, overrides: T.Optional[T.Dict[OptionKey, str]]) -> None: ovr = {} self.options = OptionOverrideProxy(ovr, self.environment.coredata.options, self.subproject) # XXX: this should happen in the interpreter + self.install_name_auto = None if has_path_sep(self.name): - # Fix failing test 53 when this becomes an error. mlog.warning(textwrap.dedent(f'''\ Target "{self.name}" has a path separator in its name. This is not supported, it can cause unexpected failures and will become - a hard error in the future.''')) + a hard error in the future. Consider using the install_name property instead''')) + self.install_name_auto = os.path.basename(self.name) + self.name = flatten_path(self.name) # dataclass comparators? def __lt__(self, other: object) -> bool: @@ -564,6 +569,21 @@ def __ge__(self, other: object) -> bool: return NotImplemented return self.get_id() >= other.get_id() + def get_install_fname(self) -> str: + return self.get_install_name() + + def get_install_name(self) -> str: + if self.install_name: + return self.install_name + if not self.install_name_auto: + return None + prefix = None + if getattr(self, 'name_prefix_set', False) or hasattr(self, 'prefix'): + prefix = self.prefix + if prefix and self.install_name_auto.startswith(prefix): + return self.install_name_auto[len(prefix):] + return self.install_name_auto + def get_default_install_dir(self) -> T.Tuple[str, str]: raise NotImplementedError @@ -1136,6 +1156,7 @@ def process_kwargs(self, kwargs): self.install_dir = typeslistify(kwargs.get('install_dir', []), (str, bool)) self.install_mode = kwargs.get('install_mode', None) + self.install_name = kwargs.get('install_name', None) self.install_tag = stringlistify(kwargs.get('install_tag', [None])) main_class = kwargs.get('main_class', '') if not isinstance(main_class, str): @@ -1264,6 +1285,18 @@ def _extract_pic_pie(self, kwargs, arg: str, option: str): def get_filename(self) -> str: return self.filename + def get_install_fname(self) -> str: + install_name = self.get_install_name() + if not install_name: + return None + install_fname = '' + if self.name_prefix_set: + install_fname = self.prefix + install_fname += self.install_name + if self.name_suffix_set: + install_fname += '.' + self.suffix + return install_fname + def get_outputs(self) -> T.List[str]: return self.outputs @@ -1980,6 +2013,15 @@ def post_init(self) -> None: def get_default_install_dir(self) -> T.Tuple[str, str]: return self.environment.get_bindir(), '{bindir}' + def get_install_fname(self) -> str: + install_name = self.get_install_name() + if not install_name: + return None + install_fname = install_name + if self.suffix: + install_fname += '.' + self.suffix + return install_fname + def description(self): '''Human friendly description of the executable''' return self.name @@ -2095,6 +2137,12 @@ def post_init(self) -> None: self.filename = self.prefix + self.name + '.' + self.suffix self.outputs = [self.filename] + def get_install_fname(self) -> str: + install_name = self.get_install_name() + if not install_name: + return None + return self.prefix + install_name + '.' + self.suffix + def get_link_deps_mapping(self, prefix: str) -> T.Mapping[str, str]: return {} @@ -2173,7 +2221,7 @@ def post_init(self) -> None: self.prefix = None if not hasattr(self, 'suffix'): self.suffix = None - self.basic_filename_tpl = '{0.prefix}{0.name}.{0.suffix}' + self.basic_filename_tpl = '{0.prefix}{1}.{0.suffix}' self.determine_filenames() def get_link_deps_mapping(self, prefix: str) -> T.Mapping[str, str]: @@ -2221,7 +2269,7 @@ def determine_filenames(self): if 'cs' in self.compilers: prefix = '' suffix = 'dll' - self.filename_tpl = '{0.prefix}{0.name}.{0.suffix}' + self.filename_tpl = '{0.prefix}{1}.{0.suffix}' create_debug_file = True # C, C++, Swift, Vala # Only Windows uses a separate import library for linking @@ -2252,9 +2300,9 @@ def determine_filenames(self): self.import_filename = self.gcc_import_filename # Shared library has the soversion if it is defined if self.soversion: - self.filename_tpl = '{0.prefix}{0.name}-{0.soversion}.{0.suffix}' + self.filename_tpl = '{0.prefix}{1}-{0.soversion}.{0.suffix}' else: - self.filename_tpl = '{0.prefix}{0.name}.{0.suffix}' + self.filename_tpl = '{0.prefix}{1}.{0.suffix}' elif self.environment.machines[self.for_machine].is_cygwin(): suffix = 'dll' self.gcc_import_filename = '{}{}.dll.a'.format(self.prefix if self.prefix is not None else 'lib', self.name) @@ -2264,47 +2312,53 @@ def determine_filenames(self): # Import library is called libfoo.dll.a self.import_filename = self.gcc_import_filename if self.soversion: - self.filename_tpl = '{0.prefix}{0.name}-{0.soversion}.{0.suffix}' + self.filename_tpl = '{0.prefix}{1}-{0.soversion}.{0.suffix}' else: - self.filename_tpl = '{0.prefix}{0.name}.{0.suffix}' + self.filename_tpl = '{0.prefix}{1}.{0.suffix}' elif self.environment.machines[self.for_machine].is_darwin(): prefix = 'lib' suffix = 'dylib' # On macOS, the filename can only contain the major version if self.soversion: # libfoo.X.dylib - self.filename_tpl = '{0.prefix}{0.name}.{0.soversion}.{0.suffix}' + self.filename_tpl = '{0.prefix}{1}.{0.soversion}.{0.suffix}' else: # libfoo.dylib - self.filename_tpl = '{0.prefix}{0.name}.{0.suffix}' + self.filename_tpl = '{0.prefix}{1}.{0.suffix}' elif self.environment.machines[self.for_machine].is_android(): prefix = 'lib' suffix = 'so' # Android doesn't support shared_library versioning - self.filename_tpl = '{0.prefix}{0.name}.{0.suffix}' + self.filename_tpl = '{0.prefix}{1}.{0.suffix}' else: prefix = 'lib' suffix = 'so' if self.ltversion: # libfoo.so.X[.Y[.Z]] (.Y and .Z are optional) - self.filename_tpl = '{0.prefix}{0.name}.{0.suffix}.{0.ltversion}' + self.filename_tpl = '{0.prefix}{1}.{0.suffix}.{0.ltversion}' elif self.soversion: # libfoo.so.X - self.filename_tpl = '{0.prefix}{0.name}.{0.suffix}.{0.soversion}' + self.filename_tpl = '{0.prefix}{1}.{0.suffix}.{0.soversion}' else: # No versioning, libfoo.so - self.filename_tpl = '{0.prefix}{0.name}.{0.suffix}' + self.filename_tpl = '{0.prefix}{1}.{0.suffix}' if self.prefix is None: self.prefix = prefix if self.suffix is None: self.suffix = suffix - self.filename = self.filename_tpl.format(self) + self.filename = self.filename_tpl.format(self, self.name) # There may have been more outputs added by the time we get here, so # only replace the first entry self.outputs[0] = self.filename if create_debug_file: self.debug_filename = os.path.splitext(self.filename)[0] + '.pdb' + def get_install_fname(self) -> str: + install_name = self.get_install_name() + if not install_name: + return None + return self.filename_tpl.format(self, install_name) + @staticmethod def _validate_darwin_versions(darwin_versions): try: @@ -2445,7 +2499,7 @@ def get_aliases(self) -> T.List[T.Tuple[str, str, str]]: # Where libfoo.so.0.100.0 is the actual library if self.suffix == 'so' and self.ltversion and self.ltversion != self.soversion: alias_tpl = self.filename_tpl.replace('ltversion', 'soversion') - ltversion_filename = alias_tpl.format(self) + ltversion_filename = alias_tpl.format(self, self.name) tag = self.install_tag[0] or 'runtime' aliases.append((ltversion_filename, self.filename, tag)) # libfoo.so.0/libfoo.0.dylib is the actual library @@ -2455,7 +2509,7 @@ def get_aliases(self) -> T.List[T.Tuple[str, str, str]]: # libfoo.so -> libfoo.so.0 # libfoo.dylib -> libfoo.0.dylib tag = self.install_tag[0] or 'devel' - aliases.append((self.basic_filename_tpl.format(self), ltversion_filename, tag)) + aliases.append((self.basic_filename_tpl.format(self, self.name), ltversion_filename, tag)) return aliases def type_suffix(self): @@ -2583,6 +2637,7 @@ def __init__(self, install_tag: T.Optional[T.List[T.Optional[str]]] = None, absolute_paths: bool = False, backend: T.Optional['Backend'] = None, + install_name: T.Optional[str] = None, ): # TODO expose keyword arg to make MachineChoice.HOST configurable super().__init__(name, subdir, subproject, False, MachineChoice.HOST, environment, @@ -2605,6 +2660,7 @@ def __init__(self, self.feed = feed self.install_dir = list(install_dir or []) self.install_mode = install_mode + self.install_name = install_name self.install_tag = _process_install_tag(install_tag, len(self.outputs)) self.name = name if name else self.outputs[0] @@ -2924,6 +2980,12 @@ def get_classpath_args(self): def get_default_install_dir(self) -> T.Tuple[str, str]: return self.environment.get_jar_dir(), '{jardir}' + def get_install_fname(self) -> str: + install_name = self.get_install_name() + if not install_name: + return None + return install_name + '.jar' + @dataclass(eq=False) class CustomTargetIndex(HoldableObject): @@ -2957,6 +3019,9 @@ def get_subdir(self) -> str: def get_filename(self) -> str: return self.output + def get_install_fname(self) -> str: + return self.target.get_install_fname() + def get_id(self) -> str: return self.target.get_id() diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index 363de547f8a3..d27963279336 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -73,6 +73,7 @@ INSTALL_KW, INSTALL_DIR_KW, INSTALL_MODE_KW, + INSTALL_NAME_KW, LINK_WITH_KW, LINK_WHOLE_KW, CT_INSTALL_TAG_KW, @@ -1997,6 +1998,7 @@ def _validate_custom_target_outputs(has_multi_in: bool, outputs: T.Iterable[str] ENV_KW.evolve(since='0.57.0'), INSTALL_KW, INSTALL_MODE_KW.evolve(since='0.47.0'), + INSTALL_NAME_KW, KwargInfo('feed', bool, default=False, since='0.59.0'), KwargInfo('capture', bool, default=False), KwargInfo('console', bool, default=False, since='0.48.0'), @@ -2089,6 +2091,7 @@ def func_custom_target(self, node: mparser.FunctionNode, args: T.Tuple[str], install=kwargs['install'], install_dir=kwargs['install_dir'], install_mode=install_mode, + install_name=kwargs['install_name'], install_tag=kwargs['install_tag'], backend=self.backend) self.add_target(tg.name, tg) @@ -2454,6 +2457,7 @@ def _warn_kwarg_install_mode_sticky(self, mode: FileMode) -> None: INSTALL_MODE_KW.evolve(since='0.38.0'), INSTALL_TAG_KW.evolve(since='0.60.0'), INSTALL_DIR_KW, + INSTALL_NAME_KW, PRESERVE_PATH_KW.evolve(since='0.64.0'), ) def func_install_data(self, node: mparser.BaseNode, @@ -2548,6 +2552,7 @@ def func_install_subdir(self, node: mparser.BaseNode, args: T.Tuple[str], 'configure_file', DEPFILE_KW.evolve(since='0.52.0'), INSTALL_MODE_KW.evolve(since='0.47.0,'), + INSTALL_NAME_KW, INSTALL_TAG_KW.evolve(since='0.60.0'), KwargInfo('capture', bool, default=False, since='0.41.0'), KwargInfo( diff --git a/mesonbuild/interpreter/type_checking.py b/mesonbuild/interpreter/type_checking.py index 8b57d06f15db..03b5822f32a4 100644 --- a/mesonbuild/interpreter/type_checking.py +++ b/mesonbuild/interpreter/type_checking.py @@ -174,6 +174,13 @@ def variables_convertor(contents: T.Union[str, T.List[str], T.Dict[str, str]]) - convertor=_install_mode_convertor, ) +INSTALL_NAME_KW: KwargInfo[T.Optional[str]] = KwargInfo( + 'install_name', + (str, NoneType), + default=None, + since='1.3.0', +) + REQUIRED_KW: KwargInfo[T.Union[bool, UserFeatureOption]] = KwargInfo( 'required', (bool, UserFeatureOption), diff --git a/mesonbuild/minstall.py b/mesonbuild/minstall.py index 49006917e914..1da62d347a85 100644 --- a/mesonbuild/minstall.py +++ b/mesonbuild/minstall.py @@ -409,7 +409,10 @@ def do_copyfile(self, from_file: str, to_file: str, dirmaker, outdir = makedirs # Create dirs if needed dirmaker.makedirs(outdir, exist_ok=True) - self.log(f'Installing {from_file} to {outdir}') + if os.path.basename(from_file) != os.path.basename(to_file): + self.log(f'Installing {from_file} to {to_file}') + else: + self.log(f'Installing {from_file} to {outdir}') if os.path.islink(from_file): if not os.path.exists(from_file): # Dangling symlink. Replicate as is. @@ -728,8 +731,11 @@ def install_targets(self, d: InstallData, dm: DirMaker, destdir: str, fullprefix file_copied = False # not set when a directory is copied fname = check_for_stampfile(t.fname) outdir = get_destdir_path(destdir, fullprefix, t.outdir) - outname = os.path.join(outdir, os.path.basename(fname)) - final_path = os.path.join(d.prefix, t.outdir, os.path.basename(fname)) + install_fname = t.install_fname + if install_fname is None: + install_fname = os.path.basename(fname) + outname = os.path.join(outdir, install_fname) + final_path = os.path.join(d.prefix, t.outdir, install_fname) should_strip = t.strip or (t.can_strip and self.options.strip) install_rpath = t.install_rpath install_name_mappings = t.install_name_mappings @@ -752,7 +758,7 @@ def install_targets(self, d: InstallData, dm: DirMaker, destdir: str, fullprefix file_copied = self.do_copyfile(wasm_source, wasm_output) elif os.path.isdir(fname): fname = os.path.join(d.build_dir, fname.rstrip('/')) - outname = os.path.join(outdir, os.path.basename(fname)) + outname = os.path.join(outdir, install_fname) dm.makedirs(outdir, exist_ok=True) self.do_copydir(d, fname, outname, None, install_mode, dm) else: diff --git a/mesonbuild/utils/universal.py b/mesonbuild/utils/universal.py index db59a601ee7c..eec1b5357c69 100644 --- a/mesonbuild/utils/universal.py +++ b/mesonbuild/utils/universal.py @@ -112,6 +112,7 @@ class _VerPickleLoadable(Protocol): 'expand_arguments', 'extract_as_list', 'first', + 'flatten_path', 'generate_list', 'get_compiler_for_source', 'get_filenames_templates_dict', @@ -1091,6 +1092,11 @@ def has_path_sep(name: str, sep: str = '/\\') -> bool: return False +def flatten_path(name: str, sep: str = '/\\', replace: str = '_') -> str: + '''Replaces any specified @sep path separators in @name with @replace''' + return name.translate(name.maketrans(sep, replace * len(sep))) + + if is_windows(): # shlex.split is not suitable for splitting command line on Window (https://bugs.python.org/issue1724822); # shlex.quote is similarly problematic. Below are "proper" implementations of these functions according to