From 0af126fec798d6dbb0d1ad52168cc1f3f1758acd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arsen=20Arsenovi=C4=87?= Date: Tue, 12 Jul 2022 15:26:22 +0200 Subject: [PATCH] install_{data,headers,subdir}: implement follow_symlinks This permits users who rely on following symlinks to stay on the old default of following them. --- .../snippets/install_follow_symlink_arg.md | 7 ++++ docs/yaml/functions/install_data.yaml | 8 ++++ docs/yaml/functions/install_headers.yaml | 8 ++++ docs/yaml/functions/install_subdir.yaml | 8 ++++ mesonbuild/backend/backends.py | 14 ++++--- mesonbuild/build.py | 3 ++ mesonbuild/interpreter/interpreter.py | 20 +++++++--- mesonbuild/interpreter/kwargs.py | 3 ++ mesonbuild/interpreter/type_checking.py | 6 +++ mesonbuild/minstall.py | 31 ++++++++------- .../foo/file1 | 1 + .../foo/link1 | 1 + .../foo/link2.h | 1 + .../meson.build | 38 +++++++++++++++++++ .../test.json | 14 +++++++ 15 files changed, 139 insertions(+), 24 deletions(-) create mode 100644 docs/markdown/snippets/install_follow_symlink_arg.md create mode 100644 test cases/common/266 install functions and follow_symlinks/foo/file1 create mode 120000 test cases/common/266 install functions and follow_symlinks/foo/link1 create mode 120000 test cases/common/266 install functions and follow_symlinks/foo/link2.h create mode 100644 test cases/common/266 install functions and follow_symlinks/meson.build create mode 100644 test cases/common/266 install functions and follow_symlinks/test.json diff --git a/docs/markdown/snippets/install_follow_symlink_arg.md b/docs/markdown/snippets/install_follow_symlink_arg.md new file mode 100644 index 000000000000..ce971d7ff722 --- /dev/null +++ b/docs/markdown/snippets/install_follow_symlink_arg.md @@ -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. diff --git a/docs/yaml/functions/install_data.yaml b/docs/yaml/functions/install_data.yaml index 5ecc318a9bbf..ff4f3363d6ba 100644 --- a/docs/yaml/functions/install_data.yaml +++ b/docs/yaml/functions/install_data.yaml @@ -69,3 +69,11 @@ kwargs: sources: type: list[file | str] 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. diff --git a/docs/yaml/functions/install_headers.yaml b/docs/yaml/functions/install_headers.yaml index 958ab156e4b2..0ac4fc5860d7 100644 --- a/docs/yaml/functions/install_headers.yaml +++ b/docs/yaml/functions/install_headers.yaml @@ -73,3 +73,11 @@ kwargs: Disable stripping child-directories from header files when installing. 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. diff --git a/docs/yaml/functions/install_subdir.yaml b/docs/yaml/functions/install_subdir.yaml index 1907cec40bb3..19abee37d63b 100644 --- a/docs/yaml/functions/install_subdir.yaml +++ b/docs/yaml/functions/install_subdir.yaml @@ -106,3 +106,11 @@ kwargs: description: | Install directory contents. 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. diff --git a/mesonbuild/backend/backends.py b/mesonbuild/backend/backends.py index 62cf16296a0f..1d2283f30000 100644 --- a/mesonbuild/backend/backends.py +++ b/mesonbuild/backend/backends.py @@ -172,6 +172,7 @@ class InstallDataBase: subproject: str tag: T.Optional[str] = None data_type: T.Optional[str] = None + follow_symlinks: T.Optional[bool] = None @dataclass(eq=False) class InstallSymlinkData: @@ -186,8 +187,9 @@ class InstallSymlinkData: class SubdirInstallData(InstallDataBase): def __init__(self, path: str, install_path: str, install_path_name: 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): - super().__init__(path, install_path, install_path_name, install_mode, subproject, tag, data_type) + subproject: str, tag: T.Optional[str] = None, data_type: T.Optional[str] = None, + 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 @@ -1832,7 +1834,7 @@ def generate_header_install(self, d: InstallData) -> None: 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, 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) def generate_man_install(self, d: InstallData) -> None: @@ -1877,7 +1879,8 @@ def generate_data_install(self, d: InstallData) -> None: dstdir_name = os.path.join(subdir_name, dst_name) tag = de.install_tag or self.guess_install_tag(dst_abs) 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) def generate_symlink_install(self, d: InstallData) -> None: @@ -1908,7 +1911,8 @@ def generate_subdir_install(self, d: InstallData) -> None: dst_dir = os.path.join(dst_dir, 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')) - 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) def get_introspection_data(self, target_id: str, target: build.Target) -> T.List['TargetIntrospectionData']: diff --git a/mesonbuild/build.py b/mesonbuild/build.py index 09437037a89e..fece0bebc8e2 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -156,6 +156,7 @@ class Headers(HoldableObject): custom_install_dir: T.Optional[str] custom_install_mode: 'FileMode' subproject: str + follow_symlinks: T.Optional[bool] = None # TODO: we really don't need any of these methods, but they're preserved to # keep APIs relying on them working. @@ -214,6 +215,7 @@ class InstallDir(HoldableObject): subproject: str from_source_dir: bool = True install_tag: T.Optional[str] = None + follow_symlinks: T.Optional[bool] = None @dataclass(eq=False) class DepManifest: @@ -2973,6 +2975,7 @@ class Data(HoldableObject): rename: T.List[str] = None install_tag: T.Optional[str] = None data_type: str = None + follow_symlinks: T.Optional[bool] = None def __post_init__(self) -> None: if self.rename is None: diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index 838ad2fdea54..6fcefd14d6c8 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -78,6 +78,7 @@ INSTALL_KW, INSTALL_DIR_KW, INSTALL_MODE_KW, + INSTALL_FOLLOW_SYMLINKS, LINK_WITH_KW, LINK_WHOLE_KW, CT_INSTALL_TAG_KW, @@ -2238,6 +2239,7 @@ def add_test(self, node: mparser.BaseNode, KwargInfo('subdir', (str, NoneType)), INSTALL_MODE_KW.evolve(since='0.47.0'), INSTALL_DIR_KW, + INSTALL_FOLLOW_SYMLINKS, ) def func_install_headers(self, node: mparser.BaseNode, args: T.Tuple[T.List['mesonlib.FileOrString']], @@ -2264,7 +2266,8 @@ def func_install_headers(self, node: mparser.BaseNode, for childdir in dirs: 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) self.build.headers.append(h) @@ -2459,6 +2462,7 @@ def _warn_kwarg_install_mode_sticky(self, mode: FileMode) -> None: INSTALL_TAG_KW.evolve(since='0.60.0'), INSTALL_DIR_KW, PRESERVE_PATH_KW.evolve(since='0.64.0'), + INSTALL_FOLLOW_SYMLINKS, ) def func_install_data(self, node: mparser.BaseNode, args: T.Tuple[T.List['mesonlib.FileOrString']], @@ -2486,15 +2490,16 @@ def func_install_data(self, node: mparser.BaseNode, 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'], - 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, install_mode: FileMode, rename: T.Optional[str], tag: T.Optional[str], 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 - dirs = collections.defaultdict(list) if preserve_path: for file in sources: @@ -2506,7 +2511,8 @@ def install_data_impl(self, sources: T.List[mesonlib.File], install_dir: str, ret_data = [] for childdir, files in dirs.items(): 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) self.build.data.extend(ret_data) @@ -2525,6 +2531,7 @@ def install_data_impl(self, sources: T.List[mesonlib.File], install_dir: str, 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_TAG_KW.evolve(since='0.60.0'), + INSTALL_FOLLOW_SYMLINKS, ) def func_install_subdir(self, node: mparser.BaseNode, args: T.Tuple[str], kwargs: 'kwtypes.FuncInstallSubdir') -> build.InstallDir: @@ -2550,7 +2557,8 @@ def func_install_subdir(self, node: mparser.BaseNode, args: T.Tuple[str], exclude, kwargs['strip_directory'], self.subproject, - install_tag=kwargs['install_tag']) + install_tag=kwargs['install_tag'], + follow_symlinks=kwargs['follow_symlinks']) self.build.install_dirs.append(idir) return idir diff --git a/mesonbuild/interpreter/kwargs.py b/mesonbuild/interpreter/kwargs.py index 1aee41435d38..e67ebf0c0e4b 100644 --- a/mesonbuild/interpreter/kwargs.py +++ b/mesonbuild/interpreter/kwargs.py @@ -124,6 +124,7 @@ class FuncInstallSubdir(TypedDict): exclude_files: T.List[str] exclude_directories: T.List[str] install_mode: FileMode + follow_symlinks: T.Optional[bool] class FuncInstallData(TypedDict): @@ -132,6 +133,7 @@ class FuncInstallData(TypedDict): sources: T.List[FileOrString] rename: T.List[str] install_mode: FileMode + follow_symlinks: T.Optional[bool] class FuncInstallHeaders(TypedDict): @@ -139,6 +141,7 @@ class FuncInstallHeaders(TypedDict): install_dir: T.Optional[str] install_mode: FileMode subdir: T.Optional[str] + follow_symlinks: T.Optional[bool] class FuncInstallMan(TypedDict): diff --git a/mesonbuild/interpreter/type_checking.py b/mesonbuild/interpreter/type_checking.py index 28c9152955fa..047aff867392 100644 --- a/mesonbuild/interpreter/type_checking.py +++ b/mesonbuild/interpreter/type_checking.py @@ -368,6 +368,12 @@ def _output_validator(outputs: T.List[str]) -> T.Optional[str]: 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) CT_INSTALL_DIR_KW: KwargInfo[T.List[T.Union[str, Literal[False]]]] = KwargInfo( diff --git a/mesonbuild/minstall.py b/mesonbuild/minstall.py index 0d397b23e306..5f8629b7fe47 100644 --- a/mesonbuild/minstall.py +++ b/mesonbuild/minstall.py @@ -64,9 +64,11 @@ class ArgumentType(Protocol): strip: bool -symlink_warning = '''Warning: trying to copy a symlink that points to a file. This will copy the file, -but this will be changed in a future version of Meson to copy the symlink as is. Please update your -build definitions so that it will not break when the change happens.''' +symlink_warning = '''\ +Warning: trying to copy a symlink that points to a file. This currently copies +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] = [] @@ -389,7 +391,8 @@ def should_preserve_existing_file(self, from_file: str, to_file: str) -> bool: return from_time <= to_time 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] 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}') @@ -417,10 +420,10 @@ def do_copyfile(self, from_file: str, to_file: str, # Dangling symlink. Replicate as is. self.copy(from_file, outdir, follow_symlinks=False) else: - # Remove this entire branch when changing the behaviour to duplicate - # symlinks rather than copying what they point to. - print(symlink_warning) - self.copy2(from_file, to_file) + if follow_symlinks is None: + follow_symlinks = True # TODO: change to False when removing the warning + print(symlink_warning) + self.copy2(from_file, to_file, follow_symlinks=follow_symlinks) else: self.copy2(from_file, to_file) selinux_updates.append(to_file) @@ -454,7 +457,7 @@ def do_symlink(self, target: str, link: str, destdir: str, full_dst_dir: str, al def do_copydir(self, data: InstallData, src_dir: str, dst_dir: 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. @@ -519,7 +522,7 @@ def do_copydir(self, data: InstallData, src_dir: str, dst_dir: str, dm.makedirs(parent_dir) self.copystat(os.path.dirname(abs_src), parent_dir) # 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) def do_install(self, datafilename: str) -> None: @@ -613,7 +616,8 @@ def install_subdirs(self, d: InstallData, dm: DirMaker, destdir: str, fullprefix full_dst_dir = get_destdir_path(destdir, fullprefix, i.install_path) self.log(f'Installing subdir {i.path} to {full_dst_dir}') 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: for i in d.data: @@ -622,7 +626,7 @@ def install_data(self, d: InstallData, dm: DirMaker, destdir: str, fullprefix: s fullfilename = i.path outfilename = get_destdir_path(destdir, fullprefix, i.install_path) 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.set_mode(outfilename, i.install_mode, d.install_umask) @@ -668,7 +672,8 @@ def install_headers(self, d: InstallData, dm: DirMaker, destdir: str, fullprefix fname = os.path.basename(fullfilename) outdir = get_destdir_path(destdir, fullprefix, t.install_path) 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.set_mode(outfilename, t.install_mode, d.install_umask) diff --git a/test cases/common/266 install functions and follow_symlinks/foo/file1 b/test cases/common/266 install functions and follow_symlinks/foo/file1 new file mode 100644 index 000000000000..9daeafb9864c --- /dev/null +++ b/test cases/common/266 install functions and follow_symlinks/foo/file1 @@ -0,0 +1 @@ +test diff --git a/test cases/common/266 install functions and follow_symlinks/foo/link1 b/test cases/common/266 install functions and follow_symlinks/foo/link1 new file mode 120000 index 000000000000..08219db9b096 --- /dev/null +++ b/test cases/common/266 install functions and follow_symlinks/foo/link1 @@ -0,0 +1 @@ +file1 \ No newline at end of file diff --git a/test cases/common/266 install functions and follow_symlinks/foo/link2.h b/test cases/common/266 install functions and follow_symlinks/foo/link2.h new file mode 120000 index 000000000000..08219db9b096 --- /dev/null +++ b/test cases/common/266 install functions and follow_symlinks/foo/link2.h @@ -0,0 +1 @@ +file1 \ No newline at end of file diff --git a/test cases/common/266 install functions and follow_symlinks/meson.build b/test cases/common/266 install functions and follow_symlinks/meson.build new file mode 100644 index 000000000000..327c02131fb3 --- /dev/null +++ b/test cases/common/266 install functions and follow_symlinks/meson.build @@ -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, +) diff --git a/test cases/common/266 install functions and follow_symlinks/test.json b/test cases/common/266 install functions and follow_symlinks/test.json new file mode 100644 index 000000000000..6a395177d28c --- /dev/null +++ b/test cases/common/266 install functions and follow_symlinks/test.json @@ -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"} + ] +}