install_{data,headers,subdir}: implement follow_symlinks

This permits users who rely on following symlinks to stay on the old
default of following them.
pull/12260/head
Arsen Arsenović 3 years ago committed by Eli Schwartz
parent 56ef698426
commit 0af126fec7
  1. 7
      docs/markdown/snippets/install_follow_symlink_arg.md
  2. 8
      docs/yaml/functions/install_data.yaml
  3. 8
      docs/yaml/functions/install_headers.yaml
  4. 8
      docs/yaml/functions/install_subdir.yaml
  5. 14
      mesonbuild/backend/backends.py
  6. 3
      mesonbuild/build.py
  7. 20
      mesonbuild/interpreter/interpreter.py
  8. 3
      mesonbuild/interpreter/kwargs.py
  9. 6
      mesonbuild/interpreter/type_checking.py
  10. 31
      mesonbuild/minstall.py
  11. 1
      test cases/common/266 install functions and follow_symlinks/foo/file1
  12. 1
      test cases/common/266 install functions and follow_symlinks/foo/link1
  13. 1
      test cases/common/266 install functions and follow_symlinks/foo/link2.h
  14. 38
      test cases/common/266 install functions and follow_symlinks/meson.build
  15. 14
      test cases/common/266 install functions and follow_symlinks/test.json

@ -0,0 +1,7 @@
## Added follow_symlinks arg to install_data, install_header, and install_subdir
The [[install_data]], [[install_headers]], [[install_subdir]] functions now
have an optional argument `follow_symlinks` that, if set to `true`, makes it so
symbolic links in the source are followed, rather than copied into the
destination tree, to match the old behavior. The default, which is currently
to follow links, is subject to change in the future.

@ -69,3 +69,11 @@ kwargs:
sources: sources:
type: list[file | str] type: list[file | str]
description: Additional files to install. description: Additional files to install.
follow_symlinks:
type: bool
since: 1.3.0
default: true
description: |
If true, dereferences links and copies their target instead. The default
value will become false in the future.

@ -73,3 +73,11 @@ kwargs:
Disable stripping child-directories from header files when installing. Disable stripping child-directories from header files when installing.
This is equivalent to GNU Automake's `nobase` option. This is equivalent to GNU Automake's `nobase` option.
follow_symlinks:
type: bool
since: 1.3.0
default: true
description: |
If true, dereferences links and copies their target instead. The default
value will become false in the future.

@ -106,3 +106,11 @@ kwargs:
description: | description: |
Install directory contents. Install directory contents.
If `strip_directory=true` only the last component of the source path is used. If `strip_directory=true` only the last component of the source path is used.
follow_symlinks:
type: bool
since: 1.3.0
default: true
description: |
If true, dereferences links and copies their target instead. The default
value will become false in the future.

