From 8c5aa031b5ffb4eb3c61083072dcab49c8927d45 Mon Sep 17 00:00:00 2001 From: Xavier Claessens Date: Thu, 12 Aug 2021 22:13:51 -0400 Subject: [PATCH] Add install tags Fixes: #7007. --- docs/markdown/Installing.md | 76 ++++++++--- docs/markdown/Python-module.md | 3 + docs/markdown/Reference-manual.md | 33 ++++- docs/markdown/snippets/install_tag.md | 6 + mesonbuild/backend/backends.py | 80 ++++++++--- mesonbuild/backend/ninjabackend.py | 2 + mesonbuild/build.py | 20 ++- mesonbuild/interpreter/interpreter.py | 24 +++- mesonbuild/interpreter/mesonmain.py | 15 +-- mesonbuild/minstall.py | 24 ++-- mesonbuild/modules/gnome.py | 4 +- mesonbuild/modules/hotdoc.py | 1 + mesonbuild/modules/i18n.py | 1 + mesonbuild/modules/pkgconfig.py | 2 +- mesonbuild/modules/python.py | 8 +- mesonbuild/modules/qt.py | 1 + test cases/unit/98 install tag/bar-custom.txt | 0 test cases/unit/98 install tag/bar-devel.h | 0 test cases/unit/98 install tag/bar-notag.txt | 0 test cases/unit/98 install tag/foo.in | 0 test cases/unit/98 install tag/foo1-devel.h | 0 test cases/unit/98 install tag/lib.c | 9 ++ test cases/unit/98 install tag/main.c | 3 + test cases/unit/98 install tag/meson.build | 73 ++++++++++ test cases/unit/98 install tag/script.py | 7 + unittests/allplatformstests.py | 125 ++++++++++++++++-- 26 files changed, 430 insertions(+), 87 deletions(-) create mode 100644 docs/markdown/snippets/install_tag.md create mode 100644 test cases/unit/98 install tag/bar-custom.txt create mode 100644 test cases/unit/98 install tag/bar-devel.h create mode 100644 test cases/unit/98 install tag/bar-notag.txt create mode 100644 test cases/unit/98 install tag/foo.in create mode 100644 test cases/unit/98 install tag/foo1-devel.h create mode 100644 test cases/unit/98 install tag/lib.c create mode 100644 test cases/unit/98 install tag/main.c create mode 100644 test cases/unit/98 install tag/meson.build create mode 100644 test cases/unit/98 install tag/script.py diff --git a/docs/markdown/Installing.md b/docs/markdown/Installing.md index 42c71f1a4..4a87ca3a4 100644 --- a/docs/markdown/Installing.md +++ b/docs/markdown/Installing.md @@ -74,7 +74,7 @@ giving an absolute install path. install_data(sources : 'foo.dat', install_dir : '/etc') # -> /etc/foo.dat ``` -## Custom install behavior +## Custom install script Sometimes you need to do more than just install basic targets. Meson makes this easy by allowing you to specify a custom script to execute @@ -133,23 +133,57 @@ command in the build tree: $ meson install --no-rebuild --only-changed ``` -## Finer control over install locations - -Sometimes it is necessary to only install a subsection of output files -or install them in different directories. This can be done by -specifying `install_dir` as an array rather than a single string. The -array must have as many items as there are outputs and each entry -specifies how the corresponding output file should be installed. For -example: - -```meson -custom_target(... - output: ['file1', 'file2', 'file3'], - install_dir: ['path1', false, 'path3'], - ... -) -``` - -In this case `file1` would be installed to `/prefix/path1/file1`, -`file2` would not be installed at all and `file3` would be installed -to `/prefix/path3/file3'. +## Installation tags + +*Since 0.60.0* + +It is possible to install only a subset of the installable files using +`meson install --tags tag1,tag2` command line. When `--tags` is specified, only +files that have been tagged with one of the tags are going to be installed. + +This is intended to be used by packagers (e.g. distributions) who typically +want to split `libfoo`, `libfoo-dev` and `libfoo-doc` packages. Instead of +duplicating the list of installed files per category in each packaging system, +it can be maintained in a single place, directly in upstream `meson.build` files. + +Meson sets predefined tags on some files. More tags are likely to be added over +time, please help extending the list of well known categories. +- `devel`: + * `static_library()`, + * `install_headers()`, + * `pkgconfig.generate()`, + * `gnome.generate_gir()` - `.gir` file, + * Files installed into `libdir` and with `.a` or `.pc` extension, + * File installed into `includedir`. +- `runtime`: + * `executable()`, + * `shared_library()`, + * `shared_module()`, + * `jar()`, + * Files installed into `bindir`. + * Files installed into `libdir` and with `.so` or `.dll` extension. +- `python-runtime`: + * `python.install_sources()`. +- `man`: + * `install_man()`. +- `doc`: + * `gnome.gtkdoc()`, + * `hotdoc.generate_doc()`. +- `i18n`: + * `i18n.gettext()`, + * `qt.compile_translations()`, + * Files installed into `localedir`. +- `typelib`: + * `gnome.generate_gir()` - `.typelib` file. + +Custom installation tag can be set using the `install_tag` keyword argument +on various functions such as `custom_target()`, `configure_file()`, +`install_subdir()` and `install_data()`. See their respective documentation +in the reference manual for details. It is recommended to use one of the +predefined tags above when possible. + +Installable files that have not been tagged either automatically by Meson, or +manually using `install_tag` keyword argument won't be installed when `--tags` +is used. They are reported at the end of `/meson-logs/meson-log.txt`, +it is recommended to add missing `install_tag` to have a tag on each installable +files. diff --git a/docs/markdown/Python-module.md b/docs/markdown/Python-module.md index 3b7b4f59c..20eae1a32 100644 --- a/docs/markdown/Python-module.md +++ b/docs/markdown/Python-module.md @@ -135,6 +135,9 @@ All positional and keyword arguments are the same as for - `subdir`: See documentation for the argument of the same name to [][`extension_module()`] +- `install_tag` *(since 0.60.0)*: A string used by `meson install --tags` command + to install only a subset of the files. By default it has the tag `python-runtime`. + #### `get_install_dir()` ``` meson diff --git a/docs/markdown/Reference-manual.md b/docs/markdown/Reference-manual.md index 6454830ca..fd78a1edc 100644 --- a/docs/markdown/Reference-manual.md +++ b/docs/markdown/Reference-manual.md @@ -304,6 +304,9 @@ false otherwise. string, the file is not installed. - `install_mode` *(since 0.47.0)*: specify the file mode in symbolic format and optionally the owner/uid and group/gid for the installed files. +- `install_tag` *(since 0.60.0)*: A string used by `meson install --tags` command + to install only a subset of the files. By default the file has no install + tag which means it is not being installed when `--tags` argument is specified. - `output`: the output file name. *(since 0.41.0)* may contain `@PLAINNAME@` or `@BASENAME@` substitutions. In configuration mode, the permissions of the input file (if it is specified) are copied to @@ -378,7 +381,7 @@ following. - `install_dir`: If only one install_dir is provided, all outputs are installed there. *Since 0.40.0* Allows you to specify the installation directory for each corresponding output. For example: - ``` + ```meson custom_target('different-install-dirs', output : ['first.file', 'second.file'], install : true, @@ -388,15 +391,21 @@ following. To only install some outputs, pass `false` for the outputs that you don't want installed. For example: - ``` - custom_target('only-install-second', - output : ['first.file', 'second.file'], - install : true, - install_dir : [false, 'otherdir]) + ```meson + custom_target('only-install-second', + output : ['first.file', 'second.file'], + install : true, + install_dir : [false, 'otherdir]) ``` This would install `second.file` to `otherdir` and not install `first.file`. - `install_mode` *(since 0.47.0)*: the file mode and optionally the owner/uid and group/gid +- `install_tag` *(since 0.60.0)*: A list of strings, one per output, used by + `meson install --tags` command to install only a subset of the files. + By default all outputs have no install tag which means they are not being + installed when `--tags` argument is specified. If only one tag is specified, + it is assumed that all outputs have the same tag. `false` can be used for + outputs that have no tag or are not installed. - `output`: list of output files - `env` *(since 0.57.0)*: environment variables to set, such as `{'NAME1': 'value1', 'NAME2': 'value2'}` or `['NAME1=value1', 'NAME2=value2']`, @@ -729,6 +738,9 @@ be passed to [shared and static libraries](#library). and optionally the owner/uid and group/gid for the installed files. - `install_rpath`: a string to set the target's rpath to after install (but *not* before that). On Windows, this argument has no effect. +- `install_tag` *(since 0.60.0)*: A string used by `meson install --tags` command + to install only a subset of the files. By default all build targets have the + tag `runtime` except for static libraries that have the `devel` tag. - `objects`: list of prebuilt object files (usually for third party products you don't have source to) that should be linked in this target, **never** use this for object files that you build yourself. @@ -1103,6 +1115,9 @@ arguments. The following keyword arguments are supported: file from `rename` list. Nested paths are allowed and they are joined with `install_dir`. Length of `rename` list must be equal to the number of sources. +- `install_tag` *(since 0.60.0)*: A string used by `meson install --tags` command + to install only a subset of the files. By default these files have no install + tag which means they are not being installed when `--tags` argument is specified. See [Installing](Installing.md) for more examples. @@ -1201,6 +1216,9 @@ The following keyword arguments are supported: the owner/uid and group/gid for the installed files. - `strip_directory` *(since 0.45.0)*: install directory contents. `strip_directory=false` by default. If `strip_directory=true` only the last component of the source path is used. +- `install_tag` *(since 0.60.0)*: A string used by `meson install --tags` command + to install only a subset of the files. By default these files have no install + tag which means they are not being installed when `--tags` argument is specified. For a given directory `foo`: ```text @@ -1975,6 +1993,9 @@ the following methods. can be specified. If `true` the script will not be run if DESTDIR is set during installation. This is useful in the case the script updates system wide cache that is only needed when copying files into final destination. + *(since 0.60.0)* `install_tag` string keyword argument can be specified. + By default the script has no install tag which means it is not being run when + `meson install --tags` argument is specified. *(since 0.54.0)* If `meson install` is called with the `--quiet` option, the environment variable `MESON_INSTALL_QUIET` will be set. diff --git a/docs/markdown/snippets/install_tag.md b/docs/markdown/snippets/install_tag.md new file mode 100644 index 000000000..36df63924 --- /dev/null +++ b/docs/markdown/snippets/install_tag.md @@ -0,0 +1,6 @@ +## Installation tags + +It is now possible to install only a subset of the installable files using +`meson install --tags tag1,tag2` command line. + +See [documentation](Installing.md#installation-tags) for more details. diff --git a/mesonbuild/backend/backends.py b/mesonbuild/backend/backends.py index aa8e844a7..6c877eab6 100644 --- a/mesonbuild/backend/backends.py +++ b/mesonbuild/backend/backends.py @@ -115,7 +115,8 @@ class InstallData: class TargetInstallData: def __init__(self, fname: str, outdir: str, aliases: T.Dict[str, str], strip: bool, install_name_mappings: T.Dict, rpath_dirs_to_remove: T.Set[bytes], - install_rpath: str, install_mode: 'FileMode', subproject: str, optional: bool = False): + install_rpath: str, install_mode: 'FileMode', subproject: str, + optional: bool = False, tag: T.Optional[str] = None): self.fname = fname self.outdir = outdir self.aliases = aliases @@ -126,22 +127,27 @@ class TargetInstallData: self.install_mode = install_mode self.subproject = subproject self.optional = optional + self.tag = tag class InstallDataBase: - def __init__(self, path: str, install_path: str, install_mode: 'FileMode', subproject: str): + def __init__(self, path: str, install_path: str, install_mode: 'FileMode', + subproject: str, tag: T.Optional[str] = None): self.path = path self.install_path = install_path self.install_mode = install_mode self.subproject = subproject + self.tag = tag class SubdirInstallData(InstallDataBase): - def __init__(self, path: str, install_path: str, install_mode: 'FileMode', exclude, subproject: str): - super().__init__(path, install_path, install_mode, subproject) + def __init__(self, path: str, install_path: str, install_mode: 'FileMode', + exclude, subproject: str, tag: T.Optional[str] = None): + super().__init__(path, install_path, install_mode, subproject, tag) self.exclude = exclude class ExecutableSerialisation: def __init__(self, cmd_args, env: T.Optional[build.EnvironmentVariables] = None, exe_wrapper=None, - workdir=None, extra_paths=None, capture=None, feed=None) -> None: + workdir=None, extra_paths=None, capture=None, feed=None, + tag: T.Optional[str] = None) -> None: self.cmd_args = cmd_args self.env = env if exe_wrapper is not None: @@ -155,6 +161,7 @@ class ExecutableSerialisation: self.skip_if_destdir = False self.verbose = False self.subproject = '' + self.tag = tag class TestSerialisation: def __init__(self, name: str, project: str, suite: T.List[str], fname: T.List[str], @@ -443,7 +450,8 @@ class Backend: def get_executable_serialisation(self, cmd, workdir=None, extra_bdeps=None, capture=None, feed=None, - env: T.Optional[build.EnvironmentVariables] = None): + env: T.Optional[build.EnvironmentVariables] = None, + tag: T.Optional[str] = None): exe = cmd[0] cmd_args = cmd[1:] if isinstance(exe, programs.ExternalProgram): @@ -490,7 +498,7 @@ class Backend: workdir = workdir or self.environment.get_build_dir() return ExecutableSerialisation(exe_cmd + cmd_args, env, exe_wrapper, workdir, - extra_paths, capture, feed) + extra_paths, capture, feed, tag) def as_meson_exe_cmdline(self, tname, exe, cmd_args, workdir=None, extra_bdeps=None, capture=None, feed=None, @@ -1032,7 +1040,7 @@ class Backend: with open(ifilename, 'w', encoding='utf-8') as f: f.write(json.dumps(mfobj)) # Copy file from, to, and with mode unchanged - d.data.append(InstallDataBase(ifilename, ofilename, None, '')) + d.data.append(InstallDataBase(ifilename, ofilename, None, '', tag='devel')) def get_regen_filelist(self): '''List of all files whose alteration means that the build @@ -1354,6 +1362,27 @@ class Backend: with open(install_data_file, 'wb') as ofile: pickle.dump(self.create_install_data(), ofile) + def guess_install_tag(self, fname: str, outdir: T.Optional[str] = None) -> T.Optional[str]: + prefix = self.environment.get_prefix() + bindir = Path(prefix, self.environment.get_bindir()) + libdir = Path(prefix, self.environment.get_libdir()) + incdir = Path(prefix, self.environment.get_includedir()) + localedir = Path(prefix, self.environment.coredata.get_option(mesonlib.OptionKey('localedir'))) + dest_path = Path(prefix, outdir, Path(fname).name) if outdir else Path(prefix, fname) + if bindir in dest_path.parents: + return 'runtime' + elif libdir in dest_path.parents: + if dest_path.suffix in {'.a', '.pc'}: + return 'devel' + elif dest_path.suffix in {'.so', '.dll'}: + return 'runtime' + elif incdir in dest_path.parents: + return 'devel' + elif localedir in dest_path.parents: + return 'i18n' + mlog.debug('Failed to guess install tag for', dest_path) + return None + def generate_target_install(self, d: InstallData) -> None: for t in self.build.get_targets().values(): if not t.should_install(): @@ -1366,6 +1395,7 @@ class Backend: "Pass 'false' for outputs that should not be installed and 'true' for\n" \ 'using the default installation directory for an output.' raise MesonException(m.format(t.name, num_out, t.get_outputs(), num_outdirs)) + assert len(t.install_tag) == num_out install_mode = t.get_custom_install_mode() # Install the target output(s) if isinstance(t, build.BuildTarget): @@ -1387,11 +1417,13 @@ class Backend: # Install primary build output (library/executable/jar, etc) # Done separately because of strip/aliases/rpath if outdirs[0] is not False: + tag = t.install_tag[0] or ('devel' if isinstance(t, build.StaticLibrary) else 'runtime') mappings = t.get_link_deps_mapping(d.prefix, self.environment) i = TargetInstallData(self.get_target_filename(t), outdirs[0], t.get_aliases(), should_strip, mappings, t.rpath_dirs_to_remove, - t.install_rpath, install_mode, t.subproject) + t.install_rpath, install_mode, t.subproject, + tag=tag) d.targets.append(i) if isinstance(t, (build.SharedLibrary, build.SharedModule, build.Executable)): @@ -1409,7 +1441,8 @@ class Backend: # Install the import library; may not exist for shared modules i = TargetInstallData(self.get_target_filename_for_linking(t), implib_install_dir, {}, False, {}, set(), '', install_mode, - t.subproject, optional=isinstance(t, build.SharedModule)) + t.subproject, optional=isinstance(t, build.SharedModule), + tag='devel') d.targets.append(i) if not should_strip and t.get_debug_filename(): @@ -1417,17 +1450,19 @@ class Backend: i = TargetInstallData(debug_file, outdirs[0], {}, False, {}, set(), '', install_mode, t.subproject, - optional=True) + optional=True, tag='devel') d.targets.append(i) # Install secondary outputs. Only used for Vala right now. if num_outdirs > 1: - for output, outdir in zip(t.get_outputs()[1:], outdirs[1:]): + for output, outdir, tag in zip(t.get_outputs()[1:], outdirs[1:], t.install_tag[1:]): # User requested that we not install this output if outdir is False: continue f = os.path.join(self.get_target_dir(t), output) + tag = tag or self.guess_install_tag(f, outdir) i = TargetInstallData(f, outdir, {}, False, {}, set(), None, - install_mode, t.subproject) + install_mode, t.subproject, + tag=tag) d.targets.append(i) elif isinstance(t, build.CustomTarget): # If only one install_dir is specified, assume that all @@ -1438,19 +1473,23 @@ class Backend: # To selectively install only some outputs, pass `false` as # the install_dir for the corresponding output by index if num_outdirs == 1 and num_out > 1: - for output in t.get_outputs(): + for output, tag in zip(t.get_outputs(), t.install_tag): f = os.path.join(self.get_target_dir(t), output) + tag = tag or self.guess_install_tag(f, outdirs[0]) i = TargetInstallData(f, outdirs[0], {}, False, {}, set(), None, install_mode, - t.subproject, optional=not t.build_by_default) + t.subproject, optional=not t.build_by_default, + tag=tag) d.targets.append(i) else: - for output, outdir in zip(t.get_outputs(), outdirs): + for output, outdir, tag in zip(t.get_outputs(), outdirs, t.install_tag): # User requested that we not install this output if outdir is False: continue f = os.path.join(self.get_target_dir(t), output) + tag = tag or self.guess_install_tag(f, outdir) i = TargetInstallData(f, outdir, {}, False, {}, set(), None, install_mode, - t.subproject, optional=not t.build_by_default) + t.subproject, optional=not t.build_by_default, + tag=tag) d.targets.append(i) def generate_custom_install_script(self, d: InstallData) -> None: @@ -1475,7 +1514,7 @@ class Backend: if not isinstance(f, File): raise MesonException(f'Invalid header type {f!r} can\'t be installed') abspath = f.absolute_path(srcdir, builddir) - i = InstallDataBase(abspath, outdir, h.get_custom_install_mode(), h.subproject) + i = InstallDataBase(abspath, outdir, h.get_custom_install_mode(), h.subproject, tag='devel') d.headers.append(i) def generate_man_install(self, d: InstallData) -> None: @@ -1495,7 +1534,7 @@ class Backend: fname = fname.replace(f'.{m.locale}', '') srcabs = f.absolute_path(self.environment.get_source_dir(), self.environment.get_build_dir()) dstabs = os.path.join(subdir, os.path.basename(fname)) - i = InstallDataBase(srcabs, dstabs, m.get_custom_install_mode(), m.subproject) + i = InstallDataBase(srcabs, dstabs, m.get_custom_install_mode(), m.subproject, tag='man') d.man.append(i) def generate_data_install(self, d: InstallData): @@ -1510,7 +1549,8 @@ class Backend: for src_file, dst_name in zip(de.sources, de.rename): assert(isinstance(src_file, mesonlib.File)) dst_abs = os.path.join(subdir, dst_name) - i = InstallDataBase(src_file.absolute_path(srcdir, builddir), dst_abs, de.install_mode, de.subproject) + tag = de.install_tag or self.guess_install_tag(dst_abs) + i = InstallDataBase(src_file.absolute_path(srcdir, builddir), dst_abs, de.install_mode, de.subproject, tag=tag) d.data.append(i) def generate_subdir_install(self, d: InstallData) -> None: diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py index 871bf9f5c..68c63b233 100644 --- a/mesonbuild/backend/ninjabackend.py +++ b/mesonbuild/backend/ninjabackend.py @@ -1513,6 +1513,7 @@ class NinjaBackend(backends.Backend): args += ['--vapi', os.path.join('..', target.vala_vapi)] valac_outputs.append(vapiname) target.outputs += [target.vala_header, target.vala_vapi] + target.install_tag += ['devel', 'devel'] # Install header and vapi to default locations if user requests this if len(target.install_dir) > 1 and target.install_dir[1] is True: target.install_dir[1] = self.environment.get_includedir() @@ -1524,6 +1525,7 @@ class NinjaBackend(backends.Backend): args += ['--gir', os.path.join('..', target.vala_gir)] valac_outputs.append(girname) target.outputs.append(target.vala_gir) + target.install_tag.append('devel') # Install GIR to default location if requested by user if len(target.install_dir) > 3 and target.install_dir[3] is True: target.install_dir[3] = os.path.join(self.environment.get_datadir(), 'gir-1.0') diff --git a/mesonbuild/build.py b/mesonbuild/build.py index 28ad60ec7..69a2d76f3 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -90,6 +90,7 @@ buildtarget_kwargs = { 'install_rpath', 'install_dir', 'install_mode', + 'install_tag', 'name_prefix', 'name_suffix', 'native', @@ -189,7 +190,8 @@ class InstallDir(HoldableObject): install_mode: 'FileMode', exclude: T.Tuple[T.Set[str], T.Set[str]], strip_directory: bool, subproject: str, - from_source_dir: bool = True): + from_source_dir: bool = True, + install_tag: T.Optional[str] = None): self.source_subdir = src_subdir self.installable_subdir = inst_subdir self.install_dir = install_dir @@ -198,6 +200,7 @@ class InstallDir(HoldableObject): self.strip_directory = strip_directory self.from_source_dir = from_source_dir self.subproject = subproject + self.install_tag = install_tag class Build: @@ -1014,6 +1017,7 @@ class BuildTarget(Target): self.install_dir = typeslistify(kwargs.get('install_dir', [None]), (str, bool)) self.install_mode = kwargs.get('install_mode', None) + self.install_tag = stringlistify(kwargs.get('install_tag', [None])) main_class = kwargs.get('main_class', '') if not isinstance(main_class, str): raise InvalidArguments('Main class must be a string') @@ -2206,6 +2210,7 @@ class CustomTarget(Target, CommandBase): 'install', 'install_dir', 'install_mode', + 'install_tag', 'build_always', 'build_always_stale', 'depends', @@ -2339,10 +2344,19 @@ class CustomTarget(Target, CommandBase): # the list index of that item will not be installed self.install_dir = typeslistify(kwargs['install_dir'], (str, bool)) self.install_mode = kwargs.get('install_mode', None) + # If only one tag is provided, assume all outputs have the same tag. + # Otherwise, we must have as much tags as outputs. + self.install_tag = typeslistify(kwargs.get('install_tag', [None]), (str, bool)) + if len(self.install_tag) == 1: + self.install_tag = self.install_tag * len(self.outputs) + elif len(self.install_tag) != len(self.outputs): + m = f'Target {self.name!r} has {len(self.outputs)} outputs but {len(self.install_tag)} "install_tag"s were found.' + raise InvalidArguments(m) else: self.install = False self.install_dir = [None] self.install_mode = None + self.install_tag = [] if 'build_always' in kwargs and 'build_always_stale' in kwargs: raise InvalidArguments('build_always and build_always_stale are mutually exclusive. Combine build_by_default and build_always_stale.') elif 'build_always' in kwargs: @@ -2625,10 +2639,12 @@ class ConfigurationData(HoldableObject): class Data(HoldableObject): def __init__(self, sources: T.List[File], install_dir: str, install_mode: 'FileMode', subproject: str, - rename: T.List[str] = None): + rename: T.List[str] = None, + install_tag: T.Optional[str] = None): self.sources = sources self.install_dir = install_dir self.install_mode = install_mode + self.install_tag = install_tag if rename is None: self.rename = [os.path.basename(f.fname) for f in self.sources] else: diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index ab7efa0cd..535229cb1 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -1598,6 +1598,7 @@ external dependencies (including libraries) must go to "dependencies".''') def func_subdir_done(self, node, args, kwargs): raise SubdirDoneRequest() + @FeatureNewKwargs('custom_target', '0.60.0', ['install_tag']) @FeatureNewKwargs('custom_target', '0.57.0', ['env']) @FeatureNewKwargs('custom_target', '0.48.0', ['console']) @FeatureNewKwargs('custom_target', '0.47.0', ['install_mode', 'build_always_stale']) @@ -1606,7 +1607,7 @@ external dependencies (including libraries) must go to "dependencies".''') @permittedKwargs({'input', 'output', 'command', 'install', 'install_dir', 'install_mode', 'build_always', 'capture', 'depends', 'depend_files', 'depfile', 'build_by_default', 'build_always_stale', 'console', 'env', - 'feed'}) + 'feed', 'install_tag'}) @typed_pos_args('custom_target', str) def func_custom_target(self, node: mparser.FunctionNode, args: T.Tuple[str], kwargs: 'TYPE_kwargs') -> build.CustomTarget: if 'depfile' in kwargs and ('@BASENAME@' in kwargs['depfile'] or '@PLAINNAME@' in kwargs['depfile']): @@ -1903,6 +1904,7 @@ This will become a hard error in the future.''' % kwargs['input'], location=self KwargInfo('sources', ContainerTypeInfo(list, (str, mesonlib.File)), listify=True, default=[]), KwargInfo('rename', ContainerTypeInfo(list, str), default=[], listify=True, since='0.46.0'), INSTALL_MODE_KW.evolve(since='0.38.0'), + KwargInfo('install_tag', str, since='0.60.0'), ) def func_install_data(self, node: mparser.BaseNode, args: T.Tuple[T.List['mesonlib.FileOrString']], @@ -1915,12 +1917,14 @@ This will become a hard error in the future.''' % kwargs['input'], location=self '"rename" and "sources" argument lists must be the same length if "rename" is given. ' f'Rename has {len(rename)} elements and sources has {len(sources)}.') - return self.install_data_impl(sources, kwargs['install_dir'], kwargs['install_mode'], rename) + return self.install_data_impl(sources, kwargs['install_dir'], kwargs['install_mode'], rename, + kwargs['install_tag']) def install_data_impl(self, sources: T.List[mesonlib.File], install_dir: str, - install_mode: FileMode, rename: T.Optional[str]) -> build.Data: + install_mode: FileMode, rename: T.Optional[str], + tag: T.Optional[str]) -> build.Data: """Just the implementation with no validation.""" - data = build.Data(sources, install_dir, install_mode, self.subproject, rename) + data = build.Data(sources, install_dir, install_mode, self.subproject, rename, tag) self.build.data.append(data) return data @@ -1928,6 +1932,7 @@ This will become a hard error in the future.''' % kwargs['input'], location=self @typed_kwargs( 'install_subdir', KwargInfo('install_dir', str, required=True), + KwargInfo('install_tag', str, since='0.60.0'), KwargInfo('strip_directory', bool, default=False), KwargInfo('exclude_files', ContainerTypeInfo(list, str), default=[], listify=True, since='0.42.0', @@ -1947,7 +1952,8 @@ This will become a hard error in the future.''' % kwargs['input'], location=self kwargs['install_mode'], exclude, kwargs['strip_directory'], - self.subproject) + self.subproject, + install_tag=kwargs['install_tag']) self.build.install_dirs.append(idir) return idir @@ -1956,9 +1962,10 @@ This will become a hard error in the future.''' % kwargs['input'], location=self @FeatureNewKwargs('configure_file', '0.41.0', ['capture']) @FeatureNewKwargs('configure_file', '0.50.0', ['install']) @FeatureNewKwargs('configure_file', '0.52.0', ['depfile']) + @FeatureNewKwargs('configure_file', '0.60.0', ['install_tag']) @permittedKwargs({'input', 'output', 'configuration', 'command', 'copy', 'depfile', 'install_dir', 'install_mode', 'capture', 'install', 'format', - 'output_format', 'encoding'}) + 'output_format', 'encoding', 'install_tag'}) @noPosargs def func_configure_file(self, node, args, kwargs): if 'output' not in kwargs: @@ -2139,7 +2146,10 @@ This will become a hard error in the future.''' % kwargs['input'], location=self 'is true') cfile = mesonlib.File.from_built_file(ofile_path, ofile_fname) install_mode = self._get_kwarg_install_mode(kwargs) - self.build.data.append(build.Data([cfile], idir, install_mode, self.subproject)) + install_tag = kwargs.get('install_tag') + if install_tag is not None and not isinstance(install_tag, str): + raise InvalidArguments('install_tag keyword argument must be string') + self.build.data.append(build.Data([cfile], idir, install_mode, self.subproject, install_tag=install_tag)) return mesonlib.File.from_built_file(self.subdir, output) def extract_incdirs(self, kwargs): diff --git a/mesonbuild/interpreter/mesonmain.py b/mesonbuild/interpreter/mesonmain.py index 97a695b9d..ea16b46a5 100644 --- a/mesonbuild/interpreter/mesonmain.py +++ b/mesonbuild/interpreter/mesonmain.py @@ -7,9 +7,9 @@ from .. import mlog from ..mesonlib import MachineChoice, OptionKey from ..programs import OverrideProgram, ExternalProgram -from ..interpreterbase import (MesonInterpreterObject, FeatureNewKwargs, FeatureNew, FeatureDeprecated, +from ..interpreterbase import (MesonInterpreterObject, FeatureNew, FeatureDeprecated, typed_pos_args, permittedKwargs, noArgsFlattening, noPosargs, noKwargs, - MesonVersionString, InterpreterException) + typed_kwargs, KwargInfo, MesonVersionString, InterpreterException) from .interpreterobjects import (ExecutableHolder, ExternalProgramHolder, CustomTargetHolder, CustomTargetIndexHolder, @@ -107,20 +107,19 @@ class MesonMain(MesonInterpreterObject): '0.55.0', self.interpreter.subproject) return script_args - @FeatureNewKwargs('add_install_script', '0.57.0', ['skip_if_destdir']) - @permittedKwargs({'skip_if_destdir'}) + @typed_kwargs('add_install_script', + KwargInfo('skip_if_destdir', bool, default=False, since='0.57.0'), + KwargInfo('install_tag', str, since='0.60.0')) def add_install_script_method(self, args: 'T.Tuple[T.Union[str, mesonlib.File, ExecutableHolder], T.Union[str, mesonlib.File, CustomTargetHolder, CustomTargetIndexHolder], ...]', kwargs): if len(args) < 1: raise InterpreterException('add_install_script takes one or more arguments') if isinstance(args[0], mesonlib.File): FeatureNew.single_use('Passing file object to script parameter of add_install_script', '0.57.0', self.interpreter.subproject) - skip_if_destdir = kwargs.get('skip_if_destdir', False) - if not isinstance(skip_if_destdir, bool): - raise InterpreterException('skip_if_destdir keyword argument must be boolean') script_args = self._process_script_args('add_install_script', args[1:], allow_built=True) script = self._find_source_script(args[0], script_args) - script.skip_if_destdir = skip_if_destdir + script.skip_if_destdir = kwargs['skip_if_destdir'] + script.tag = kwargs['install_tag'] self.build.install_scripts.append(script) @permittedKwargs(set()) diff --git a/mesonbuild/minstall.py b/mesonbuild/minstall.py index d8faad625..6660d6c58 100644 --- a/mesonbuild/minstall.py +++ b/mesonbuild/minstall.py @@ -25,7 +25,7 @@ import sys import typing as T from . import environment -from .backend.backends import InstallData +from .backend.backends import InstallData, InstallDataBase, TargetInstallData, ExecutableSerialisation from .coredata import major_versions_differ, MesonVersionMismatchException from .coredata import version as coredata_version from .mesonlib import Popen_safe, RealPathAction, is_windows @@ -56,6 +56,7 @@ if T.TYPE_CHECKING: destdir: str dry_run: bool skip_subprojects: str + tags: str symlink_warning = '''Warning: trying to copy a symlink that points to a file. This will copy the file, @@ -81,6 +82,8 @@ def add_arguments(parser: argparse.ArgumentParser) -> None: help='Doesn\'t actually install, but print logs. (Since 0.57.0)') parser.add_argument('--skip-subprojects', nargs='?', const='*', default='', help='Do not install files from given subprojects. (Since 0.58.0)') + parser.add_argument('--tags', default=None, + help='Install only targets having one of the given tags. (Since 0.60.0)') class DirMaker: def __init__(self, lf: T.TextIO, makedirs: T.Callable[..., None]): @@ -303,6 +306,7 @@ class Installer: # ['*'] means skip all, # ['sub1', ...] means skip only those. self.skip_subprojects = [i.strip() for i in options.skip_subprojects.split(',')] + self.tags = [i.strip() for i in options.tags.split(',')] if options.tags else None def remove(self, *args: T.Any, **kwargs: T.Any) -> None: if not self.dry_run: @@ -371,8 +375,10 @@ class Installer: return run_exe(*args, **kwargs) return 0 - def install_subproject(self, subproject: str) -> bool: - if subproject and (subproject in self.skip_subprojects or '*' in self.skip_subprojects): + def should_install(self, d: T.Union[TargetInstallData, InstallDataBase, ExecutableSerialisation]) -> bool: + if d.subproject and (d.subproject in self.skip_subprojects or '*' in self.skip_subprojects): + return False + if self.tags and d.tag not in self.tags: return False return True @@ -552,7 +558,7 @@ class Installer: def install_subdirs(self, d: InstallData, dm: DirMaker, destdir: str, fullprefix: str) -> None: for i in d.install_subdirs: - if not self.install_subproject(i.subproject): + if not self.should_install(i): continue self.did_install_something = True full_dst_dir = get_destdir_path(destdir, fullprefix, i.install_path) @@ -562,7 +568,7 @@ class Installer: def install_data(self, d: InstallData, dm: DirMaker, destdir: str, fullprefix: str) -> None: for i in d.data: - if not self.install_subproject(i.subproject): + if not self.should_install(i): continue fullfilename = i.path outfilename = get_destdir_path(destdir, fullprefix, i.install_path) @@ -573,7 +579,7 @@ class Installer: def install_man(self, d: InstallData, dm: DirMaker, destdir: str, fullprefix: str) -> None: for m in d.man: - if not self.install_subproject(m.subproject): + if not self.should_install(m): continue full_source_filename = m.path outfilename = get_destdir_path(destdir, fullprefix, m.install_path) @@ -584,7 +590,7 @@ class Installer: def install_headers(self, d: InstallData, dm: DirMaker, destdir: str, fullprefix: str) -> None: for t in d.headers: - if not self.install_subproject(t.subproject): + if not self.should_install(t): continue fullfilename = t.path fname = os.path.basename(fullfilename) @@ -605,7 +611,7 @@ class Installer: env['MESON_INSTALL_QUIET'] = '1' for i in d.install_scripts: - if not self.install_subproject(i.subproject): + if not self.should_install(i): continue name = ' '.join(i.cmd_args) if i.skip_if_destdir and destdir: @@ -625,7 +631,7 @@ class Installer: def install_targets(self, d: InstallData, dm: DirMaker, destdir: str, fullprefix: str) -> None: for t in d.targets: - if not self.install_subproject(t.subproject): + if not self.should_install(t): continue if not os.path.exists(t.fname): # For example, import libraries of shared modules are optional diff --git a/mesonbuild/modules/gnome.py b/mesonbuild/modules/gnome.py index a9cd1d393..a2f98bc3f 100644 --- a/mesonbuild/modules/gnome.py +++ b/mesonbuild/modules/gnome.py @@ -747,6 +747,7 @@ class GnomeModule(ExtensionModule): scankwargs['install'] = kwargs['install'] scankwargs['install_dir'] = kwargs.get('install_dir_gir', os.path.join(state.environment.get_datadir(), 'gir-1.0')) + scankwargs['install_tag'] = 'devel' if 'build_by_default' in kwargs: scankwargs['build_by_default'] = kwargs['build_by_default'] @@ -764,6 +765,7 @@ class GnomeModule(ExtensionModule): typelib_kwargs['install'] = kwargs['install'] typelib_kwargs['install_dir'] = kwargs.get('install_dir_typelib', os.path.join(state.environment.get_libdir(), 'girepository-1.0')) + typelib_kwargs['install_tag'] = 'typelib' if 'build_by_default' in kwargs: typelib_kwargs['build_by_default'] = kwargs['build_by_default'] @@ -1146,7 +1148,7 @@ class GnomeModule(ExtensionModule): state.test(check_args, env=check_env, workdir=check_workdir, depends=custom_target) res = [custom_target, alias_target] if kwargs.get('install', True): - res.append(state.backend.get_executable_serialisation(command + args)) + res.append(state.backend.get_executable_serialisation(command + args, tag='doc')) return ModuleReturnValue(custom_target, res) def _get_build_args(self, kwargs, state, depends): diff --git a/mesonbuild/modules/hotdoc.py b/mesonbuild/modules/hotdoc.py index 4dccd067a..19a1728c7 100644 --- a/mesonbuild/modules/hotdoc.py +++ b/mesonbuild/modules/hotdoc.py @@ -354,6 +354,7 @@ class HotdocTargetBuilder: '--builddir', os.path.join(self.builddir, self.subdir)] + self.hotdoc.get_command() + ['run', '--conf-file', hotdoc_config_name]) + install_script.tag = 'doc' return (target, install_script) diff --git a/mesonbuild/modules/i18n.py b/mesonbuild/modules/i18n.py index 57d25fd56..539f82bdb 100644 --- a/mesonbuild/modules/i18n.py +++ b/mesonbuild/modules/i18n.py @@ -181,6 +181,7 @@ class I18nModule(ExtensionModule): # to custom_targets. Crude hack: set the build target's subdir manually. # Bonus: the build tree has something usable as an uninstalled bindtextdomain() target dir. 'install_dir': path.join(install_dir, l, 'LC_MESSAGES'), + 'install_tag': 'i18n', } gmotarget = build.CustomTarget(l+'.mo', path.join(state.subdir, l, 'LC_MESSAGES'), state.subproject, gmo_kwargs) targets.append(gmotarget) diff --git a/mesonbuild/modules/pkgconfig.py b/mesonbuild/modules/pkgconfig.py index 48bbc34de..e57c4f665 100644 --- a/mesonbuild/modules/pkgconfig.py +++ b/mesonbuild/modules/pkgconfig.py @@ -548,7 +548,7 @@ class PkgConfigModule(ExtensionModule): self._generate_pkgconfig_file(state, deps, subdirs, name, description, url, version, pcfile, conflicts, variables, unescaped_variables, False, dataonly) - res = build.Data([mesonlib.File(True, state.environment.get_scratch_dir(), pcfile)], pkgroot, None, state.subproject) + res = build.Data([mesonlib.File(True, state.environment.get_scratch_dir(), pcfile)], pkgroot, None, state.subproject, install_tag='devel') variables = self.interpreter.extract_variables(kwargs, argname='uninstalled_variables', dict_new=True) variables = parse_variable_list(variables) unescaped_variables = self.interpreter.extract_variables(kwargs, argname='unescaped_uninstalled_variables') diff --git a/mesonbuild/modules/python.py b/mesonbuild/modules/python.py index f38becf05..65a73a735 100644 --- a/mesonbuild/modules/python.py +++ b/mesonbuild/modules/python.py @@ -344,6 +344,7 @@ if T.TYPE_CHECKING: pure: bool subdir: str + install_tag: T.Optional[str] class PythonInstallation(ExternalProgramHolder): @@ -436,14 +437,15 @@ class PythonInstallation(ExternalProgramHolder): return dep @typed_pos_args('install_data', varargs=(str, mesonlib.File)) - @typed_kwargs('python_installation.install_sources', _PURE_KW, _SUBDIR_KW) + @typed_kwargs('python_installation.install_sources', _PURE_KW, _SUBDIR_KW, + KwargInfo('install_tag', str, since='0.60.0')) def install_sources_method(self, args: T.Tuple[T.List[T.Union[str, mesonlib.File]]], kwargs: 'PyInstallKw') -> 'Data': + tag = kwargs['install_tag'] or 'runtime' return self.interpreter.install_data_impl( self.interpreter.source_strings_to_files(args[0]), self._get_install_dir_impl(kwargs['pure'], kwargs['subdir']), - mesonlib.FileMode(), - None) + mesonlib.FileMode(), rename=None, tag=tag) @noPosargs @typed_kwargs('python_installation.install_dir', _PURE_KW, _SUBDIR_KW) diff --git a/mesonbuild/modules/qt.py b/mesonbuild/modules/qt.py index 5efd668ae..ed66ae94a 100644 --- a/mesonbuild/modules/qt.py +++ b/mesonbuild/modules/qt.py @@ -513,6 +513,7 @@ class QtBaseModule(ExtensionModule): lrelease_kwargs = {'output': '@BASENAME@.qm', 'input': ts, 'install': kwargs.get('install', False), + 'install_tag': 'i18n', 'build_by_default': kwargs.get('build_by_default', False), 'command': cmd} if install_dir is not None: diff --git a/test cases/unit/98 install tag/bar-custom.txt b/test cases/unit/98 install tag/bar-custom.txt new file mode 100644 index 000000000..e69de29bb diff --git a/test cases/unit/98 install tag/bar-devel.h b/test cases/unit/98 install tag/bar-devel.h new file mode 100644 index 000000000..e69de29bb diff --git a/test cases/unit/98 install tag/bar-notag.txt b/test cases/unit/98 install tag/bar-notag.txt new file mode 100644 index 000000000..e69de29bb diff --git a/test cases/unit/98 install tag/foo.in b/test cases/unit/98 install tag/foo.in new file mode 100644 index 000000000..e69de29bb diff --git a/test cases/unit/98 install tag/foo1-devel.h b/test cases/unit/98 install tag/foo1-devel.h new file mode 100644 index 000000000..e69de29bb diff --git a/test cases/unit/98 install tag/lib.c b/test cases/unit/98 install tag/lib.c new file mode 100644 index 000000000..2ea9c7dd1 --- /dev/null +++ b/test cases/unit/98 install tag/lib.c @@ -0,0 +1,9 @@ +#if defined _WIN32 || defined __CYGWIN__ +#define DLL_PUBLIC __declspec(dllexport) +#else +#define DLL_PUBLIC +#endif + +int DLL_PUBLIC foo(void) { + return 0; +} diff --git a/test cases/unit/98 install tag/main.c b/test cases/unit/98 install tag/main.c new file mode 100644 index 000000000..0fb4389f7 --- /dev/null +++ b/test cases/unit/98 install tag/main.c @@ -0,0 +1,3 @@ +int main(int argc, char *argv[]) { + return 0; +} diff --git a/test cases/unit/98 install tag/meson.build b/test cases/unit/98 install tag/meson.build new file mode 100644 index 000000000..ad1692ae1 --- /dev/null +++ b/test cases/unit/98 install tag/meson.build @@ -0,0 +1,73 @@ +project('install tag', 'c') + +# Those files should not be tagged +configure_file(input: 'foo.in', output: 'foo-notag.h', + configuration: {'foo': 'bar'}, + install_dir: get_option('datadir'), + install: true, +) +install_data('bar-notag.txt', + install_dir: get_option('datadir') +) +custom_target('ct1', + output: ['out1-notag.txt', 'out2-notag.txt'], + command: ['script.py', '@OUTPUT@'], + install_dir: get_option('datadir'), + install: true, +) + +# Those files should be tagged as devel +install_headers('foo1-devel.h') +install_data('bar-devel.h', + install_dir: get_option('includedir'), +) +configure_file(input: 'foo.in', output: 'foo2-devel.h', + configuration: {'foo': 'bar'}, + install_dir: get_option('includedir'), + install: true, +) +static_library('static', 'lib.c', + install: true, +) + +# Those files should have 'runtime' tag +executable('app', 'main.c', + install: true, +) +shared_library('shared', 'lib.c', + install: true, +) +both_libraries('both', 'lib.c', + install: true, +) + +# Those files should have custom tag +install_data('bar-custom.txt', + install_dir: get_option('datadir'), + install_tag: 'custom') +configure_file(input: 'foo.in', output: 'foo-custom.h', + configuration: {'foo': 'bar'}, + install_dir: get_option('datadir'), + install_tag: 'custom', + install: true, +) +both_libraries('bothcustom', 'lib.c', + install_tag: 'custom', + install: true, +) +custom_target('ct2', + output: ['out1-custom.txt', 'out2-custom.txt'], + command: ['script.py', '@OUTPUT@'], + install_dir: get_option('datadir'), + install_tag: 'custom', + install: true, +) + +# First is custom, 2nd is devel, 3rd has no tag +custom_target('ct3', + output: ['out3-custom.txt', 'out-devel.h', 'out3-notag.txt'], + command: ['script.py', '@OUTPUT@'], + install_dir: [get_option('datadir'), get_option('includedir'), get_option('datadir')], + install_tag: ['custom', 'devel', false], + install: true, +) diff --git a/test cases/unit/98 install tag/script.py b/test cases/unit/98 install tag/script.py new file mode 100644 index 000000000..c5f3be9ed --- /dev/null +++ b/test cases/unit/98 install tag/script.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python3 + +import sys + +for f in sys.argv[1:]: + with open(f, 'w') as f: + pass diff --git a/unittests/allplatformstests.py b/unittests/allplatformstests.py index a97ba63a7..32e084d19 100644 --- a/unittests/allplatformstests.py +++ b/unittests/allplatformstests.py @@ -476,6 +476,13 @@ class AllPlatformTests(BasePlatformTests): self.assertPathListEqual(intro[2]['install_filename'], ['/usr/include/first.h', None]) self.assertPathListEqual(intro[3]['install_filename'], [None, '/usr/bin/second.sh']) + def read_install_logs(self): + # Find logged files and directories + with Path(self.builddir, 'meson-logs', 'install-log.txt').open(encoding='utf-8') as f: + return list(map(lambda l: Path(l.strip()), + filter(lambda l: not l.startswith('#'), + f.readlines()))) + def test_install_log_content(self): ''' Tests that the install-log.txt is consistent with the installed files and directories. @@ -490,13 +497,7 @@ class AllPlatformTests(BasePlatformTests): expected = {installpath: 0} for name in installpath.rglob('*'): expected[name] = 0 - def read_logs(): - # Find logged files and directories - with Path(self.builddir, 'meson-logs', 'install-log.txt').open(encoding='utf-8') as f: - return list(map(lambda l: Path(l.strip()), - filter(lambda l: not l.startswith('#'), - f.readlines()))) - logged = read_logs() + logged = self.read_install_logs() for name in logged: self.assertTrue(name in expected, f'Log contains extra entry {name}') expected[name] += 1 @@ -509,14 +510,14 @@ class AllPlatformTests(BasePlatformTests): # actually installed windows_proof_rmtree(self.installdir) self._run(self.meson_command + ['install', '--dry-run', '--destdir', self.installdir], workdir=self.builddir) - self.assertEqual(logged, read_logs()) + self.assertEqual(logged, self.read_install_logs()) self.assertFalse(os.path.exists(self.installdir)) # If destdir is relative to build directory it should install # exactly the same files. rel_installpath = os.path.relpath(self.installdir, self.builddir) self._run(self.meson_command + ['install', '--dry-run', '--destdir', rel_installpath, '-C', self.builddir]) - self.assertEqual(logged, read_logs()) + self.assertEqual(logged, self.read_install_logs()) def test_uninstall(self): exename = os.path.join(self.installdir, 'usr/bin/prog' + exe_suffix) @@ -3756,3 +3757,109 @@ class AllPlatformTests(BasePlatformTests): cc = detect_compiler_for(env, 'c', MachineChoice.HOST) link_args = env.coredata.get_external_link_args(cc.for_machine, cc.language) self.assertEqual(sorted(link_args), sorted(['-flto'])) + + def test_install_tag(self) -> None: + testdir = os.path.join(self.unit_test_dir, '98 install tag') + self.init(testdir) + self.build() + + env = get_fake_env(testdir, self.builddir, self.prefix) + cc = detect_c_compiler(env, MachineChoice.HOST) + + def shared_lib_name(name): + if cc.get_id() in {'msvc', 'clang-cl'}: + return f'bin/{name}.dll' + elif is_windows(): + return f'bin/lib{name}.dll' + elif is_cygwin(): + return f'bin/cyg{name}.dll' + elif is_osx(): + return f'lib/lib{name}.dylib' + return f'lib/lib{name}.so' + + def exe_name(name): + if is_windows() or is_cygwin(): + return f'{name}.exe' + return name + + installpath = Path(self.installdir) + + expected_common = { + installpath, + Path(installpath, 'usr'), + } + + expected_devel = expected_common | { + Path(installpath, 'usr/include'), + Path(installpath, 'usr/include/foo1-devel.h'), + Path(installpath, 'usr/include/bar-devel.h'), + Path(installpath, 'usr/include/foo2-devel.h'), + Path(installpath, 'usr/include/out-devel.h'), + Path(installpath, 'usr/lib'), + Path(installpath, 'usr/lib/libstatic.a'), + Path(installpath, 'usr/lib/libboth.a'), + } + + if cc.get_id() in {'msvc', 'clang-cl'}: + expected_devel |= { + Path(installpath, 'usr/bin'), + Path(installpath, 'usr/bin/app.pdb'), + Path(installpath, 'usr/bin/both.pdb'), + Path(installpath, 'usr/bin/bothcustom.pdb'), + Path(installpath, 'usr/bin/shared.pdb'), + Path(installpath, 'usr/lib/both.lib'), + Path(installpath, 'usr/lib/bothcustom.lib'), + Path(installpath, 'usr/lib/shared.lib'), + } + elif is_windows() or is_cygwin(): + expected_devel |= { + Path(installpath, 'usr/lib/libboth.dll.a'), + Path(installpath, 'usr/lib/libshared.dll.a'), + Path(installpath, 'usr/lib/libbothcustom.dll.a'), + } + + expected_runtime = expected_common | { + Path(installpath, 'usr/bin'), + Path(installpath, 'usr/bin/' + exe_name('app')), + Path(installpath, 'usr/' + shared_lib_name('shared')), + Path(installpath, 'usr/' + shared_lib_name('both')), + } + + expected_custom = expected_common | { + Path(installpath, 'usr/share'), + Path(installpath, 'usr/share/bar-custom.txt'), + Path(installpath, 'usr/share/foo-custom.h'), + Path(installpath, 'usr/share/out1-custom.txt'), + Path(installpath, 'usr/share/out2-custom.txt'), + Path(installpath, 'usr/share/out3-custom.txt'), + Path(installpath, 'usr/lib'), + Path(installpath, 'usr/lib/libbothcustom.a'), + Path(installpath, 'usr/' + shared_lib_name('bothcustom')), + } + + if is_windows() or is_cygwin(): + expected_custom |= {Path(installpath, 'usr/bin')} + else: + expected_runtime |= {Path(installpath, 'usr/lib')} + + expected_runtime_custom = expected_runtime | expected_custom + + expected_all = expected_devel | expected_runtime | expected_custom | { + Path(installpath, 'usr/share/foo-notag.h'), + Path(installpath, 'usr/share/bar-notag.txt'), + Path(installpath, 'usr/share/out1-notag.txt'), + Path(installpath, 'usr/share/out2-notag.txt'), + Path(installpath, 'usr/share/out3-notag.txt'), + } + + def do_install(tags=None): + extra_args = ['--tags', tags] if tags else [] + self._run(self.meson_command + ['install', '--dry-run', '--destdir', self.installdir] + extra_args, workdir=self.builddir) + installed = self.read_install_logs() + return sorted(installed) + + self.assertEqual(sorted(expected_devel), do_install('devel')) + self.assertEqual(sorted(expected_runtime), do_install('runtime')) + self.assertEqual(sorted(expected_custom), do_install('custom')) + self.assertEqual(sorted(expected_runtime_custom), do_install('runtime,custom')) + self.assertEqual(sorted(expected_all), do_install())