From afe667654aec540a21b3c64aa604bae324bd457e Mon Sep 17 00:00:00 2001 From: Keith Packard Date: Fri, 7 Jul 2023 12:57:15 -0700 Subject: [PATCH 1/2] Add 'install_name' property for targets The 'install_name' property is used in place of 'name' when constructing the install target path. This allows projects to generate files using one name and then install them using another. This can be used when generating multiple targets with the same basename but different install directories within the same source directory. Signed-off-by: Keith Packard --- mesonbuild/backend/backends.py | 3 +- mesonbuild/backend/ninjabackend.py | 7 ++- mesonbuild/build.py | 83 ++++++++++++++++++++----- mesonbuild/interpreter/interpreter.py | 5 ++ mesonbuild/interpreter/type_checking.py | 7 +++ mesonbuild/minstall.py | 14 +++-- 6 files changed, 97 insertions(+), 22 deletions(-) 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..020f967d7412 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -92,6 +92,7 @@ 'install_rpath', 'install_dir', 'install_mode', + 'install_name', 'install_tag', 'name_prefix', 'name_suffix', @@ -519,6 +520,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: @@ -564,6 +566,12 @@ 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: + return self.install_name + def get_default_install_dir(self) -> T.Tuple[str, str]: raise NotImplementedError @@ -1136,6 +1144,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 +1273,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 +2001,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 +2125,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 +2209,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 +2257,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 +2288,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 +2300,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 +2487,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 +2497,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 +2625,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 +2648,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 +2968,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 +3007,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: From dbd24c38717a8a2fbdda9102996be94089c78769 Mon Sep 17 00:00:00 2001 From: Keith Packard Date: Fri, 7 Jul 2023 13:01:01 -0700 Subject: [PATCH 2/2] build: Provide backward compatability for targets with path separators To work around the restriction that only one target with a given name could be created in any project directory, projects could prefix that name with a unique directory name and things would "just work". Take this example: static_library('foo/libbar', [a.c, b.c], install : true, install_dir : 'foo' ) static_library('bletch/libbar', [d.c, e.c], install : true, install_dir : 'bletch' ) This would generate two libraries. The 'lib' prefix is prepended to the directory names by the StaticLibrary code, while the target names include the 'lib' prefix explicitly on 'libbar': libfoo/libbar.a libbletch/libbar.a. Installation would copy the files. The copy code uses only the basename from the source file when constructing the destination, stripping off libfoo/ and libbletch/: libfoo/libbar.a -> foo/libbar.a libbletch/libbar.a -> bletch/libbar.a We need to preserve this behavior while switching the internal implementation to using the new 'install-name' support. 1. When a target name includes a path separator, extract the basename and assign that to the 'install_name_auto' property. Then replace the path separators with '_' to 'flatten' the name, avoiding incidental creation of directories inside the build tree. 2. When install_name is not set, use this install_name_auto instead, first removing any matching prefix (e.g. 'lib' for libraries) as those will be re-added when the install filename is created. Projects which depend on this behavior can be converted to use 'install-name' directly, once they depend on a version of meson with that feature. Signed-off-by: Keith Packard --- mesonbuild/build.py | 18 +++++++++++++++--- mesonbuild/utils/universal.py | 6 ++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/mesonbuild/build.py b/mesonbuild/build.py index 020f967d7412..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, @@ -538,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: @@ -570,7 +573,16 @@ def get_install_fname(self) -> str: return self.get_install_name() def get_install_name(self) -> str: - return self.install_name + 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 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