@ -172,6 +172,7 @@ class InstallDataBase:
subproject: str subproject: str
tag: T.Optional[str] = None tag: T.Optional[str] = None
data_type: T.Optional[str] = None data_type: T.Optional[str] = None
follow_symlinks: T.Optional[bool] = None
@dataclass(eq=False) @dataclass(eq=False)
class InstallSymlinkData: class InstallSymlinkData:
@ -186,8 +187,9 @@ class InstallSymlinkData:
class SubdirInstallData(InstallDataBase): class SubdirInstallData(InstallDataBase):
def __init__(self, path: str, install_path: str, install_path_name: str, def __init__(self, path: str, install_path: str, install_path_name: str,
install_mode: 'FileMode', exclude: T.Tuple[T.Set[str], T.Set[str]], install_mode: 'FileMode', exclude: T.Tuple[T.Set[str], T.Set[str]],
subproject: str, tag: T.Optional[str] = None, data_type: T.Optional[str] = None): subproject: str, tag: T.Optional[str] = None, data_type: T.Optional[str] = None,
super().__init__(path, install_path, install_path_name, install_mode, subproject, tag, data_type) follow_symlinks: T.Optional[bool] = None):
super().__init__(path, install_path, install_path_name, install_mode, subproject, tag, data_type, follow_symlinks)
self.exclude = exclude self.exclude = exclude
@ -1832,7 +1834,7 @@ class Backend:
if not isinstance(f, File): if not isinstance(f, File):
raise MesonException(f'Invalid header type {f!r} can\'t be installed') raise MesonException(f'Invalid header type {f!r} can\'t be installed')
abspath = f.absolute_path(srcdir, builddir) abspath = f.absolute_path(srcdir, builddir)
i = InstallDataBase(abspath, outdir, outdir_name, h.get_custom_install_mode(), h.subproject, tag='devel') i = InstallDataBase(abspath, outdir, outdir_name, h.get_custom_install_mode(), h.subproject, tag='devel', follow_symlinks=h.follow_symlinks)
d.headers.append(i) d.headers.append(i)
def generate_man_install(self, d: InstallData) -> None: def generate_man_install(self, d: InstallData) -> None:
@ -1877,7 +1879,8 @@ class Backend:
dstdir_name = os.path.join(subdir_name, dst_name) dstdir_name = os.path.join(subdir_name, dst_name)
tag = de.install_tag or self.guess_install_tag(dst_abs) tag = de.install_tag or self.guess_install_tag(dst_abs)
i = InstallDataBase(src_file.absolute_path(srcdir, builddir), dst_abs, dstdir_name, i = InstallDataBase(src_file.absolute_path(srcdir, builddir), dst_abs, dstdir_name,
de.install_mode, de.subproject, tag=tag, data_type=de.data_type) de.install_mode, de.subproject, tag=tag, data_type=de.data_type,
follow_symlinks=de.follow_symlinks)
d.data.append(i) d.data.append(i)
def generate_symlink_install(self, d: InstallData) -> None: def generate_symlink_install(self, d: InstallData) -> None:
@ -1908,7 +1911,8 @@ class Backend:
dst_dir = os.path.join(dst_dir, os.path.basename(src_dir)) dst_dir = os.path.join(dst_dir, os.path.basename(src_dir))
dst_name = os.path.join(dst_name, os.path.basename(src_dir)) dst_name = os.path.join(dst_name, os.path.basename(src_dir))
tag = sd.install_tag or self.guess_install_tag(os.path.join(sd.install_dir, 'dummy')) tag = sd.install_tag or self.guess_install_tag(os.path.join(sd.install_dir, 'dummy'))
i = SubdirInstallData(src_dir, dst_dir, dst_name, sd.install_mode, sd.exclude, sd.subproject, tag) i = SubdirInstallData(src_dir, dst_dir, dst_name, sd.install_mode, sd.exclude, sd.subproject, tag,
follow_symlinks=sd.follow_symlinks)
d.install_subdirs.append(i) d.install_subdirs.append(i)
def get_introspection_data(self, target_id: str, target: build.Target) -> T.List['TargetIntrospectionData']: def get_introspection_data(self, target_id: str, target: build.Target) -> T.List['TargetIntrospectionData']:

@ -156,6 +156,7 @@ class Headers(HoldableObject):
custom_install_dir: T.Optional[str] custom_install_dir: T.Optional[str]
custom_install_mode: 'FileMode' custom_install_mode: 'FileMode'
subproject: str subproject: str
follow_symlinks: T.Optional[bool] = None
# TODO: we really don't need any of these methods, but they're preserved to # TODO: we really don't need any of these methods, but they're preserved to
# keep APIs relying on them working. # keep APIs relying on them working.
@ -214,6 +215,7 @@ class InstallDir(HoldableObject):
subproject: str subproject: str
from_source_dir: bool = True from_source_dir: bool = True
install_tag: T.Optional[str] = None install_tag: T.Optional[str] = None
follow_symlinks: T.Optional[bool] = None
@dataclass(eq=False) @dataclass(eq=False)
class DepManifest: class DepManifest:
@ -2973,6 +2975,7 @@ class Data(HoldableObject):
rename: T.List[str] = None rename: T.List[str] = None
install_tag: T.Optional[str] = None install_tag: T.Optional[str] = None
data_type: str = None data_type: str = None
follow_symlinks: T.Optional[bool] = None
def __post_init__(self) -> None: def __post_init__(self) -> None:
if self.rename is None: if self.rename is None:

