aboutsummaryrefslogtreecommitdiff
path: root/mesonbuild/minstall.py
diff options
context:
space:
mode:
Diffstat (limited to 'mesonbuild/minstall.py')
-rw-r--r--mesonbuild/minstall.py183
1 files changed, 115 insertions, 68 deletions
diff --git a/mesonbuild/minstall.py b/mesonbuild/minstall.py
index 153ac34..3e425eb 100644
--- a/mesonbuild/minstall.py
+++ b/mesonbuild/minstall.py
@@ -12,19 +12,24 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-import sys, pickle, os, shutil, subprocess, errno
-import argparse
-import shlex
from glob import glob
from pathlib import Path
+import argparse
+import errno
+import os
+import pickle
+import shlex
+import shutil
+import subprocess
+import sys
+import typing as T
from . import environment
-from .scripts import depfixer
-from .scripts import destdir_join
-from .mesonlib import is_windows, Popen_safe
from .backend.backends import InstallData
from .coredata import major_versions_differ, MesonVersionMismatchException
from .coredata import version as coredata_version
+from .mesonlib import is_windows, Popen_safe
+from .scripts import depfixer, destdir_join
try:
from __main__ import __file__ as main_file
except ImportError:
@@ -32,13 +37,30 @@ except ImportError:
# This is only used for pkexec which is not, so this is fine.
main_file = None
+if T.TYPE_CHECKING:
+ from .mesonlib import FileMode
+
+ try:
+ from typing import Protocol
+ except AttributeError:
+ from typing_extensions import Protocol # type: ignore
+
+ class ArgumentType(Protocol):
+ """Typing information for the object returned by argparse."""
+ no_rebuild: bool
+ only_changed: bool
+ profile: bool
+ quiet: bool
+ wd: str
+
+
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.'''
-selinux_updates = []
+selinux_updates: T.List[str] = []
-def add_arguments(parser):
+def add_arguments(parser: argparse.Namespace) -> None:
parser.add_argument('-C', default='.', dest='wd',
help='directory to cd into before running')
parser.add_argument('--profile-self', action='store_true', dest='profile',
@@ -51,11 +73,11 @@ def add_arguments(parser):
help='Do not print every file that was installed.')
class DirMaker:
- def __init__(self, lf):
+ def __init__(self, lf: T.TextIO):
self.lf = lf
- self.dirs = []
+ self.dirs: T.List[str] = []
- def makedirs(self, path, exist_ok=False):
+ def makedirs(self, path: str, exist_ok: bool = False) -> None:
dirname = os.path.normpath(path)
dirs = []
while dirname != os.path.dirname(dirname):
@@ -72,25 +94,29 @@ class DirMaker:
dirs.reverse()
self.dirs += dirs
- def __enter__(self):
+ def __enter__(self) -> 'DirMaker':
return self
- def __exit__(self, exception_type, value, traceback):
+ def __exit__(self, exception_type: T.Type[Exception], value: T.Any, traceback: T.Any) -> None:
self.dirs.reverse()
for d in self.dirs:
append_to_log(self.lf, d)
-def is_executable(path, follow_symlinks=False):
+
+def is_executable(path: str, follow_symlinks: bool = False) -> bool:
'''Checks whether any of the "x" bits are set in the source file mode.'''
return bool(os.stat(path, follow_symlinks=follow_symlinks).st_mode & 0o111)
-def append_to_log(lf, line):
+
+def append_to_log(lf: T.TextIO, line: str) -> None:
lf.write(line)
if not line.endswith('\n'):
lf.write('\n')
lf.flush()
-def set_chown(path, user=None, group=None, dir_fd=None, follow_symlinks=True):
+
+def set_chown(path: str, user: T.Optional[str] = None, group: T.Optional[str] = None,
+ dir_fd: T.Optional[int] = None, follow_symlinks: bool = True) -> None:
# shutil.chown will call os.chown without passing all the parameters
# and particularly follow_symlinks, thus we replace it temporary
# with a lambda with all the parameters so that follow_symlinks will
@@ -98,26 +124,39 @@ def set_chown(path, user=None, group=None, dir_fd=None, follow_symlinks=True):
# Not nice, but better than actually rewriting shutil.chown until
# this python bug is fixed: https://bugs.python.org/issue18108
real_os_chown = os.chown
+
+ def chown(path: T.Union[int, str, 'os.PathLike[str]', bytes, 'os.PathLike[bytes]'],
+ uid: int, gid: int, *, dir_fd: T.Optional[int] = dir_fd,
+ follow_symlinks: bool = follow_symlinks) -> None:
+ """Override the default behavior of os.chown
+
+ Use a real function rather than a lambda to help mypy out. Also real
+ functions are faster.
+ """
+ real_os_chown(path, gid, uid, dir_fd=dir_fd, follow_symlinks=follow_symlinks)
+
try:
- os.chown = lambda p, u, g: real_os_chown(p, u, g,
- dir_fd=dir_fd,
- follow_symlinks=follow_symlinks)
+ os.chown = chown
shutil.chown(path, user, group)
- except Exception:
- raise
finally:
os.chown = real_os_chown
-def set_chmod(path, mode, dir_fd=None, follow_symlinks=True):
+
+def set_chmod(path: str, mode: int, dir_fd: T.Optional[int] = None,
+ follow_symlinks: bool = True) -> None:
try:
os.chmod(path, mode, dir_fd=dir_fd, follow_symlinks=follow_symlinks)
except (NotImplementedError, OSError, SystemError):
if not os.path.islink(path):
os.chmod(path, mode, dir_fd=dir_fd)
-def sanitize_permissions(path, umask):
+
+def sanitize_permissions(path: str, umask: T.Union[str, int]) -> None:
+ # TODO: with python 3.8 or typing_extensions we could replace this with
+ # `umask: T.Union[T.Literal['preserve'], int]`, which would be mroe correct
if umask == 'preserve':
return
+ assert isinstance(umask, int), 'umask should only be "preserver" or an integer'
new_perms = 0o777 if is_executable(path, follow_symlinks=False) else 0o666
new_perms &= ~umask
try:
@@ -126,7 +165,8 @@ def sanitize_permissions(path, umask):
msg = '{!r}: Unable to set permissions {!r}: {}, ignoring...'
print(msg.format(path, new_perms, e.strerror))
-def set_mode(path, mode, default_umask):
+
+def set_mode(path: str, mode: T.Optional['FileMode'], default_umask: T.Union[str, int]) -> None:
if mode is None or (mode.perms_s or mode.owner or mode.group) is None:
# Just sanitize permissions with the default umask
sanitize_permissions(path, default_umask)
@@ -159,7 +199,8 @@ def set_mode(path, mode, default_umask):
else:
sanitize_permissions(path, default_umask)
-def restore_selinux_contexts():
+
+def restore_selinux_contexts() -> None:
'''
Restores the SELinux context for files in @selinux_updates
@@ -189,15 +230,15 @@ def restore_selinux_contexts():
'Standard error:', err.decode(), sep='\n')
-def get_destdir_path(d, path):
+def get_destdir_path(destdir: str, fullprefix: str, path: str) -> str:
if os.path.isabs(path):
- output = destdir_join(d.destdir, path)
+ output = destdir_join(destdir, path)
else:
- output = os.path.join(d.fullprefix, path)
+ output = os.path.join(fullprefix, path)
return output
-def check_for_stampfile(fname):
+def check_for_stampfile(fname: str) -> str:
'''Some languages e.g. Rust have output files
whose names are not known at configure time.
Check if this is the case and return the real
@@ -222,19 +263,20 @@ def check_for_stampfile(fname):
return files[0]
return fname
+
class Installer:
- def __init__(self, options, lf):
+ def __init__(self, options: 'ArgumentType', lf: T.TextIO):
self.did_install_something = False
self.options = options
self.lf = lf
self.preserved_file_count = 0
- def log(self, msg):
+ def log(self, msg: str) -> None:
if not self.options.quiet:
print(msg)
- def should_preserve_existing_file(self, from_file, to_file):
+ def should_preserve_existing_file(self, from_file: str, to_file: str) -> bool:
if not self.options.only_changed:
return False
# Always replace danging symlinks
@@ -244,7 +286,8 @@ class Installer:
to_time = os.stat(to_file).st_mtime
return from_time <= to_time
- def do_copyfile(self, from_file, to_file, makedirs=None):
+ def do_copyfile(self, from_file: str, to_file: str,
+ makedirs: T.Optional[T.Tuple[T.Any, str]] = None) -> bool:
outdir = os.path.split(to_file)[0]
if not os.path.isfile(from_file) and not os.path.islink(from_file):
raise RuntimeError('Tried to install something that isn\'t a file:'
@@ -282,7 +325,9 @@ class Installer:
append_to_log(self.lf, to_file)
return True
- def do_copydir(self, data, src_dir, dst_dir, exclude, install_mode):
+ 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:
'''
Copies the contents of directory @src_dir into @dst_dir.
@@ -328,7 +373,7 @@ class Installer:
if os.path.exists(abs_dst):
print('Tried to copy directory {} but a file of that name already exists.'.format(abs_dst))
sys.exit(1)
- data.dirmaker.makedirs(abs_dst)
+ dm.makedirs(abs_dst)
shutil.copystat(abs_src, abs_dst)
sanitize_permissions(abs_dst, data.install_umask)
for f in files:
@@ -356,34 +401,34 @@ class Installer:
raise MesonVersionMismatchException(obj.version, coredata_version)
return obj
- def do_install(self, datafilename):
+ def do_install(self, datafilename: str) -> None:
with open(datafilename, 'rb') as ifile:
d = self.check_installdata(pickle.load(ifile))
- d.destdir = os.environ.get('DESTDIR', '')
- d.fullprefix = destdir_join(d.destdir, d.prefix)
+ destdir = os.environ.get('DESTDIR', '')
+ fullprefix = destdir_join(destdir, d.prefix)
if d.install_umask != 'preserve':
+ assert isinstance(d.install_umask, int)
os.umask(d.install_umask)
self.did_install_something = False
try:
- d.dirmaker = DirMaker(self.lf)
- with d.dirmaker:
- self.install_subdirs(d) # Must be first, because it needs to delete the old subtree.
- self.install_targets(d)
- self.install_headers(d)
- self.install_man(d)
- self.install_data(d)
+ with DirMaker(self.lf) as dm:
+ self.install_subdirs(d, dm, destdir, fullprefix) # Must be first, because it needs to delete the old subtree.
+ self.install_targets(d, dm, destdir, fullprefix)
+ self.install_headers(d, dm, destdir, fullprefix)
+ self.install_man(d, dm, destdir, fullprefix)
+ self.install_data(d, dm, destdir, fullprefix)
restore_selinux_contexts()
- self.run_install_script(d)
+ self.run_install_script(d, fullprefix)
if not self.did_install_something:
self.log('Nothing to install.')
if not self.options.quiet and self.preserved_file_count > 0:
self.log('Preserved {} unchanged files, see {} for the full list'
.format(self.preserved_file_count, os.path.normpath(self.lf.name)))
except PermissionError:
- if shutil.which('pkexec') is not None and 'PKEXEC_UID' not in os.environ and d.destdir is None:
+ if shutil.which('pkexec') is not None and 'PKEXEC_UID' not in os.environ and destdir is None:
print('Installation failed due to insufficient permissions.')
print('Attempting to use polkit to gain elevated privileges...')
os.execlp('pkexec', 'pkexec', sys.executable, main_file, *sys.argv[1:],
@@ -391,50 +436,50 @@ class Installer:
else:
raise
- def install_subdirs(self, d):
+ def install_subdirs(self, d: InstallData, dm: DirMaker, destdir: str, fullprefix: str) -> None:
for (src_dir, dst_dir, mode, exclude) in d.install_subdirs:
self.did_install_something = True
- full_dst_dir = get_destdir_path(d, dst_dir)
+ full_dst_dir = get_destdir_path(destdir, fullprefix, dst_dir)
self.log('Installing subdir {} to {}'.format(src_dir, full_dst_dir))
- d.dirmaker.makedirs(full_dst_dir, exist_ok=True)
- self.do_copydir(d, src_dir, full_dst_dir, exclude, mode)
+ dm.makedirs(full_dst_dir, exist_ok=True)
+ self.do_copydir(d, src_dir, full_dst_dir, exclude, mode, dm)
- def install_data(self, d):
+ def install_data(self, d: InstallData, dm: DirMaker, destdir: str, fullprefix: str) -> None:
for i in d.data:
fullfilename = i[0]
- outfilename = get_destdir_path(d, i[1])
+ outfilename = get_destdir_path(destdir, fullprefix, i[1])
mode = i[2]
outdir = os.path.dirname(outfilename)
- if self.do_copyfile(fullfilename, outfilename, makedirs=(d.dirmaker, outdir)):
+ if self.do_copyfile(fullfilename, outfilename, makedirs=(dm, outdir)):
self.did_install_something = True
set_mode(outfilename, mode, d.install_umask)
- def install_man(self, d):
+ def install_man(self, d: InstallData, dm: DirMaker, destdir: str, fullprefix: str) -> None:
for m in d.man:
full_source_filename = m[0]
- outfilename = get_destdir_path(d, m[1])
+ outfilename = get_destdir_path(destdir, fullprefix, m[1])
outdir = os.path.dirname(outfilename)
install_mode = m[2]
- if self.do_copyfile(full_source_filename, outfilename, makedirs=(d.dirmaker, outdir)):
+ if self.do_copyfile(full_source_filename, outfilename, makedirs=(dm, outdir)):
self.did_install_something = True
set_mode(outfilename, install_mode, d.install_umask)
- def install_headers(self, d):
+ def install_headers(self, d: InstallData, dm: DirMaker, destdir: str, fullprefix: str) -> None:
for t in d.headers:
fullfilename = t[0]
fname = os.path.basename(fullfilename)
- outdir = get_destdir_path(d, t[1])
+ outdir = get_destdir_path(destdir, fullprefix, t[1])
outfilename = os.path.join(outdir, fname)
install_mode = t[2]
- if self.do_copyfile(fullfilename, outfilename, makedirs=(d.dirmaker, outdir)):
+ if self.do_copyfile(fullfilename, outfilename, makedirs=(dm, outdir)):
self.did_install_something = True
set_mode(outfilename, install_mode, d.install_umask)
- def run_install_script(self, d):
+ def run_install_script(self, d: InstallData, fullprefix: str) -> None:
env = {'MESON_SOURCE_ROOT': d.source_dir,
'MESON_BUILD_ROOT': d.build_dir,
'MESON_INSTALL_PREFIX': d.prefix,
- 'MESON_INSTALL_DESTDIR_PREFIX': d.fullprefix,
+ 'MESON_INSTALL_DESTDIR_PREFIX': fullprefix,
'MESONINTROSPECT': ' '.join([shlex.quote(x) for x in d.mesonintrospect]),
}
if self.options.quiet:
@@ -459,7 +504,7 @@ class Installer:
print('FAILED: install script \'{}\' exit code {}, stopped'.format(name, rc))
sys.exit(rc)
- def install_targets(self, d):
+ def install_targets(self, d: InstallData, dm: DirMaker, destdir: str, fullprefix: str) -> None:
for t in d.targets:
if not os.path.exists(t.fname):
# For example, import libraries of shared modules are optional
@@ -470,7 +515,7 @@ class Installer:
raise RuntimeError('File {!r} could not be found'.format(t.fname))
file_copied = False # not set when a directory is copied
fname = check_for_stampfile(t.fname)
- outdir = get_destdir_path(d, t.outdir)
+ outdir = get_destdir_path(destdir, fullprefix, t.outdir)
outname = os.path.join(outdir, os.path.basename(fname))
final_path = os.path.join(d.prefix, t.outdir, os.path.basename(fname))
aliases = t.aliases
@@ -481,7 +526,7 @@ class Installer:
if not os.path.exists(fname):
raise RuntimeError('File {!r} could not be found'.format(fname))
elif os.path.isfile(fname):
- file_copied = self.do_copyfile(fname, outname, makedirs=(d.dirmaker, outdir))
+ file_copied = self.do_copyfile(fname, outname, makedirs=(dm, outdir))
set_mode(outname, install_mode, d.install_umask)
if should_strip and d.strip_bin is not None:
if fname.endswith('.jar'):
@@ -504,8 +549,8 @@ class Installer:
elif os.path.isdir(fname):
fname = os.path.join(d.build_dir, fname.rstrip('/'))
outname = os.path.join(outdir, os.path.basename(fname))
- d.dirmaker.makedirs(outdir, exist_ok=True)
- self.do_copydir(d, fname, outname, None, install_mode)
+ dm.makedirs(outdir, exist_ok=True)
+ self.do_copydir(d, fname, outname, None, install_mode, dm)
else:
raise RuntimeError('Unknown file type for {!r}'.format(fname))
printed_symlink_error = False
@@ -534,6 +579,7 @@ class Installer:
else:
raise
+
def rebuild_all(wd: str) -> bool:
if not (Path(wd) / 'build.ninja').is_file():
print('Only ninja backend is supported to rebuild the project before installation.')
@@ -551,7 +597,8 @@ def rebuild_all(wd: str) -> bool:
return True
-def run(opts):
+
+def run(opts: 'ArgumentType') -> int:
datafilename = 'meson-private/install.dat'
private_dir = os.path.dirname(datafilename)
log_dir = os.path.join(private_dir, '../meson-logs')