From ecff598bceb211e46b6e985459a35bab54f4d862 Mon Sep 17 00:00:00 2001 From: Keith Packard Date: Mon, 25 Nov 2024 16:35:32 -0800 Subject: [PATCH] build: Support 'rename' in build_target and custom_target This allows built results to be renamed during installation, just as with install_data. This feature is motivated by the need to install multiple files from a single source directory into different install directories but with the same basename. In my case, I build dozens of 'libc.a' files which need to be installed into the toolchain's multilib heirarchy correctly. Signed-off-by: Keith Packard --- docs/yaml/functions/_build_target_base.yaml | 9 ++++++++ docs/yaml/functions/custom_target.yaml | 9 ++++++++ mesonbuild/backend/backends.py | 18 +++++++++------ mesonbuild/build.py | 14 ++++++++++++ mesonbuild/interpreter/interpreter.py | 13 ++++++++++- mesonbuild/interpreter/type_checking.py | 7 ++++++ mesonbuild/minstall.py | 22 +++++++++++-------- mesonbuild/mintro.py | 4 ++-- .../109 custom target capture/meson.build | 12 ++++++++++ .../109 custom target capture/test.json | 3 ++- .../common/117 shared module/meson.build | 7 ++++++ test cases/common/117 shared module/test.json | 1 + 12 files changed, 99 insertions(+), 20 deletions(-) diff --git a/docs/yaml/functions/_build_target_base.yaml b/docs/yaml/functions/_build_target_base.yaml index 1721b29cfe5a..0b4b41958e38 100644 --- a/docs/yaml/functions/_build_target_base.yaml +++ b/docs/yaml/functions/_build_target_base.yaml @@ -328,3 +328,12 @@ kwargs: This allows renaming similar to the dependency renaming feature of cargo or `extern crate foo as bar` inside rust code. + + rename: + type: list[str] + since: 1.6.99 + description: | + If specified renames each output into corresponding file from + `rename` list during install. Nested paths are allowed and they + are joined with `install_dir`. Length of `rename` list must be + equal to the number of outputs. diff --git a/docs/yaml/functions/custom_target.yaml b/docs/yaml/functions/custom_target.yaml index 585d2602aff7..4fdcc24c077d 100644 --- a/docs/yaml/functions/custom_target.yaml +++ b/docs/yaml/functions/custom_target.yaml @@ -235,3 +235,12 @@ kwargs: their input from a file and instead read it from standard input. When this argument is set to `true`, Meson feeds the input file to `stdin`. Note that your argument list may not contain `@INPUT@` when feed mode is active. + + rename: + type: list[str] + since: 1.6.99 + description: | + If specified renames each output into corresponding file from + `rename` list during install. Nested paths are allowed and they + are joined with `install_dir`. Length of `rename` list must be + equal to the number of outputs. diff --git a/mesonbuild/backend/backends.py b/mesonbuild/backend/backends.py index a4be50f664b1..cf8c078ebff9 100644 --- a/mesonbuild/backend/backends.py +++ b/mesonbuild/backend/backends.py @@ -142,11 +142,14 @@ class TargetInstallData: optional: bool = False tag: T.Optional[str] = None can_strip: bool = False + out_fname: T.Optional[str] = None def __post_init__(self, outdir_name: T.Optional[str]) -> None: if outdir_name is None: outdir_name = os.path.join('{prefix}', self.outdir) - self.out_name = os.path.join(outdir_name, os.path.basename(self.fname)) + if self.out_fname is None: + self.out_fname = os.path.basename(self.fname) + self.out_name = os.path.join(outdir_name, self.out_fname) @dataclass(eq=False) class InstallEmptyDir: @@ -1709,6 +1712,7 @@ def generate_target_install(self, d: InstallData) -> None: if not t.should_install(): continue outdirs, install_dir_names, custom_install_dir = t.get_install_dir() + rename = t.get_rename() # Sanity-check the outputs and install_dirs num_outdirs, num_out = len(outdirs), len(t.get_outputs()) if num_outdirs not in {1, num_out}: @@ -1749,7 +1753,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, out_fname=rename[0]) d.targets.append(i) for alias, to, tag in t.get_aliases(): @@ -1787,14 +1791,14 @@ def generate_target_install(self, d: InstallData) -> None: d.targets.append(i) # Install secondary outputs. Only used for Vala right now. if num_outdirs > 1: - for output, outdir, outdir_name, tag in zip(t.get_outputs()[1:], outdirs[1:], install_dir_names[1:], t.install_tag[1:]): + for output, outdir, outdir_name, tag, out_fname in zip(t.get_outputs()[1:], outdirs[1:], install_dir_names[1:], t.install_tag[1:], rename[1:]): # User requested that we not install this output if outdir is False: continue f = os.path.join(self.get_target_dir(t), output) i = TargetInstallData(f, outdir, outdir_name, False, {}, set(), None, install_mode, t.subproject, - tag=tag) + tag=tag, out_fname=out_fname) d.targets.append(i) elif isinstance(t, build.CustomTarget): # If only one install_dir is specified, assume that all @@ -1815,10 +1819,10 @@ def generate_target_install(self, d: InstallData) -> None: i = TargetInstallData(f, first_outdir, first_outdir_name, False, {}, set(), None, install_mode, t.subproject, optional=not t.build_by_default, - tag=tag) + tag=tag, out_fname=rename[0]) d.targets.append(i) else: - for output, outdir, outdir_name, tag in zip(t.get_outputs(), outdirs, install_dir_names, t.install_tag): + for output, outdir, outdir_name, tag, out_fname in zip(t.get_outputs(), outdirs, install_dir_names, t.install_tag, rename): # User requested that we not install this output if outdir is False: continue @@ -1827,7 +1831,7 @@ def generate_target_install(self, d: InstallData) -> None: i = TargetInstallData(f, outdir, outdir_name, False, {}, set(), None, install_mode, t.subproject, optional=not t.build_by_default, - tag=tag) + tag=tag, out_fname=out_fname) d.targets.append(i) def generate_custom_install_script(self, d: InstallData) -> None: diff --git a/mesonbuild/build.py b/mesonbuild/build.py index a00209ad45a8..046ecfe1d38b 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -97,6 +97,7 @@ class DFeatures(TypedDict): 'native', 'objects', 'override_options', + 'rename', 'sources', 'gnu_symbol_visibility', 'link_language', @@ -1127,6 +1128,11 @@ def get_custom_install_dir(self) -> T.List[T.Union[str, Literal[False]]]: def get_custom_install_mode(self) -> T.Optional['FileMode']: return self.install_mode + def get_rename(self) -> T.List[str]: + if self.rename: + return self.rename + return [None] * len(self.outputs) + def process_kwargs(self, kwargs): self.process_kwargs_base(kwargs) self.original_kwargs = kwargs @@ -1164,6 +1170,7 @@ def process_kwargs(self, kwargs): (str, bool)) self.install_mode = kwargs.get('install_mode', None) self.install_tag = stringlistify(kwargs.get('install_tag', [None])) + self.rename = kwargs.get('rename', None) if not isinstance(self, Executable): # build_target will always populate these as `None`, which is fine if kwargs.get('gui_app') is not None: @@ -2621,6 +2628,7 @@ def __init__(self, feed: bool = False, install: bool = False, install_dir: T.Optional[T.List[T.Union[str, Literal[False]]]] = None, + rename: T.Optional[T.List[str]] = None, install_mode: T.Optional[FileMode] = None, install_tag: T.Optional[T.List[T.Optional[str]]] = None, absolute_paths: bool = False, @@ -2647,6 +2655,7 @@ def __init__(self, self.extra_depends = list(extra_depends or []) self.feed = feed self.install_dir = list(install_dir or []) + self.rename = rename self.install_mode = install_mode self.install_tag = _process_install_tag(install_tag, len(self.outputs)) self.name = name if name else self.outputs[0] @@ -2766,6 +2775,11 @@ def get_link_dep_subdirs(self) -> T.AbstractSet[str]: def get_all_link_deps(self): return [] + def get_rename(self) -> T.List[str]: + if self.rename: + return self.rename + return [None] * len(self.outputs) + def is_internal(self) -> bool: ''' Returns True if this is a not installed static library. diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index 58385c58c5f7..c02e903ac92a 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -2056,6 +2056,7 @@ def _validate_custom_target_outputs(self, has_multi_in: bool, outputs: T.Iterabl INSTALL_MODE_KW.evolve(since='0.47.0'), KwargInfo('feed', bool, default=False, since='0.59.0'), KwargInfo('capture', bool, default=False), + KwargInfo('rename', ContainerTypeInfo(list, str), default=[], listify=True, since='1.6.0'), KwargInfo('console', bool, default=False, since='0.48.0'), ) def func_custom_target(self, node: mparser.FunctionNode, args: T.Tuple[str], @@ -2121,7 +2122,11 @@ def func_custom_target(self, node: mparser.FunctionNode, args: T.Tuple[str], 'or the same number of elements as the output keyword argument. ' f'(there are {len(kwargs["install_tag"])} install_tags, ' f'and {len(kwargs["output"])} outputs)') - + if len(kwargs['rename']) not in {0, 1, len(kwargs['output'])}: + raise InvalidArguments('custom_target: rename argument must have 0 or 1 outputs, ' + 'or the same number of elements as the output keyword argument. ' + f'(there are {len(kwargs["rename"])} rename, ' + f'and {len(kwargs["output"])} outputs)') for t in kwargs['output']: self.validate_forbidden_targets(t) self._validate_custom_target_outputs(len(inputs) > 1, kwargs['output'], "custom_target") @@ -2147,6 +2152,7 @@ def func_custom_target(self, node: mparser.FunctionNode, args: T.Tuple[str], install_dir=kwargs['install_dir'], install_mode=install_mode, install_tag=kwargs['install_tag'], + rename=kwargs['rename'], backend=self.backend) self.add_target(tg.name, tg) return tg @@ -3482,6 +3488,11 @@ def build_target(self, node: mparser.BaseNode, args: T.Tuple[str, SourcesVarargs target = targetclass(name, self.subdir, self.subproject, for_machine, srcs, struct, objs, self.environment, self.compilers[for_machine], kwargs) + if len(kwargs['rename']) not in {0, 1, len(target.outputs)}: + raise InvalidArguments('build_target: rename argument must have 0 or 1 outputs, ' + 'or same as the number of outputs. ' + f'(there are {len(kwargs["rename"])} rename, ' + f'and {len(target.outputs)} outputs)') self.add_target(name, target) self.project_args_frozen = True return target diff --git a/mesonbuild/interpreter/type_checking.py b/mesonbuild/interpreter/type_checking.py index ed34be950065..eab8de61628f 100644 --- a/mesonbuild/interpreter/type_checking.py +++ b/mesonbuild/interpreter/type_checking.py @@ -584,6 +584,13 @@ def _objects_validator(vals: T.List[ObjectTypes]) -> T.Optional[str]: ('1.1.0', 'generated sources as positional "objects" arguments') }, ), + KwargInfo( + 'rename', + ContainerTypeInfo(list, str), + listify=True, + default=[], + since='1.6.99', + ), ] diff --git a/mesonbuild/minstall.py b/mesonbuild/minstall.py index 860826bf1b84..430c2811c111 100644 --- a/mesonbuild/minstall.py +++ b/mesonbuild/minstall.py @@ -393,6 +393,10 @@ def do_copyfile(self, from_file: str, to_file: str, makedirs: T.Optional[T.Tuple[T.Any, str]] = None, follow_symlinks: T.Optional[bool] = None) -> bool: outdir = os.path.split(to_file)[0] + if os.path.basename(from_file) == os.path.basename(to_file): + outpath = outdir + else: + outpath = to_file if not os.path.isfile(from_file) and not os.path.islink(from_file): raise MesonException(f'Tried to install something that isn\'t a file: {from_file!r}') # copyfile fails if the target file already exists, so remove it to @@ -405,10 +409,10 @@ def do_copyfile(self, from_file: str, to_file: str, append_to_log(self.lf, f'# Preserving old file {to_file}\n') self.preserved_file_count += 1 return False - self.log(f'Installing {from_file} to {outdir}') + self.log(f'Installing {from_file} to {outpath}') self.remove(to_file) else: - self.log(f'Installing {from_file} to {outdir}') + self.log(f'Installing {from_file} to {outpath}') if makedirs: # Unpack tuple dirmaker, outdir = makedirs @@ -734,9 +738,10 @@ def install_targets(self, d: InstallData, dm: DirMaker, destdir: str, fullprefix raise MesonException(f'File {t.fname!r} could not be found') file_copied = False # not set when a directory is copied fname = check_for_stampfile(t.fname) + out_fname = t.out_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)) + outname = os.path.join(outdir, out_fname) + final_path = os.path.join(d.prefix, t.outdir, out_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 @@ -744,10 +749,10 @@ def install_targets(self, d: InstallData, dm: DirMaker, destdir: str, fullprefix if not os.path.exists(fname): raise MesonException(f'File {fname!r} could not be found') elif os.path.isfile(fname): - file_copied = self.do_copyfile(fname, outname, makedirs=(dm, outdir)) + file_copied = self.do_copyfile(fname, outname, makedirs=(dm, os.path.dirname(outname))) if should_strip and d.strip_bin is not None: - if fname.endswith('.jar'): - self.log('Not stripping jar target: {}'.format(os.path.basename(fname))) + if out_fname.endswith('.jar'): + self.log('Not stripping jar target: {}'.format(os.path.basename(out_fname))) continue self.do_strip(d.strip_bin, fname, outname) if fname.endswith('.js'): @@ -759,8 +764,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)) - dm.makedirs(outdir, exist_ok=True) + dm.makedirs(os.path.dirname(outname), exist_ok=True) self.do_copydir(d, fname, outname, None, install_mode, dm) else: raise RuntimeError(f'Unknown file type for {fname!r}') diff --git a/mesonbuild/mintro.py b/mesonbuild/mintro.py index 810a2b674b40..d04391688a44 100644 --- a/mesonbuild/mintro.py +++ b/mesonbuild/mintro.py @@ -110,7 +110,7 @@ def list_installed(installdata: backends.InstallData) -> T.Dict[str, str]: if installdata is not None: for t in installdata.targets: res[os.path.join(installdata.build_dir, t.fname)] = \ - os.path.join(installdata.prefix, t.outdir, os.path.basename(t.fname)) + os.path.join(installdata.prefix, t.out_name) for i in installdata.data: res[i.path] = os.path.join(installdata.prefix, i.install_path) for i in installdata.headers: @@ -233,7 +233,7 @@ def list_targets(builddata: build.Build, installdata: backends.InstallData, back install_lookuptable = {} for i in installdata.targets: basename = os.path.basename(i.fname) - install_lookuptable[basename] = [str(PurePath(installdata.prefix, i.outdir, basename))] + install_lookuptable[basename] = [str(PurePath(installdata.prefix, i.out_name))] for s in installdata.symlinks: # Symlink's target must already be in the table. They share the same list # to support symlinks to symlinks recursively, such as .so -> .so.0 -> .so.1.2.3 diff --git a/test cases/common/109 custom target capture/meson.build b/test cases/common/109 custom target capture/meson.build index b7622014a99d..c2f38195e696 100644 --- a/test cases/common/109 custom target capture/meson.build +++ b/test cases/common/109 custom target capture/meson.build @@ -22,3 +22,15 @@ if not os.path.exists(sys.argv[1]): ''' test('capture-wrote', python3, args : ['-c', ct_output_exists, mytarget]) + +mytarget2 = custom_target('bindat2', + output : 'data2.dat', + input : 'data_source.txt', + rename : 'subdir2/data.dat', + capture : true, + command : [python3, comp, '@INPUT@'], + install : true, + install_dir : 'subdir' +) + +test('capture-wrote2', python3, args : ['-c', ct_output_exists, mytarget2]) diff --git a/test cases/common/109 custom target capture/test.json b/test cases/common/109 custom target capture/test.json index ba66b024aaa9..c9d91321808f 100644 --- a/test cases/common/109 custom target capture/test.json +++ b/test cases/common/109 custom target capture/test.json @@ -1,5 +1,6 @@ { "installed": [ - {"type": "file", "file": "usr/subdir/data.dat"} + {"type": "file", "file": "usr/subdir/data.dat"}, + {"type": "file", "file": "usr/subdir/subdir2/data.dat"} ] } diff --git a/test cases/common/117 shared module/meson.build b/test cases/common/117 shared module/meson.build index 94d17a716da9..fd4c6650e899 100644 --- a/test cases/common/117 shared module/meson.build +++ b/test cases/common/117 shared module/meson.build @@ -34,6 +34,13 @@ test('import test', e, args : m) m2 = build_target('mymodule2', 'module.c', target_type: 'shared_module') test('import test 2', e, args : m2) +# Same as above, but installed into a sub directory +m3 = shared_module('mymodule3', 'module.c', + install : true, + install_dir : join_paths(get_option('libdir'), 'modules'), + rename: 'subdir/libmymodule.so') +test('import test 3', e, args : m3) + # Shared module that does not export any symbols shared_module('nosyms', 'nosyms.c', override_options: ['werror=false'], diff --git a/test cases/common/117 shared module/test.json b/test cases/common/117 shared module/test.json index 33bfeff07600..41323e047142 100644 --- a/test cases/common/117 shared module/test.json +++ b/test cases/common/117 shared module/test.json @@ -1,6 +1,7 @@ { "installed": [ {"type": "expr", "file": "usr/lib/modules/libnosyms?so"}, + {"type": "expr", "file": "usr/lib/modules/subdir/libmymodule.so"}, {"type": "implibempty", "file": "usr/lib/modules/libnosyms"}, {"type": "pdb", "file": "usr/lib/modules/nosyms"} ]