Skip to content

Commit

Permalink
add install_symlink function
Browse files Browse the repository at this point in the history
Allows installing symlinks directly from meson, which can
become useful in multiple scenarios. Current main use is to
help moving forward mesonbuild#9557
  • Loading branch information
pabloyoyoista authored and eli-schwartz committed Dec 1, 2021
1 parent bb5a09d commit 4f882ff
Show file tree
Hide file tree
Showing 14 changed files with 180 additions and 17 deletions.
1 change: 1 addition & 0 deletions data/syntax-highlighting/vim/syntax/meson.vim
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ syn keyword mesonBuiltin
\ install_headers
\ install_man
\ install_subdir
\ install_symlink
\ install_emptydir
\ is_disabler
\ is_variable
Expand Down
11 changes: 11 additions & 0 deletions docs/markdown/snippets/install_symlink.md
Original file line number Diff line number Diff line change
@@ -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')
```
34 changes: 34 additions & 0 deletions docs/yaml/functions/install_symlink.yaml
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions mesonbuild/ast/interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ def __init__(self, source_root: str, subdir: str, subproject: str, visitors: T.O
'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,
Expand Down
20 changes: 20 additions & 0 deletions mesonbuild/backend/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ def __init__(self, source_dir: str, build_dir: str, prefix: str, libdir: str,
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
Expand Down Expand Up @@ -168,6 +169,15 @@ def __init__(self, path: str, install_path: str, install_path_name: str,
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]],
Expand Down Expand Up @@ -1497,6 +1507,7 @@ def create_install_data(self) -> InstallData:
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
Expand Down Expand Up @@ -1717,6 +1728,15 @@ def generate_data_install(self, d: InstallData) -> None:
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:
Expand Down
16 changes: 16 additions & 0 deletions mesonbuild/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ def __init__(self, environment: environment.Environment):
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 = ''
Expand Down Expand Up @@ -329,6 +330,9 @@ def get_man(self) -> T.List['Man']:
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

Expand Down Expand Up @@ -2802,6 +2806,18 @@ def __init__(self, sources: T.List[File], install_dir: str, install_dir_name: st
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,
Expand Down
22 changes: 22 additions & 0 deletions mesonbuild/interpreter/interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,7 @@ def build_func_dict(self):
'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,
Expand Down Expand Up @@ -425,6 +426,7 @@ def build_holder_map(self) -> None:
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,
Expand Down Expand Up @@ -476,6 +478,8 @@ def process_new_values(self, invalues: T.List[T.Union[TYPE_var, ExecutableSerial
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
Expand Down Expand Up @@ -1968,6 +1972,24 @@ def func_install_emptydir(self, node: mparser.BaseNode, args: T.Tuple[str], kwar

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',
Expand Down
3 changes: 3 additions & 0 deletions mesonbuild/interpreter/interpreterobjects.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
62 changes: 45 additions & 17 deletions mesonbuild/minstall.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@
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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -394,7 +398,9 @@ def run_exe(self, *args: T.Any, **kwargs: T.Any) -> int:
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:
Expand Down Expand Up @@ -452,6 +458,29 @@ def do_copyfile(self, from_file: str, to_file: str,
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:
Expand Down Expand Up @@ -558,6 +587,7 @@ def do_install(self, datafilename: str) -> None:
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)
Expand Down Expand Up @@ -596,6 +626,16 @@ def install_data(self, d: InstallData, dm: DirMaker, destdir: str, fullprefix: s
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):
Expand Down Expand Up @@ -712,21 +752,9 @@ def install_targets(self, d: InstallData, dm: DirMaker, destdir: str, fullprefix
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:
Expand Down
1 change: 1 addition & 0 deletions test cases/common/247 install_symlink/datafile.dat
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
this is a data file
9 changes: 9 additions & 0 deletions test cases/common/247 install_symlink/meson.build
Original file line number Diff line number Diff line change
@@ -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')
7 changes: 7 additions & 0 deletions test cases/common/247 install_symlink/test.json
Original file line number Diff line number Diff line change
@@ -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"}
]
}
3 changes: 3 additions & 0 deletions test cases/failing/118 pathsep in install_symlink/meson.build
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
project('symlink_pathsep')

install_symlink('foo/bar', pointing_to: '/usr/baz/bar', install_dir: '/usr')
7 changes: 7 additions & 0 deletions test cases/failing/118 pathsep in install_symlink/test.json
Original file line number Diff line number Diff line change
@@ -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."
}
]
}

0 comments on commit 4f882ff

Please sign in to comment.