diff options
Diffstat (limited to 'mesonbuild/minstall.py')
-rw-r--r-- | mesonbuild/minstall.py | 183 |
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') |