Add install tags

Fixes: #7007.
pull/9132/head
Xavier Claessens 3 years ago committed by Xavier Claessens
parent 6d055b1e27
commit 8c5aa031b5
  1. 76
      docs/markdown/Installing.md
  2. 3
      docs/markdown/Python-module.md
  3. 25
      docs/markdown/Reference-manual.md
  4. 6
      docs/markdown/snippets/install_tag.md
  5. 80
      mesonbuild/backend/backends.py
  6. 2
      mesonbuild/backend/ninjabackend.py
  7. 20
      mesonbuild/build.py
  8. 24
      mesonbuild/interpreter/interpreter.py
  9. 15
      mesonbuild/interpreter/mesonmain.py
  10. 24
      mesonbuild/minstall.py
  11. 4
      mesonbuild/modules/gnome.py
  12. 1
      mesonbuild/modules/hotdoc.py
  13. 1
      mesonbuild/modules/i18n.py
  14. 2
      mesonbuild/modules/pkgconfig.py
  15. 8
      mesonbuild/modules/python.py
  16. 1
      mesonbuild/modules/qt.py
  17. 0
      test cases/unit/98 install tag/bar-custom.txt
  18. 0
      test cases/unit/98 install tag/bar-devel.h
  19. 0
      test cases/unit/98 install tag/bar-notag.txt
  20. 0
      test cases/unit/98 install tag/foo.in
  21. 0
      test cases/unit/98 install tag/foo1-devel.h
  22. 9
      test cases/unit/98 install tag/lib.c
  23. 3
      test cases/unit/98 install tag/main.c
  24. 73
      test cases/unit/98 install tag/meson.build
  25. 7
      test cases/unit/98 install tag/script.py
  26. 125
      unittests/allplatformstests.py

@ -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 `<builddir>/meson-logs/meson-log.txt`,
it is recommended to add missing `install_tag` to have a tag on each installable
files.

@ -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

@ -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,7 +391,7 @@ following.
To only install some outputs, pass `false` for the outputs that you
don't want installed. For example:
```
```meson
custom_target('only-install-second',
output : ['first.file', 'second.file'],
install : true,
@ -397,6 +400,12 @@ following.
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.

@ -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.

@ -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:

@ -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')

@ -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:

@ -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):

@ -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())

@ -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

@ -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):

@ -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)

@ -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)

@ -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')

@ -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)

@ -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:

@ -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;
}

@ -0,0 +1,3 @@
int main(int argc, char *argv[]) {
return 0;
}

@ -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,
)

@ -0,0 +1,7 @@
#!/usr/bin/env python3
import sys
for f in sys.argv[1:]:
with open(f, 'w') as f:
pass

@ -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())

Loading…
Cancel
Save