From 4f882ff8ec81cbc42b097d3aee8ca4a8013f538b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Correa=20G=C3=B3mez?= Date: Mon, 22 Nov 2021 15:46:15 +0100 Subject: [PATCH] add install_symlink function Allows installing symlinks directly from meson, which can become useful in multiple scenarios. Current main use is to help moving forward #9557 --- data/syntax-highlighting/vim/syntax/meson.vim | 1 + docs/markdown/snippets/install_symlink.md | 11 ++++ docs/yaml/functions/install_symlink.yaml | 34 ++++++++++ mesonbuild/ast/interpreter.py | 1 + mesonbuild/backend/backends.py | 20 ++++++ mesonbuild/build.py | 16 +++++ mesonbuild/interpreter/interpreter.py | 22 +++++++ mesonbuild/interpreter/interpreterobjects.py | 3 + mesonbuild/minstall.py | 62 ++++++++++++++----- .../common/247 install_symlink/datafile.dat | 1 + .../common/247 install_symlink/meson.build | 9 +++ .../common/247 install_symlink/test.json | 7 +++ .../meson.build | 3 + .../118 pathsep in install_symlink/test.json | 7 +++ 14 files changed, 180 insertions(+), 17 deletions(-) create mode 100644 docs/markdown/snippets/install_symlink.md create mode 100644 docs/yaml/functions/install_symlink.yaml create mode 100644 test cases/common/247 install_symlink/datafile.dat create mode 100644 test cases/common/247 install_symlink/meson.build create mode 100644 test cases/common/247 install_symlink/test.json create mode 100644 test cases/failing/118 pathsep in install_symlink/meson.build create mode 100644 test cases/failing/118 pathsep in install_symlink/test.json diff --git a/data/syntax-highlighting/vim/syntax/meson.vim b/data/syntax-highlighting/vim/syntax/meson.vim index 0af0d776f..15b47cd47 100644 --- a/data/syntax-highlighting/vim/syntax/meson.vim +++ b/data/syntax-highlighting/vim/syntax/meson.vim @@ -99,6 +99,7 @@ syn keyword mesonBuiltin \ install_headers \ install_man \ install_subdir + \ install_symlink \ install_emptydir \ is_disabler \ is_variable diff --git a/docs/markdown/snippets/install_symlink.md b/docs/markdown/snippets/install_symlink.md new file mode 100644 index 000000000..752c42244 --- /dev/null +++ b/docs/markdown/snippets/install_symlink.md @@ -0,0 +1,11 @@ +## install_symlink function + +It is now possible to request for symbolic links to be installed during +installation. The `install_symlink` function takes a positional argument to +the link name, and installs a symbolic link pointing to `pointing_to` target. +The link will be created under `install_dir` directory and cannot contain path +separators. + +```meson +install_symlink('target', pointing_to: '../bin/target', install_dir: '/usr/sbin') +``` diff --git a/docs/yaml/functions/install_symlink.yaml b/docs/yaml/functions/install_symlink.yaml new file mode 100644 index 000000000..d9f0de627 --- /dev/null +++ b/docs/yaml/functions/install_symlink.yaml @@ -0,0 +1,34 @@ +name: install_symlink +returns: void +since: 0.61.0 +description: | + Installs a symbolic link to `pointing_to` target under install_dir. + +posargs: + link_name: + type: str + description: | + Name of the created link under `install_dir`. + It cannot contain path separators. Those should go in `install_dir`. + +kwargs: + pointing_to: + type: str + required: true + description: | + Target to point the link to. + Can be absolute or relative and that will be respected when creating the link. + + install_dir: + type: str + required: true + description: | + The absolute or relative path to the installation directory for the links. + If this is a relative path, it is assumed to be relative to the prefix. + + install_tag: + type: str + description: | + A string used by the `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. diff --git a/mesonbuild/ast/interpreter.py b/mesonbuild/ast/interpreter.py index ec93ff552..f5a1e5e41 100644 --- a/mesonbuild/ast/interpreter.py +++ b/mesonbuild/ast/interpreter.py @@ -103,6 +103,7 @@ class AstInterpreter(InterpreterBase): 'install_man': self.func_do_nothing, 'install_data': self.func_do_nothing, 'install_subdir': self.func_do_nothing, + 'install_symlink': self.func_do_nothing, 'install_emptydir': self.func_do_nothing, 'configuration_data': self.func_do_nothing, 'configure_file': self.func_do_nothing, diff --git a/mesonbuild/backend/backends.py b/mesonbuild/backend/backends.py index c9cf6fd6f..fbb30659d 100644 --- a/mesonbuild/backend/backends.py +++ b/mesonbuild/backend/backends.py @@ -123,6 +123,7 @@ class InstallData: self.man: T.List[InstallDataBase] = [] self.emptydir: T.List[InstallEmptyDir] = [] self.data: T.List[InstallDataBase] = [] + self.symlinks: T.List[InstallSymlinkData] = [] self.install_scripts: T.List[ExecutableSerialisation] = [] self.install_subdirs: T.List[SubdirInstallData] = [] self.mesonintrospect = mesonintrospect @@ -168,6 +169,15 @@ class InstallDataBase: self.tag = tag self.data_type = data_type +class InstallSymlinkData: + def __init__(self, target: str, name: str, install_path: str, + subproject: str, tag: T.Optional[str] = None): + self.target = target + self.name = name + self.install_path = install_path + self.subproject = subproject + self.tag = tag + 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]], @@ -1497,6 +1507,7 @@ class Backend: self.generate_man_install(d) self.generate_emptydir_install(d) self.generate_data_install(d) + self.generate_symlink_install(d) self.generate_custom_install_script(d) self.generate_subdir_install(d) return d @@ -1717,6 +1728,15 @@ class Backend: de.install_mode, de.subproject, tag=tag, data_type=de.data_type) d.data.append(i) + def generate_symlink_install(self, d: InstallData) -> None: + links: T.List[build.SymlinkData] = self.build.get_symlinks() + for l in links: + assert isinstance(l, build.SymlinkData) + install_dir = l.install_dir + name_abs = os.path.join(install_dir, l.name) + s = InstallSymlinkData(l.target, name_abs, install_dir, l.subproject, l.install_tag) + d.symlinks.append(s) + def generate_subdir_install(self, d: InstallData) -> None: for sd in self.build.get_install_subdirs(): if sd.from_source_dir: diff --git a/mesonbuild/build.py b/mesonbuild/build.py index 545575c08..89c158e47 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -251,6 +251,7 @@ class Build: self.man: T.List[Man] = [] self.emptydir: T.List[EmptyDir] = [] self.data: T.List[Data] = [] + self.symlinks: T.List[SymlinkData] = [] self.static_linker: PerMachine[StaticLinker] = PerMachine(None, None) self.subprojects = {} self.subproject_dir = '' @@ -329,6 +330,9 @@ class Build: def get_data(self) -> T.List['Data']: return self.data + def get_symlinks(self) -> T.List['SymlinkData']: + return self.symlinks + def get_emptydir(self) -> T.List['EmptyDir']: return self.emptydir @@ -2802,6 +2806,18 @@ class Data(HoldableObject): self.subproject = subproject self.data_type = data_type +class SymlinkData(HoldableObject): + def __init__(self, target: str, name: str, install_dir: str, + subproject: str, install_tag: T.Optional[str] = None): + self.target = target + if name != os.path.basename(name): + raise InvalidArguments(f'Link name is "{name}", but link names cannot contain path separators. ' + 'The dir part should be in install_dir.') + self.name = name + self.install_dir = install_dir + self.subproject = subproject + self.install_tag = install_tag + class TestSetup: def __init__(self, exe_wrapper: T.List[str], gdb: bool, timeout_multiplier: int, env: EnvironmentVariables, diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index 97e1a06ce..ae6be3bc8 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -364,6 +364,7 @@ class Interpreter(InterpreterBase, HoldableObject): 'install_headers': self.func_install_headers, 'install_man': self.func_install_man, 'install_subdir': self.func_install_subdir, + 'install_symlink': self.func_install_symlink, 'is_disabler': self.func_is_disabler, 'is_variable': self.func_is_variable, 'jar': self.func_jar, @@ -425,6 +426,7 @@ class Interpreter(InterpreterBase, HoldableObject): build.Man: OBJ.ManHolder, build.EmptyDir: OBJ.EmptyDirHolder, build.Data: OBJ.DataHolder, + build.SymlinkData: OBJ.SymlinkDataHolder, build.InstallDir: OBJ.InstallDirHolder, build.IncludeDirs: OBJ.IncludeDirsHolder, build.EnvironmentVariables: OBJ.EnvironmentVariablesHolder, @@ -476,6 +478,8 @@ class Interpreter(InterpreterBase, HoldableObject): self.build.install_scripts.append(v) elif isinstance(v, build.Data): self.build.data.append(v) + elif isinstance(v, build.SymlinkData): + self.build.symlinks.append(v) elif isinstance(v, dependencies.InternalDependency): # FIXME: This is special cased and not ideal: # The first source is our new VapiTarget, the rest are deps @@ -1968,6 +1972,24 @@ This will become a hard error in the future.''', location=node) return d + @FeatureNew('install_symlink', '0.61.0') + @typed_pos_args('symlink_name', str) + @typed_kwargs( + 'install_symlink', + KwargInfo('pointing_to', str, required=True), + KwargInfo('install_dir', str, required=True), + KwargInfo('install_tag', (str, NoneType)), + ) + def func_install_symlink(self, node: mparser.BaseNode, + args: T.Tuple[T.List[str]], + kwargs) -> build.SymlinkData: + name = args[0] # Validation while creating the SymlinkData object + target = kwargs['pointing_to'] + l = build.SymlinkData(target, name, kwargs['install_dir'], + self.subproject, kwargs['install_tag']) + self.build.symlinks.append(l) + return l + @typed_pos_args('subdir', str) @typed_kwargs( 'subdir', diff --git a/mesonbuild/interpreter/interpreterobjects.py b/mesonbuild/interpreter/interpreterobjects.py index 602623ae8..6ff4b0fcc 100644 --- a/mesonbuild/interpreter/interpreterobjects.py +++ b/mesonbuild/interpreter/interpreterobjects.py @@ -641,6 +641,9 @@ class HeadersHolder(ObjectHolder[build.Headers]): class DataHolder(ObjectHolder[build.Data]): pass +class SymlinkDataHolder(ObjectHolder[build.SymlinkData]): + pass + class InstallDirHolder(ObjectHolder[build.InstallDir]): pass diff --git a/mesonbuild/minstall.py b/mesonbuild/minstall.py index 7979fe65f..60d533cf3 100644 --- a/mesonbuild/minstall.py +++ b/mesonbuild/minstall.py @@ -26,7 +26,10 @@ import sys import typing as T from . import environment -from .backend.backends import InstallData, InstallDataBase, InstallEmptyDir, TargetInstallData, ExecutableSerialisation +from .backend.backends import ( + InstallData, InstallDataBase, InstallEmptyDir, InstallSymlinkData, + TargetInstallData, ExecutableSerialisation +) from .coredata import major_versions_differ, MesonVersionMismatchException from .coredata import version as coredata_version from .mesonlib import Popen_safe, RealPathAction, is_windows @@ -317,6 +320,7 @@ class Installer: def __init__(self, options: 'ArgumentType', lf: T.TextIO): self.did_install_something = False + self.printed_symlink_error = False self.options = options self.lf = lf self.preserved_file_count = 0 @@ -394,7 +398,9 @@ class Installer: return run_exe(*args, **kwargs) return 0 - def should_install(self, d: T.Union[TargetInstallData, InstallEmptyDir, InstallDataBase, ExecutableSerialisation]) -> bool: + def should_install(self, d: T.Union[TargetInstallData, InstallEmptyDir, + InstallDataBase, InstallSymlinkData, + 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: @@ -452,6 +458,29 @@ class Installer: append_to_log(self.lf, to_file) return True + def do_symlink(self, target: str, link: str, full_dst_dir: str) -> bool: + abs_target = target + if not os.path.isabs(target): + abs_target = os.path.join(full_dst_dir, target) + if not os.path.exists(abs_target): + raise RuntimeError(f'Tried to install symlink to missing file {abs_target}') + if os.path.exists(link): + if not os.path.islink(link): + raise RuntimeError(f'Destination {link!r} already exists and is not a symlink') + self.remove(link) + if not self.printed_symlink_error: + self.log(f'Installing symlink pointing to {target} to {link}') + try: + self.symlink(target, link, target_is_directory=os.path.isdir(abs_target)) + except (NotImplementedError, OSError): + if not self.printed_symlink_error: + print("Symlink creation does not work on this platform. " + "Skipping all symlinking.") + self.printed_symlink_error = True + return False + append_to_log(self.lf, link) + return True + 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: @@ -558,6 +587,7 @@ class Installer: self.install_man(d, dm, destdir, fullprefix) self.install_emptydir(d, dm, destdir, fullprefix) self.install_data(d, dm, destdir, fullprefix) + self.install_symlinks(d, dm, destdir, fullprefix) self.restore_selinux_contexts(destdir) self.apply_ldconfig(dm, destdir, libdir) self.run_install_script(d, destdir, fullprefix) @@ -596,6 +626,16 @@ class Installer: self.did_install_something = True self.set_mode(outfilename, i.install_mode, d.install_umask) + def install_symlinks(self, d: InstallData, dm: DirMaker, destdir: str, fullprefix: str) -> None: + for s in d.symlinks: + if not self.should_install(s): + continue + full_dst_dir = get_destdir_path(destdir, fullprefix, s.install_path) + full_link_name = get_destdir_path(destdir, fullprefix, s.name) + dm.makedirs(full_dst_dir, exist_ok=True) + if self.do_symlink(s.target, full_link_name, full_dst_dir): + self.did_install_something = True + def install_man(self, d: InstallData, dm: DirMaker, destdir: str, fullprefix: str) -> None: for m in d.man: if not self.should_install(m): @@ -712,21 +752,9 @@ class Installer: self.do_copydir(d, fname, outname, None, install_mode, dm) else: raise RuntimeError(f'Unknown file type for {fname!r}') - printed_symlink_error = False - for alias, to in aliases.items(): - try: - symlinkfilename = os.path.join(outdir, alias) - try: - self.remove(symlinkfilename) - except FileNotFoundError: - pass - self.symlink(to, symlinkfilename) - append_to_log(self.lf, symlinkfilename) - except (NotImplementedError, OSError): - if not printed_symlink_error: - print("Symlink creation does not work on this platform. " - "Skipping all symlinking.") - printed_symlink_error = True + for alias, target in aliases.items(): + symlinkfilename = os.path.join(outdir, alias) + self.do_symlink(target, symlinkfilename, outdir) if file_copied: self.did_install_something = True try: diff --git a/test cases/common/247 install_symlink/datafile.dat b/test cases/common/247 install_symlink/datafile.dat new file mode 100644 index 000000000..ff3104ba1 --- /dev/null +++ b/test cases/common/247 install_symlink/datafile.dat @@ -0,0 +1 @@ +this is a data file diff --git a/test cases/common/247 install_symlink/meson.build b/test cases/common/247 install_symlink/meson.build new file mode 100644 index 000000000..ae3038231 --- /dev/null +++ b/test cases/common/247 install_symlink/meson.build @@ -0,0 +1,9 @@ +project('install_emptydir') + +if build_machine.system() == 'windows' and meson.backend() == 'ninja' + error('MESON_SKIP_TEST windows does not support symlinks unless root or in development mode') +endif + +install_data('datafile.dat', install_dir: 'share/progname/C') +install_symlink('datafile.dat', pointing_to: '../C/datafile.dat', install_dir: 'share/progname/es') +install_symlink('rename_datafile.dat', pointing_to: '../C/datafile.dat', install_dir: 'share/progname/fr') diff --git a/test cases/common/247 install_symlink/test.json b/test cases/common/247 install_symlink/test.json new file mode 100644 index 000000000..33aa76e22 --- /dev/null +++ b/test cases/common/247 install_symlink/test.json @@ -0,0 +1,7 @@ +{ + "installed": [ + {"type": "file", "file": "usr/share/progname/C/datafile.dat"}, + {"type": "file", "file": "usr/share/progname/es/datafile.dat"}, + {"type": "file", "file": "usr/share/progname/fr/rename_datafile.dat"} + ] +} diff --git a/test cases/failing/118 pathsep in install_symlink/meson.build b/test cases/failing/118 pathsep in install_symlink/meson.build new file mode 100644 index 000000000..cce82c237 --- /dev/null +++ b/test cases/failing/118 pathsep in install_symlink/meson.build @@ -0,0 +1,3 @@ +project('symlink_pathsep') + +install_symlink('foo/bar', pointing_to: '/usr/baz/bar', install_dir: '/usr') diff --git a/test cases/failing/118 pathsep in install_symlink/test.json b/test cases/failing/118 pathsep in install_symlink/test.json new file mode 100644 index 000000000..e3f3a4a88 --- /dev/null +++ b/test cases/failing/118 pathsep in install_symlink/test.json @@ -0,0 +1,7 @@ +{ + "stdout": [ + { + "line": "test cases/failing/118 pathsep in install_symlink/meson.build:3:0: ERROR: Link name is \"foo/bar\", but link names cannot contain path separators. The dir part should be in install_dir." + } + ] +}