blob: a8c0cbda5206c076d68772adaccf6c30cc4fe85c [file] [log] [blame]
# Copyright 2018 Gentoo Foundation
# Distributed under the terms of the GNU General Public License v2
__all__ = ['install_mask_dir', 'InstallMask']
import collections
import errno
import fnmatch
import functools
import operator
import sys
from portage import os, _unicode_decode
from portage.exception import (
OperationNotPermitted, PermissionDenied, FileNotFound)
from portage.util import normalize_path
if sys.hexversion >= 0x3000000:
_unicode = str
else:
_unicode = unicode
def _defaultdict_tree():
return collections.defaultdict(_defaultdict_tree)
_pattern = collections.namedtuple('_pattern', (
'orig_index',
'is_inclusive',
'pattern',
'leading_slash',
))
class InstallMask(object):
def __init__(self, install_mask):
"""
@param install_mask: INSTALL_MASK value
@type install_mask: str
"""
# Patterns not anchored with leading slash
self._unanchored = []
# Patterns anchored with leading slash are indexed by leading
# non-glob components, making it possible to minimize the
# number of fnmatch calls. For example:
# /foo*/bar -> {'.': ['/foo*/bar']}
# /foo/bar* -> {'foo': {'.': ['/foo/bar*']}}
# /foo/bar/ -> {'foo': {'bar': {'.': ['/foo/bar/']}}}
self._anchored = _defaultdict_tree()
for orig_index, pattern in enumerate(install_mask.split()):
# if pattern starts with -, possibly exclude this path
is_inclusive = not pattern.startswith('-')
if not is_inclusive:
pattern = pattern[1:]
pattern_obj = _pattern(orig_index, is_inclusive, pattern, pattern.startswith('/'))
# absolute path pattern
if pattern_obj.leading_slash:
current_dir = self._anchored
for component in list(filter(None, pattern.split('/'))):
if '*' in component:
break
else:
current_dir = current_dir[component]
current_dir.setdefault('.', []).append(pattern_obj)
# filename
else:
self._unanchored.append(pattern_obj)
def _iter_relevant_patterns(self, path):
"""
Iterate over patterns that may be relevant for the given path.
Patterns anchored with leading / are indexed by leading
non-glob components, making it possible to minimize the
number of fnmatch calls.
"""
current_dir = self._anchored
components = list(filter(None, path.split('/')))
patterns = []
patterns.extend(current_dir.get('.', []))
for component in components:
next_dir = current_dir.get(component, None)
if next_dir is None:
break
current_dir = next_dir
patterns.extend(current_dir.get('.', []))
if patterns:
# Sort by original pattern index, since order matters for
# non-inclusive patterns.
patterns.extend(self._unanchored)
if any(not pattern.is_inclusive for pattern in patterns):
patterns.sort(key=operator.attrgetter('orig_index'))
return iter(patterns)
return iter(self._unanchored)
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_obj in self._iter_relevant_patterns(path):
is_inclusive, pattern = pattern_obj.is_inclusive, pattern_obj.pattern
# absolute path pattern
if pattern_obj.leading_slash:
# handle trailing slash for explicit directory match
if path.endswith('/'):
pattern = pattern.rstrip('/') + '/'
# 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:].rstrip('/') + '/*')):
ret = is_inclusive
# filename
else:
if fnmatch.fnmatch(os.path.basename(path), pattern):
ret = is_inclusive
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(_unicode(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):
try:
parent = _unicode_decode(parent, errors='strict')
except UnicodeDecodeError:
continue
dir_stack.append(parent)
for fname in files:
try:
fname = _unicode_decode(fname, errors='strict')
except UnicodeDecodeError:
continue
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