@ -78,6 +78,7 @@ from .type_checking import (
INSTALL_KW, INSTALL_KW,
INSTALL_DIR_KW, INSTALL_DIR_KW,
INSTALL_MODE_KW, INSTALL_MODE_KW,
INSTALL_FOLLOW_SYMLINKS,
LINK_WITH_KW, LINK_WITH_KW,
LINK_WHOLE_KW, LINK_WHOLE_KW,
CT_INSTALL_TAG_KW, CT_INSTALL_TAG_KW,
@ -2238,6 +2239,7 @@ class Interpreter(InterpreterBase, HoldableObject):
KwargInfo('subdir', (str, NoneType)), KwargInfo('subdir', (str, NoneType)),
INSTALL_MODE_KW.evolve(since='0.47.0'), INSTALL_MODE_KW.evolve(since='0.47.0'),
INSTALL_DIR_KW, INSTALL_DIR_KW,
INSTALL_FOLLOW_SYMLINKS,
) )
def func_install_headers(self, node: mparser.BaseNode, def func_install_headers(self, node: mparser.BaseNode,
args: T.Tuple[T.List['mesonlib.FileOrString']], args: T.Tuple[T.List['mesonlib.FileOrString']],
@ -2264,7 +2266,8 @@ class Interpreter(InterpreterBase, HoldableObject):
for childdir in dirs: for childdir in dirs:
h = build.Headers(dirs[childdir], os.path.join(install_subdir, childdir), kwargs['install_dir'], h = build.Headers(dirs[childdir], os.path.join(install_subdir, childdir), kwargs['install_dir'],
install_mode, self.subproject) install_mode, self.subproject,
follow_symlinks=kwargs['follow_symlinks'])
ret_headers.append(h) ret_headers.append(h)
self.build.headers.append(h) self.build.headers.append(h)
@ -2459,6 +2462,7 @@ class Interpreter(InterpreterBase, HoldableObject):
INSTALL_TAG_KW.evolve(since='0.60.0'), INSTALL_TAG_KW.evolve(since='0.60.0'),
INSTALL_DIR_KW, INSTALL_DIR_KW,
PRESERVE_PATH_KW.evolve(since='0.64.0'), PRESERVE_PATH_KW.evolve(since='0.64.0'),
INSTALL_FOLLOW_SYMLINKS,
) )
def func_install_data(self, node: mparser.BaseNode, def func_install_data(self, node: mparser.BaseNode,
args: T.Tuple[T.List['mesonlib.FileOrString']], args: T.Tuple[T.List['mesonlib.FileOrString']],
@ -2486,15 +2490,16 @@ class Interpreter(InterpreterBase, HoldableObject):
install_mode = self._warn_kwarg_install_mode_sticky(kwargs['install_mode']) install_mode = self._warn_kwarg_install_mode_sticky(kwargs['install_mode'])
return self.install_data_impl(sources, install_dir, install_mode, rename, kwargs['install_tag'], return self.install_data_impl(sources, install_dir, install_mode, rename, kwargs['install_tag'],
preserve_path=kwargs['preserve_path']) preserve_path=kwargs['preserve_path'],
follow_symlinks=kwargs['follow_symlinks'])
def install_data_impl(self, sources: T.List[mesonlib.File], install_dir: str, def install_data_impl(self, sources: T.List[mesonlib.File], install_dir: str,
install_mode: FileMode, rename: T.Optional[str], install_mode: FileMode, rename: T.Optional[str],
tag: T.Optional[str], tag: T.Optional[str],
install_data_type: T.Optional[str] = None, install_data_type: T.Optional[str] = None,
preserve_path: bool = False) -> build.Data: preserve_path: bool = False,
follow_symlinks: T.Optional[bool] = None) -> build.Data:
install_dir_name = install_dir.optname if isinstance(install_dir, P_OBJ.OptionString) else install_dir install_dir_name = install_dir.optname if isinstance(install_dir, P_OBJ.OptionString) else install_dir
dirs = collections.defaultdict(list) dirs = collections.defaultdict(list)
if preserve_path: if preserve_path:
for file in sources: for file in sources:
@ -2506,7 +2511,8 @@ class Interpreter(InterpreterBase, HoldableObject):
ret_data = [] ret_data = []
for childdir, files in dirs.items(): for childdir, files in dirs.items():
d = build.Data(files, os.path.join(install_dir, childdir), os.path.join(install_dir_name, childdir), d = build.Data(files, os.path.join(install_dir, childdir), os.path.join(install_dir_name, childdir),
install_mode, self.subproject, rename, tag, install_data_type) install_mode, self.subproject, rename, tag, install_data_type,
follow_symlinks)
ret_data.append(d) ret_data.append(d)
self.build.data.extend(ret_data) self.build.data.extend(ret_data)
@ -2525,6 +2531,7 @@ class Interpreter(InterpreterBase, HoldableObject):
validator=lambda x: 'cannot be absolute' if any(os.path.isabs(d) for d in x) else None), validator=lambda x: 'cannot be absolute' if any(os.path.isabs(d) for d in x) else None),
INSTALL_MODE_KW.evolve(since='0.38.0'), INSTALL_MODE_KW.evolve(since='0.38.0'),
INSTALL_TAG_KW.evolve(since='0.60.0'), INSTALL_TAG_KW.evolve(since='0.60.0'),
INSTALL_FOLLOW_SYMLINKS,
) )
def func_install_subdir(self, node: mparser.BaseNode, args: T.Tuple[str], def func_install_subdir(self, node: mparser.BaseNode, args: T.Tuple[str],
kwargs: 'kwtypes.FuncInstallSubdir') -> build.InstallDir: kwargs: 'kwtypes.FuncInstallSubdir') -> build.InstallDir:
@ -2550,7 +2557,8 @@ class Interpreter(InterpreterBase, HoldableObject):
exclude, exclude,
kwargs['strip_directory'], kwargs['strip_directory'],
self.subproject, self.subproject,
install_tag=kwargs['install_tag']) install_tag=kwargs['install_tag'],
follow_symlinks=kwargs['follow_symlinks'])
self.build.install_dirs.append(idir) self.build.install_dirs.append(idir)
return idir return idir

