The InstallMask.match code comes from the dblink _is_install_masked method from portage-mgorny:
https://github.com/mgorny/portage/commit/f5ac3af3216d209618a2f232b4bf720bc8b520ad --- bin/misc-functions.sh | 73 -------------------------- pym/_emerge/PackagePhase.py | 13 ++++- pym/portage/dbapi/vartree.py | 24 ++++++--- pym/portage/util/install_mask.py | 109 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 139 insertions(+), 80 deletions(-) create mode 100644 pym/portage/util/install_mask.py diff --git a/bin/misc-functions.sh b/bin/misc-functions.sh index 4fe6ead17..ea2557724 100755 --- a/bin/misc-functions.sh +++ b/bin/misc-functions.sh @@ -317,72 +317,6 @@ postinst_qa_check() { done < <(printf "%s\0" "${qa_checks[@]}" | LC_ALL=C sort -u -z) } -install_mask() { - local root="$1" - shift - local install_mask="$*" - - # We think of $install_mask as a space-separated list of - # globs. We don't want globbing in the "for" loop; that is, we - # want to keep the asterisks in the indivual entries. - local shopts=$- - set -o noglob - local no_inst - for no_inst in ${install_mask}; do - # Here, $no_inst is a single "entry" potentially - # containing a glob. From now on, we *do* want to - # expand it. - set +o noglob - - # The standard case where $no_inst is something that - # the shell could expand on its own. - if [[ -e "${root}"/${no_inst} || -L "${root}"/${no_inst} || - "${root}"/${no_inst} != $(echo "${root}"/${no_inst}) ]] ; then - __quiet_mode || einfo "Removing ${no_inst}" - rm -Rf "${root}"/${no_inst} >&/dev/null - fi - - # We also want to allow the user to specify a "bare - # glob." For example, $no_inst="*.a" should prevent - # ALL files ending in ".a" from being installed, - # regardless of their location/depth. We achieve this - # by passing the pattern to `find`. - find "${root}" \( -path "${no_inst}" -or -name "${no_inst}" \) \ - -print0 2> /dev/null \ - | LC_ALL=C sort -z \ - | while read -r -d ''; do - __quiet_mode || einfo "Removing /${REPLY#${root}}" - rm -Rf "${REPLY}" >&/dev/null - done - - done - # set everything back the way we found it - set +o noglob - set -${shopts} -} - -preinst_mask() { - if [ -z "${D}" ]; then - eerror "${FUNCNAME}: D is unset" - return 1 - fi - - if ! ___eapi_has_prefix_variables; then - local ED=${D} - fi - - # Make sure $PWD is not ${D} so that we don't leave gmon.out files - # in there in case any tools were built with -pg in CFLAGS. - cd "${T}" - - install_mask "${ED}" "${INSTALL_MASK}" - - # remove share dir if unnessesary - if has nodoc $FEATURES || has noman $FEATURES || has noinfo $FEATURES; then - rmdir "${ED%/}/usr/share" &> /dev/null - fi -} - preinst_sfperms() { if [ -z "${D}" ]; then eerror "${FUNCNAME}: D is unset" @@ -506,13 +440,6 @@ __dyn_package() { # in there in case any tools were built with -pg in CFLAGS. cd "${T}" || die - if [[ -n ${PKG_INSTALL_MASK} ]] ; then - # The caller makes ${D} refer to a temporary copy in this - # case, so that this does not mask files from the normal - # install image. - install_mask "${D%/}${EPREFIX}/" "${PKG_INSTALL_MASK}" - fi - local tar_options="" [[ $PORTAGE_VERBOSE = 1 ]] && tar_options+=" -v" has xattr ${FEATURES} && [[ $(tar --help 2> /dev/null) == *--xattrs* ]] && tar_options+=" --xattrs" diff --git a/pym/_emerge/PackagePhase.py b/pym/_emerge/PackagePhase.py index 083745059..35137532a 100644 --- a/pym/_emerge/PackagePhase.py +++ b/pym/_emerge/PackagePhase.py @@ -11,6 +11,8 @@ import portage from portage import os from portage import _encodings from portage import _unicode_encode +from portage.util._async.AsyncFunction import AsyncFunction +from portage.util.install_mask import install_mask_dir, InstallMask class PackagePhase(CompositeTask): @@ -31,7 +33,7 @@ class PackagePhase(CompositeTask): encoding=_encodings['fs'], errors='strict'), mode='r', encoding=_encodings['repo.content'], errors='replace') as f: - self._pkg_install_mask = f.read().split() + self._pkg_install_mask = InstallMask(f.read()) except OSError: self._pkg_install_mask = None if self._pkg_install_mask: @@ -51,6 +53,15 @@ class PackagePhase(CompositeTask): if self._default_exit(proc) != os.EX_OK: self.wait() else: + self._start_task(AsyncFunction( + target=install_mask_dir, + args=(self._proot, self._pkg_install_mask)), + self._pkg_install_mask_exit) + + def _pkg_install_mask_exit(self, proc): + if self._default_exit(proc) != os.EX_OK: + self.wait() + else: self._start_package_phase() def _start_package_phase(self): diff --git a/pym/portage/dbapi/vartree.py b/pym/portage/dbapi/vartree.py index bed76d80f..bf7bd8c2a 100644 --- a/pym/portage/dbapi/vartree.py +++ b/pym/portage/dbapi/vartree.py @@ -32,6 +32,7 @@ portage.proxy.lazyimport.lazyimport(globals(), 'grabdict,normalize_path,new_protect_filename', 'portage.util.digraph:digraph', 'portage.util.env_update:env_update', + 'portage.util.install_mask:install_mask_dir,InstallMask', 'portage.util.listdir:dircache,listdir', 'portage.util.movefile:movefile', 'portage.util.path:first_existing,iter_parents', @@ -3841,15 +3842,26 @@ class dblink(object): max_dblnk = dblnk self._installed_instance = max_dblnk - # Apply INSTALL_MASK before collision-protect, since it may + # Update INSTALL_MASK before collision-protect, since it may # be useful to avoid collisions in some scenarios. # We cannot detect if this is needed or not here as INSTALL_MASK can be # modified by bashrc files. - phase = MiscFunctionsProcess(background=False, - commands=["preinst_mask"], phase="preinst", - scheduler=self._scheduler, settings=self.settings) - phase.start() - phase.wait() + try: + with io.open(_unicode_encode(os.path.join(inforoot, "INSTALL_MASK"), + encoding=_encodings['fs'], errors='strict'), + mode='r', encoding=_encodings['repo.content'], + errors='replace') as f: + install_mask = InstallMask(f.read()) + except OSError: + install_mask = None + + if install_mask: + install_mask_dir(self.settings["ED"], install_mask) + if any(x in self.settings.features for x in ('nodoc', 'noman', 'noinfo')): + try: + os.rmdir(os.path.join(self.settings["ED"], 'usr', 'share')) + except OSError: + pass # We check for unicode encoding issues after src_install. However, # the check must be repeated here for binary packages (it's diff --git a/pym/portage/util/install_mask.py b/pym/portage/util/install_mask.py new file mode 100644 index 000000000..64fe0b21a --- /dev/null +++ b/pym/portage/util/install_mask.py @@ -0,0 +1,109 @@ +# Copyright 2018 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 + +__all__ = ['install_mask_dir', 'InstallMask'] + +import errno +import fnmatch +import os + +from portage.exception import ( + OperationNotPermitted, PermissionDenied, FileNotFound) +from portage.util import normalize_path + + +class InstallMask(object): + def __init__(self, install_mask): + """ + @param install_mask: INSTALL_MASK value + @type install_mask: str + """ + self._install_mask = install_mask.split() + + def match(self, path): + """ + @param path: file path relative to ${ED} + @type path: str + @rtype: bool + @return: True if path matches INSTALL_MASK, False otherwise + """ + ret = False + for pattern in self._install_mask: + # absolute path pattern + if pattern.startswith('/'): + # match either exact path or one of parent dirs + # the latter is done via matching pattern/* + if (fnmatch.fnmatch(path, pattern[1:]) + or fnmatch.fnmatch(path, pattern[1:] + '/*')): + ret = True + break + # filename + else: + if fnmatch.fnmatch(os.path.basename(path), pattern): + ret = True + break + return ret + + +_exc_map = { + errno.ENOENT: FileNotFound, + errno.EPERM: OperationNotPermitted, + errno.EACCES: PermissionDenied, +} + + +def _raise_exc(e): + """ + Wrap OSError with portage.exception wrapper exceptions, with + __cause__ chaining when python supports it. + + @param e: os exception + @type e: OSError + @raise PortageException: portage.exception wrapper exception + """ + wrapper_cls = _exc_map.get(e.errno) + if wrapper_cls is None: + raise + wrapper = wrapper_cls(str(e)) + wrapper.__cause__ = e + raise wrapper + + +def install_mask_dir(base_dir, install_mask, onerror=None): + """ + Remove files and directories matched by INSTALL_MASK. + + @param base_dir: directory path corresponding to ${ED} + @type base_dir: str + @param install_mask: INSTALL_MASK configuration + @type install_mask: InstallMask + """ + onerror = onerror or _raise_exc + base_dir = normalize_path(base_dir) + base_dir_len = len(base_dir) + 1 + dir_stack = [] + + # Remove masked files. + for parent, dirs, files in os.walk(base_dir, onerror=onerror): + dir_stack.append(parent) + for fname in files: + abs_path = os.path.join(parent, fname) + relative_path = abs_path[base_dir_len:] + if install_mask.match(relative_path): + try: + os.unlink(abs_path) + except OSError as e: + onerror(e) + + # Remove masked dirs (unless non-empty due to exclusions). + while True: + try: + dir_path = dir_stack.pop() + except IndexError: + break + + if install_mask.match(dir_path[base_dir_len:]): + try: + os.rmdir(dir_path) + except OSError: + pass -- 2.13.6