From c91e3225ac5deb5fa89ee7959ea7da9145f156c9 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. The rename parameter must either be empty, indicating that renaming is disabled, or have length equal to the number of outputs generated. Using an empty list to disable renaming matches the behavior of install_data. Signed-off-by: Keith Packard --- cross/linux-mingw-w64-64bit.txt | 1 + docs/yaml/functions/_build_target_base.yaml | 10 ++++++++ docs/yaml/functions/custom_target.yaml | 10 ++++++++ mesonbuild/backend/backends.py | 20 +++++++++------- mesonbuild/build.py | 18 +++++++++++++-- mesonbuild/interpreter/interpreter.py | 13 ++++++++++- mesonbuild/interpreter/type_checking.py | 7 ++++++ mesonbuild/minstall.py | 23 +++++++++++-------- mesonbuild/mintro.py | 4 ++-- .../109 custom target capture/meson.build | 12 ++++++++++ .../109 custom target capture/test.json | 3 ++- .../common/117 shared module/meson.build | 15 ++++++++++++ test cases/common/117 shared module/test.json | 5 +++- 13 files changed, 117 insertions(+), 24 deletions(-) diff --git a/cross/linux-mingw-w64-64bit.txt b/cross/linux-mingw-w64-64bit.txt index 08fa70410430..7c846b88c511 100644 --- a/cross/linux-mingw-w64-64bit.txt +++ b/cross/linux-mingw-w64-64bit.txt @@ -10,6 +10,7 @@ exe_wrapper = 'wine' cmake = '/usr/bin/cmake' [properties] +skip_sanity_check = true # Directory that contains 'bin', 'lib', etc root = '/usr/x86_64-w64-mingw32' # Directory that contains 'bin', 'lib', etc for the toolchain and system libraries diff --git a/docs/yaml/functions/_build_target_base.yaml b/docs/yaml/functions/_build_target_base.yaml index 1721b29cfe5a..76ef76437eb7 100644 --- a/docs/yaml/functions/_build_target_base.yaml +++ b/docs/yaml/functions/_build_target_base.yaml @@ -328,3 +328,13 @@ 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.7.0 + 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`. The `rename` list must either be + empty (which disables this feature) or its length 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..11586156b416 100644 --- a/docs/yaml/functions/custom_target.yaml +++ b/docs/yaml/functions/custom_target.yaml @@ -235,3 +235,13 @@ 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.7.0 + 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`. The `rename` list must either be + empty (which disables this feature) or its length must be equal + to the number of outputs, diff --git a/mesonbuild/backend/backends.py b/mesonbuild/backend/backends.py index a4be50f664b1..a51d801ce8c9 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 @@ -1809,16 +1813,16 @@ def generate_target_install(self, d: InstallData) -> None: # to the length of outputs… if num_outdirs == 1 and num_out > 1: if first_outdir is not False: - for output, tag in zip(t.get_outputs(), t.install_tag): + for output, tag, out_fname in zip(t.get_outputs(), t.install_tag, rename): tag = tag or self.guess_install_tag(output, first_outdir) f = os.path.join(self.get_target_dir(t), output) 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=out_fname) 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..ea322916b403 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. @@ -3093,13 +3107,13 @@ class Data(HoldableObject): install_dir_name: str install_mode: 'FileMode' subproject: str - rename: T.List[str] = None + rename: T.Optional[T.List[str]] = None install_tag: T.Optional[str] = None data_type: str = None follow_symlinks: T.Optional[bool] = None def __post_init__(self) -> None: - if self.rename is None: + if not self.rename: self.rename = [os.path.basename(f.fname) for f in self.sources] @dataclass(eq=False) diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index 58385c58c5f7..5d2429e3873e 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), NoneType), default=None, listify=True, since='1.7.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 kwargs['rename'] and len(kwargs['rename']) != len(kwargs['output']): + raise InvalidArguments('custom_target: rename argument must either be empty or have ' + 'length equal to the number of outputs. ' + 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 kwargs['rename'] and len(kwargs['rename']) != len(target.outputs): + raise InvalidArguments('build_target: rename argument must either be empty or have ' + 'length equal to 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..9f86ec53f5c8 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), NoneType), + listify=True, + default=None, + since='1.7.0', + ), ] diff --git a/mesonbuild/minstall.py b/mesonbuild/minstall.py index 860826bf1b84..d86fe395853d 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,8 @@ 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) + outname = outname.rstrip('/') + 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..f169a1e953e4 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.outdir, t.out_fname) 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.outdir, i.out_fname))] 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..1faea007a4f0 100644 --- a/test cases/common/117 shared module/meson.build +++ b/test cases/common/117 shared module/meson.build @@ -34,6 +34,21 @@ test('import test', e, args : m) m2 = build_target('mymodule2', 'module.c', target_type: 'shared_module') test('import test 2', e, args : m2) +if build_machine.system() == 'windows' + install_ext = '.dll' +elif build_machine.system() == 'darwin' + install_ext = '.dylib' +else + install_ext = '.so' +endif + +# 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' + install_ext) +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..2847a4887805 100644 --- a/test cases/common/117 shared module/test.json +++ b/test cases/common/117 shared module/test.json @@ -2,6 +2,9 @@ "installed": [ {"type": "expr", "file": "usr/lib/modules/libnosyms?so"}, {"type": "implibempty", "file": "usr/lib/modules/libnosyms"}, - {"type": "pdb", "file": "usr/lib/modules/nosyms"} + {"type": "pdb", "file": "usr/lib/modules/nosyms"}, + {"type": "expr", "file": "usr/lib/modules/subdir/libmymodule?so"}, + {"type": "implib", "file": "usr/lib/modules/subdir/libmymodule"}, + {"type": "pdb", "file": "usr/lib/modules/subdir/mymodule"} ] }