@ -124,6 +124,7 @@ class FuncInstallSubdir(TypedDict):
exclude_files: T.List[str] exclude_files: T.List[str]
exclude_directories: T.List[str] exclude_directories: T.List[str]
install_mode: FileMode install_mode: FileMode
follow_symlinks: T.Optional[bool]
class FuncInstallData(TypedDict): class FuncInstallData(TypedDict):
@ -132,6 +133,7 @@ class FuncInstallData(TypedDict):
sources: T.List[FileOrString] sources: T.List[FileOrString]
rename: T.List[str] rename: T.List[str]
install_mode: FileMode install_mode: FileMode
follow_symlinks: T.Optional[bool]
class FuncInstallHeaders(TypedDict): class FuncInstallHeaders(TypedDict):
@ -139,6 +141,7 @@ class FuncInstallHeaders(TypedDict):
install_dir: T.Optional[str] install_dir: T.Optional[str]
install_mode: FileMode install_mode: FileMode
subdir: T.Optional[str] subdir: T.Optional[str]
follow_symlinks: T.Optional[bool]
class FuncInstallMan(TypedDict): class FuncInstallMan(TypedDict):

@ -368,6 +368,12 @@ CT_INSTALL_TAG_KW: KwargInfo[T.List[T.Union[str, bool]]] = KwargInfo(
INSTALL_TAG_KW: KwargInfo[T.Optional[str]] = KwargInfo('install_tag', (str, NoneType)) INSTALL_TAG_KW: KwargInfo[T.Optional[str]] = KwargInfo('install_tag', (str, NoneType))
INSTALL_FOLLOW_SYMLINKS: KwargInfo[T.Optional[bool]] = KwargInfo(
'follow_symlinks',
(bool, NoneType),
since='1.3.0',
)
INSTALL_KW = KwargInfo('install', bool, default=False) INSTALL_KW = KwargInfo('install', bool, default=False)
CT_INSTALL_DIR_KW: KwargInfo[T.List[T.Union[str, Literal[False]]]] = KwargInfo( CT_INSTALL_DIR_KW: KwargInfo[T.List[T.Union[str, Literal[False]]]] = KwargInfo(

@ -64,9 +64,11 @@ if T.TYPE_CHECKING:
strip: bool strip: bool
symlink_warning = '''Warning: trying to copy a symlink that points to a file. This will copy the file, symlink_warning = '''\
but this will be changed in a future version of Meson to copy the symlink as is. Please update your Warning: trying to copy a symlink that points to a file. This currently copies
build definitions so that it will not break when the change happens.''' the file by default, but will be changed in a future version of Meson to copy
the link instead. Set follow_symlinks to true to preserve current behavior, or
false to copy the link.'''
selinux_updates: T.List[str] = [] selinux_updates: T.List[str] = []
@ -389,7 +391,8 @@ class Installer:
return from_time <= to_time return from_time <= to_time
def do_copyfile(self, from_file: str, to_file: str, def do_copyfile(self, from_file: str, to_file: str,
makedirs: T.Optional[T.Tuple[T.Any, str]] = None) -> bool: makedirs: T.Optional[T.Tuple[T.Any, str]] = None,
follow_symlinks: T.Optional[bool] = None) -> bool:
outdir = os.path.split(to_file)[0] outdir = os.path.split(to_file)[0]
if not os.path.isfile(from_file) and not os.path.islink(from_file): if not os.path.isfile(from_file) and not os.path.islink(from_file):
raise MesonException(f'Tried to install something that isn\'t a file: {from_file!r}') raise MesonException(f'Tried to install something that isn\'t a file: {from_file!r}')
@ -417,10 +420,10 @@ class Installer:
# Dangling symlink. Replicate as is. # Dangling symlink. Replicate as is.
self.copy(from_file, outdir, follow_symlinks=False) self.copy(from_file, outdir, follow_symlinks=False)
else: else:
# Remove this entire branch when changing the behaviour to duplicate if follow_symlinks is None:
# symlinks rather than copying what they point to. follow_symlinks = True # TODO: change to False when removing the warning
print(symlink_warning) print(symlink_warning)
self.copy2(from_file, to_file) self.copy2(from_file, to_file, follow_symlinks=follow_symlinks)
else: else:
self.copy2(from_file, to_file) self.copy2(from_file, to_file)
selinux_updates.append(to_file) selinux_updates.append(to_file)
@ -454,7 +457,7 @@ class Installer:
def do_copydir(self, data: InstallData, src_dir: str, dst_dir: str, def do_copydir(self, data: InstallData, src_dir: str, dst_dir: str,
exclude: T.Optional[T.Tuple[T.Set[str], T.Set[str]]], exclude: T.Optional[T.Tuple[T.Set[str], T.Set[str]]],
install_mode: 'FileMode', dm: DirMaker) -> None: install_mode: 'FileMode', dm: DirMaker, follow_symlinks: T.Optional[bool] = None) -> None:
''' '''
Copies the contents of directory @src_dir into @dst_dir. Copies the contents of directory @src_dir into @dst_dir.
@ -519,7 +522,7 @@ class Installer:
dm.makedirs(parent_dir) dm.makedirs(parent_dir)
self.copystat(os.path.dirname(abs_src), parent_dir) self.copystat(os.path.dirname(abs_src), parent_dir)
# FIXME: what about symlinks? # FIXME: what about symlinks?
self.do_copyfile(abs_src, abs_dst) self.do_copyfile(abs_src, abs_dst, follow_symlinks=follow_symlinks)
self.set_mode(abs_dst, install_mode, data.install_umask) self.set_mode(abs_dst, install_mode, data.install_umask)
def do_install(self, datafilename: str) -> None: def do_install(self, datafilename: str) -> None:
@ -613,7 +616,8 @@ class Installer:
full_dst_dir = get_destdir_path(destdir, fullprefix, i.install_path) full_dst_dir = get_destdir_path(destdir, fullprefix, i.install_path)
self.log(f'Installing subdir {i.path} to {full_dst_dir}') self.log(f'Installing subdir {i.path} to {full_dst_dir}')
dm.makedirs(full_dst_dir, exist_ok=True) dm.makedirs(full_dst_dir, exist_ok=True)
self.do_copydir(d, i.path, full_dst_dir, i.exclude, i.install_mode, dm) self.do_copydir(d, i.path, full_dst_dir, i.exclude, i.install_mode, dm,
follow_symlinks=i.follow_symlinks)
def install_data(self, d: InstallData, dm: DirMaker, destdir: str, fullprefix: str) -> None: def install_data(self, d: InstallData, dm: DirMaker, destdir: str, fullprefix: str) -> None:
for i in d.data: for i in d.data:
@ -622,7 +626,7 @@ class Installer:
fullfilename = i.path fullfilename = i.path
outfilename = get_destdir_path(destdir, fullprefix, i.install_path) outfilename = get_destdir_path(destdir, fullprefix, i.install_path)
outdir = os.path.dirname(outfilename) outdir = os.path.dirname(outfilename)
if self.do_copyfile(fullfilename, outfilename, makedirs=(dm, outdir)): if self.do_copyfile(fullfilename, outfilename, makedirs=(dm, outdir), follow_symlinks=i.follow_symlinks):
self.did_install_something = True self.did_install_something = True
self.set_mode(outfilename, i.install_mode, d.install_umask) self.set_mode(outfilename, i.install_mode, d.install_umask)
@ -668,7 +672,8 @@ class Installer:
fname = os.path.basename(fullfilename) fname = os.path.basename(fullfilename)
outdir = get_destdir_path(destdir, fullprefix, t.install_path) outdir = get_destdir_path(destdir, fullprefix, t.install_path)
outfilename = os.path.join(outdir, fname) outfilename = os.path.join(outdir, fname)
if self.do_copyfile(fullfilename, outfilename, makedirs=(dm, outdir)): if self.do_copyfile(fullfilename, outfilename, makedirs=(dm, outdir),
follow_symlinks=t.follow_symlinks):
self.did_install_something = True self.did_install_something = True
self.set_mode(outfilename, t.install_mode, d.install_umask) self.set_mode(outfilename, t.install_mode, d.install_umask)

@ -0,0 +1,38 @@
project('install_data following symlinks')
install_data(
'foo/link1',
install_dir: get_option('datadir') / 'followed',
follow_symlinks: true,
)
install_headers(
'foo/link2.h',
follow_symlinks: true,
subdir: 'followed'
)
install_data(
'foo/link1',
install_dir: get_option('datadir'),
follow_symlinks: false,
)
install_headers(
'foo/link2.h',
follow_symlinks: false,
)
install_subdir(
'foo',
install_dir: get_option('datadir') / 'subdir',
strip_directory: true,
follow_symlinks: false,
)
install_subdir(
'foo',
install_dir: get_option('datadir') / 'subdir_followed',
strip_directory: true,
follow_symlinks: true,
)

@ -0,0 +1,14 @@
{
"installed": [
{"type": "link", "file": "usr/share/link1"},
{"type": "link", "file": "usr/include/link2.h"},
{"type": "file", "file": "usr/share/followed/link1"},
{"type": "file", "file": "usr/include/followed/link2.h"},
{"type": "link", "file": "usr/share/subdir/link1"},
{"type": "link", "file": "usr/share/subdir/link2.h"},
{"type": "file", "file": "usr/share/subdir/file1"},
{"type": "file", "file": "usr/share/subdir_followed/link1"},
{"type": "file", "file": "usr/share/subdir_followed/link2.h"},
{"type": "file", "file": "usr/share/subdir_followed/file1"}
]
}
Loading…
Cancel
Save