diff options
author | Fabian Groffen <grobian@gentoo.org> | 2019-02-28 13:31:19 +0100 |
---|---|---|
committer | Fabian Groffen <grobian@gentoo.org> | 2019-02-28 13:31:19 +0100 |
commit | 991d7349c8faab28bf956c00e477e115654ba3b0 (patch) | |
tree | 972397af92c66b767237a06bc338b3ca253bf0a6 | |
parent | LinkageMapMachO: ensure availability of 'os' (diff) | |
parent | Updates for portage-2.3.62 release (diff) | |
download | portage-991d7349.tar.gz portage-991d7349.tar.bz2 portage-991d7349.zip |
Merge remote-tracking branch 'overlays-gentoo-org/master' into prefix
Signed-off-by: Fabian Groffen <grobian@gentoo.org>
28 files changed, 839 insertions, 168 deletions
diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 0a5d49bb6..b817561ef 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -1,6 +1,56 @@ Release Notes; upgrade information mainly. Features/major bugfixes are listed in NEWS +portage-2.3.62 +================================== +* Bug Fixes: + - Bug 678278 unprivileged sync emergelog lock permission denied + + +portage-2.3.61 +================================== +* Bug Fixes: + - Bug 677776 gnome2_icon_cache_update -> xdg_icon_cache_update + - Bug 677800 Don't define a default for ACCEPT_LICENSE + - Bug 678218 locks: handle sshfs hardlink inode numbers + - FL-6227 cpuinfo: use better available CPU calculation + + +portage-2.3.60 +================================== +* Bug Fixes: + - Bug 636798 handle lock file removal on NFS + + +portage-2.3.59 +================================== +* Bug Fixes: + - Bug 675868 pid-sandbox: pid-ns-init TIOCSCTTY after setsid + + +portage-2.3.58 +================================== +* Bug Fixes: + - Bug 675868 run pid-sandbox pid-ns-init as root + - Bug 675870 setsid for pid-sandbox process group signals + - Bug 676014 use local ECLASS variable during ebuild inherit + + +portage-2.3.57 +================================== +* Bug Fixes: + - Bug 675756 emerge: compare new SLOT USE to installed SLOT + - Bug 675826 INSTALL_MASK scalability: minimize fnmatch calls + - Bug 675828 pid-sandbox: fix child process signal disposition + + +portage-2.3.56 +================================== +* Bug Fixes: + - Bug 675284 restore canonicalize func + - Bug 675312 pid-sandbox: execute pid-ns-init as pid 1 + + portage-2.3.55 ================================== * Bug Fixes: diff --git a/bin/ebuild.sh b/bin/ebuild.sh index eb5615056..5acf36995 100755 --- a/bin/ebuild.sh +++ b/bin/ebuild.sh @@ -240,24 +240,22 @@ inherit() { ECLASS_DEPTH=$(($ECLASS_DEPTH + 1)) if [[ ${ECLASS_DEPTH} -gt 1 ]]; then debug-print "*** Multiple Inheritence (Level: ${ECLASS_DEPTH})" - fi - if [[ -n $ECLASS && -n ${!__export_funcs_var} ]] ; then - echo "QA Notice: EXPORT_FUNCTIONS is called before inherit in" \ - "$ECLASS.eclass. For compatibility with <=portage-2.1.6.7," \ - "only call EXPORT_FUNCTIONS after inherit(s)." \ - | fmt -w 75 | while read -r ; do eqawarn "$REPLY" ; done + # Since ECLASS_DEPTH > 1, the following variables are locals from the + # previous inherit call in the call stack. + if [[ -n ${ECLASS} && -n ${!__export_funcs_var} ]] ; then + eqawarn "QA Notice: EXPORT_FUNCTIONS is called before inherit in ${ECLASS}.eclass." + eqawarn "For compatibility with <=portage-2.1.6.7, only call EXPORT_FUNCTIONS" + eqawarn "after inherit(s)." + fi fi + local -x ECLASS + local __export_funcs_var local repo_location local location local potential_location local x - - # These variables must be restored before returning. - local PECLASS=$ECLASS - local prev_export_funcs_var=$__export_funcs_var - local B_IUSE local B_REQUIRED_USE local B_DEPEND @@ -269,7 +267,7 @@ inherit() { location="" potential_location="" - export ECLASS="$1" + ECLASS="$1" __export_funcs_var=__export_functions_$ECLASS_DEPTH unset $__export_funcs_var @@ -379,12 +377,6 @@ inherit() { shift done ((--ECLASS_DEPTH)) # Returns 1 when ECLASS_DEPTH reaches 0. - if (( ECLASS_DEPTH > 0 )) ; then - export ECLASS=$PECLASS - __export_funcs_var=$prev_export_funcs_var - else - unset ECLASS __export_funcs_var - fi return 0 } @@ -476,7 +468,7 @@ __try_source() { # If $- contains x, then tracing has already been enabled # elsewhere for some reason. We preserve it's state so as # not to interfere. - if [[ ${qa} ]]; then + if ! ${qa} ; then source "${1}" else __qa_source "${1}" diff --git a/bin/misc-functions.sh b/bin/misc-functions.sh index 692becaf6..d01ffe43a 100755 --- a/bin/misc-functions.sh +++ b/bin/misc-functions.sh @@ -47,7 +47,6 @@ install_symlink_html_docs() { fi } -# PREFIX LOCAL: we need this on some platforms # replacement for "readlink -f" or "realpath" READLINK_F_WORKS="" canonicalize() { @@ -81,7 +80,6 @@ canonicalize() { cd "${wd}" return 1 } -# END PREFIX LOCAL install_qa_check() { local d f i qa_var x paths qa_checks=() checks_run=() diff --git a/bin/pid-ns-init b/bin/pid-ns-init index 843257b70..cfbd65280 100644 --- a/bin/pid-ns-init +++ b/bin/pid-ns-init @@ -1,23 +1,120 @@ #!/usr/bin/env python -# Copyright 2018 Gentoo Authors +# Copyright 2018-2019 Gentoo Authors # Distributed under the terms of the GNU General Public License v2 +import errno +import fcntl +import functools import os +import platform +import signal +import subprocess import sys +import termios + + +KILL_SIGNALS = ( + signal.SIGINT, + signal.SIGTERM, + signal.SIGHUP, +) + + +def forward_kill_signal(pid, signum, frame): + if pid == 0: + # Avoid a signal feedback loop, since signals sent to the + # process group are also sent to the current process. + signal.signal(signum, signal.SIG_DFL) + os.kill(pid, signum) + + +def preexec_fn(uid, gid, groups, umask): + if gid is not None: + os.setgid(gid) + if groups is not None: + os.setgroups(groups) + if uid is not None: + os.setuid(uid) + if umask is not None: + os.umask(umask) + + # CPython >= 3 subprocess.Popen handles this internally. + if sys.version_info.major < 3 or platform.python_implementation() != 'CPython': + for signum in ( + signal.SIGHUP, + signal.SIGINT, + signal.SIGPIPE, + signal.SIGQUIT, + signal.SIGTERM, + ): + signal.signal(signum, signal.SIG_DFL) def main(argv): if len(argv) < 2: - return 'Usage: {} <main-child-pid>'.format(argv[0]) - main_child_pid = int(argv[1]) + return 'Usage: {} <main-child-pid> or <uid> <gid> <groups> <umask> <pass_fds> <binary> <argv0> [arg]..'.format(argv[0]) + + if len(argv) == 2: + # The child process is init (pid 1) in a child pid namespace, and + # the current process supervises from within the global pid namespace + # (forwarding signals to init and forwarding exit status to the parent + # process). + main_child_pid = int(argv[1]) + setsid = False + proc = None + else: + # The current process is init (pid 1) in a child pid namespace. + uid, gid, groups, umask, pass_fds, binary, args = argv[1], argv[2], argv[3], argv[4], tuple(int(fd) for fd in argv[5].split(',')), argv[6], argv[7:] + uid = int(uid) if uid else None + gid = int(gid) if gid else None + groups = tuple(int(group) for group in groups.split(',')) if groups else None + umask = int(umask) if umask else None + + popen_kwargs = {} + popen_kwargs['preexec_fn'] = functools.partial(preexec_fn, uid, gid, groups, umask) + if sys.version_info.major > 2: + popen_kwargs['pass_fds'] = pass_fds + # Isolate parent process from process group SIGSTOP (bug 675870) + setsid = True + os.setsid() + if sys.stdout.isatty(): + try: + fcntl.ioctl(sys.stdout, termios.TIOCSCTTY, 0) + except EnvironmentError as e: + if e.errno == errno.EPERM: + # This means that stdout refers to the controlling terminal + # of the parent process, and in this case we do not want to + # steel it. + pass + else: + raise + proc = subprocess.Popen(args, executable=binary, **popen_kwargs) + main_child_pid = proc.pid + + # If setsid has been called, use kill(0, signum) to + # forward signals to the entire process group. + sig_handler = functools.partial(forward_kill_signal, 0 if setsid else main_child_pid) + for signum in KILL_SIGNALS: + signal.signal(signum, sig_handler) # wait for child processes while True: - pid, status = os.wait() + try: + pid, status = os.wait() + except EnvironmentError as e: + if e.errno == errno.EINTR: + continue + raise if pid == main_child_pid: + if proc is not None: + # Suppress warning messages like this: + # ResourceWarning: subprocess 1234 is still running + proc.returncode = 0 + if os.WIFEXITED(status): return os.WEXITSTATUS(status) elif os.WIFSIGNALED(status): + signal.signal(os.WTERMSIG(status), signal.SIG_DFL) os.kill(os.getpid(), os.WTERMSIG(status)) # go to the unreachable place break diff --git a/bin/postinst-qa-check.d/50gnome2-utils b/bin/postinst-qa-check.d/50gnome2-utils deleted file mode 100644 index 29ea7c844..000000000 --- a/bin/postinst-qa-check.d/50gnome2-utils +++ /dev/null @@ -1,64 +0,0 @@ -# check for missing calls to gnome2-utils regen functions - -gnome2_icon_cache_check() { - type -P gtk-update-icon-cache &>/dev/null || return - - local d f all_files=() missing - for d in usr/share/icons/*/; do - # gnome2_icon_cache_update updates only themes with an index - [[ -f ${d}/index.theme ]] || continue - - local files=() find_args=( - # gtk-update-icon-cache supports only specific file - # suffixes; match that to avoid false positives - '(' -name '*.png' -o -name '*.svg' - -o -name '*.xpm' -o -name '*.icon' ')' - ) - # if the cache does not exist at all, we complain for any file - # otherwise, we look for files newer than the cache - [[ -f ${d}/icon-theme.cache ]] && - find_args+=( -newercm "${d}"/icon-theme.cache ) || missing=1 - - # (use -mindepth 2 to easily skip the cache files) - while read -r -d $'\0' f; do - files+=( "${f}" ) - done < <(find "${d}" -mindepth 2 -type f "${find_args[@]}" -print0) - - # if any files were found, update the db to avoid repeating - # the warning for subsequent packages - if [[ ${files[@]} ]]; then - all_files+=("${files[@]}") - addwrite "${d}" - gtk-update-icon-cache -qf "${d}" - fi - done - - # preinst initializes the baseline state for the posinst check - [[ ${PORTAGE_QA_PHASE} == preinst ]] && return - - # parallel-install makes it impossible to blame a specific package - has parallel-install ${FEATURES} && return - - # avoid false-positives on first install (bug 649464) - [[ ${PN} == gtk-update-icon-cache ]] && return - - # The eqatag call is prohibitively expensive if the cache is - # missing and there are a large number of files. - if [[ -z ${missing} && ${all_files[@]} ]]; then - eqawarn "QA Notice: new icons were found installed but GTK+ icon cache" - eqawarn "has not been updated:" - eqatag -v gnome2-utils.icon-cache "${all_files[@]/#//}" - eqawarn "Please make sure to call gnome2_icon_cache_update()" - eqawarn "in pkg_postinst() and pkg_postrm() phases of appropriate pkgs." - fi -} - -gnome2_utils_postinst_check() { - cd "${EROOT:-/}" || die - gnome2_icon_cache_check -} - -gnome2_utils_postinst_check -: # guarantee successful exit - -# vim:ft=sh diff --git a/bin/postinst-qa-check.d/50xdg-utils b/bin/postinst-qa-check.d/50xdg-utils index b33df4743..9a7c036e6 100644 --- a/bin/postinst-qa-check.d/50xdg-utils +++ b/bin/postinst-qa-check.d/50xdg-utils @@ -46,6 +46,59 @@ xdg_desktop_database_check() { fi } +xdg_icon_cache_check() { + type -P gtk-update-icon-cache &>/dev/null || return + + local d f all_files=() missing + for d in usr/share/icons/*/; do + # xdg_icon_cache_update updates only themes with an index + [[ -f ${d}/index.theme ]] || continue + + local files=() find_args=( + # gtk-update-icon-cache supports only specific file + # suffixes; match that to avoid false positives + '(' -name '*.png' -o -name '*.svg' + -o -name '*.xpm' -o -name '*.icon' ')' + ) + # if the cache does not exist at all, we complain for any file + # otherwise, we look for files newer than the cache + [[ -f ${d}/icon-theme.cache ]] && + find_args+=( -newercm "${d}"/icon-theme.cache ) || missing=1 + + # (use -mindepth 2 to easily skip the cache files) + while read -r -d $'\0' f; do + files+=( "${f}" ) + done < <(find "${d}" -mindepth 2 -type f "${find_args[@]}" -print0) + + # if any files were found, update the db to avoid repeating + # the warning for subsequent packages + if [[ ${files[@]} ]]; then + all_files+=("${files[@]}") + addwrite "${d}" + gtk-update-icon-cache -qf "${d}" + fi + done + + # preinst initializes the baseline state for the posinst check + [[ ${PORTAGE_QA_PHASE} == preinst ]] && return + + # parallel-install makes it impossible to blame a specific package + has parallel-install ${FEATURES} && return + + # avoid false-positives on first install (bug 649464) + [[ ${PN} == gtk-update-icon-cache ]] && return + + # The eqatag call is prohibitively expensive if the cache is + # missing and there are a large number of files. + if [[ -z ${missing} && ${all_files[@]} ]]; then + eqawarn "QA Notice: new icons were found installed but icon cache" + eqawarn "has not been updated:" + eqatag -v xdg-utils.icon-cache "${all_files[@]/#//}" + eqawarn "Please make sure to call xdg_icon_cache_update()" + eqawarn "in pkg_postinst() and pkg_postrm() phases of appropriate pkgs." + fi +} + xdg_mimeinfo_database_check() { type -P update-mime-database &>/dev/null || return @@ -92,6 +145,7 @@ xdg_mimeinfo_database_check() { xdg_utils_postinst_check() { cd "${EROOT:-/}" || die xdg_desktop_database_check + xdg_icon_cache_check xdg_mimeinfo_database_check } diff --git a/bin/preinst-qa-check.d/50gnome2-utils b/bin/preinst-qa-check.d/50gnome2-utils deleted file mode 120000 index ee57f814d..000000000 --- a/bin/preinst-qa-check.d/50gnome2-utils +++ /dev/null @@ -1 +0,0 @@ -../postinst-qa-check.d/50gnome2-utils
\ No newline at end of file diff --git a/bin/socks5-server.py b/bin/socks5-server.py index d46cf5345..f5d995f0d 100644 --- a/bin/socks5-server.py +++ b/bin/socks5-server.py @@ -17,6 +17,12 @@ else: # getattr() necessary because async is a keyword in Python >=3.7. asyncio_ensure_future = getattr(asyncio, 'async') +try: + current_task = asyncio.current_task +except AttributeError: + # Deprecated since Python 3.7 + current_task = asyncio.Task.current_task + class Socks5Server(object): """ @@ -154,7 +160,7 @@ class Socks5Server(object): # otherwise, start two loops: # remote -> local... t = asyncio_ensure_future(self.handle_proxied_conn( - proxied_reader, writer, asyncio.Task.current_task())) + proxied_reader, writer, current_task())) # and local -> remote... try: diff --git a/cnf/make.conf.example b/cnf/make.conf.example index 81b9c0328..34957eddd 100644 --- a/cnf/make.conf.example +++ b/cnf/make.conf.example @@ -1,4 +1,4 @@ -# Copyright 1999-2013 Gentoo Foundation +# Copyright 1999-2019 Gentoo Authors # Distributed under the terms of the GNU General Public License v2 # Contains local system settings for Portage system @@ -84,11 +84,11 @@ # license_groups file (see portage(5) man page). In addition to license # and group names, the * and -* wildcard tokens are also supported. # -# Accept any license except those in the EULA license group (default). -#ACCEPT_LICENSE="* -@EULA" -# # Only accept licenses in the FREE license group (i.e. Free Software). #ACCEPT_LICENSE="-* @FREE" +# +# Accept any license except those in the EULA license group. +#ACCEPT_LICENSE="* -@EULA" # Portage Directories # =================== diff --git a/cnf/make.globals b/cnf/make.globals index 5013957ea..e71325c91 100644 --- a/cnf/make.globals +++ b/cnf/make.globals @@ -1,4 +1,4 @@ -# Copyright 1999-2018 Gentoo Authors +# Copyright 1999-2019 Gentoo Authors # Distributed under the terms of the GNU General Public License v2 # System-wide defaults for the Portage system @@ -23,7 +23,6 @@ FCFLAGS="" # Approved by the mirror-admin team. GENTOO_MIRRORS="http://distfiles.gentoo.org" -ACCEPT_LICENSE="* -@EULA" ACCEPT_PROPERTIES="*" ACCEPT_RESTRICT="*" diff --git a/lib/_emerge/SpawnProcess.py b/lib/_emerge/SpawnProcess.py index cd535d143..395d66bb9 100644 --- a/lib/_emerge/SpawnProcess.py +++ b/lib/_emerge/SpawnProcess.py @@ -32,7 +32,7 @@ class SpawnProcess(SubProcess): _spawn_kwarg_names = ("env", "opt_name", "fd_pipes", "uid", "gid", "groups", "umask", "logfile", "path_lookup", "pre_exec", "close_fds", "cgroup", - "unshare_ipc", "unshare_net") + "unshare_ipc", "unshare_mount", "unshare_pid", "unshare_net") __slots__ = ("args",) + \ _spawn_kwarg_names + ("_pipe_logger", "_selinux_type",) diff --git a/lib/_emerge/resolver/output.py b/lib/_emerge/resolver/output.py index 24340576c..ed88cc51f 100644 --- a/lib/_emerge/resolver/output.py +++ b/lib/_emerge/resolver/output.py @@ -1,4 +1,4 @@ -# Copyright 2010-2018 Gentoo Foundation +# Copyright 2010-2019 Gentoo Authors # Distributed under the terms of the GNU General Public License v2 """Resolver output display operation. @@ -673,9 +673,14 @@ class Display(object): pkg_info.previous_pkg = self.vardb.match_pkgs( Atom('=' + pkg.cpv))[0] else: - slot_matches = self.vardb.match_pkgs(pkg.slot_atom) - if slot_matches: - pkg_info.previous_pkg = slot_matches[0] + cp_slot_matches = self.vardb.match_pkgs(pkg.slot_atom) + if cp_slot_matches: + pkg_info.previous_pkg = cp_slot_matches[0] + else: + cp_matches = self.vardb.match_pkgs(Atom(pkg.cp)) + if cp_matches: + # Use highest installed other-slot package instance. + pkg_info.previous_pkg = cp_matches[-1] return pkg_info diff --git a/lib/portage/dbapi/porttree.py b/lib/portage/dbapi/porttree.py index 76b7967f7..64a5f3681 100644 --- a/lib/portage/dbapi/porttree.py +++ b/lib/portage/dbapi/porttree.py @@ -1339,11 +1339,19 @@ class portagetree(object): " constructor is unused", DeprecationWarning, stacklevel=2) - self.portroot = settings["PORTDIR"] self.__virtual = virtual self.dbapi = portdbapi(mysettings=settings) @property + def portroot(self): + """Deprecated. Use the portdbapi getRepositoryPath method instead.""" + warnings.warn("The portroot attribute of " + "portage.dbapi.porttree.portagetree is deprecated. Use the " + "portdbapi getRepositoryPath method instead.", + DeprecationWarning, stacklevel=3) + return self.settings['PORTDIR'] + + @property def root(self): warnings.warn("The root attribute of " + \ "portage.dbapi.porttree.portagetree" + \ @@ -1383,7 +1391,11 @@ class portagetree(object): return self.dbapi.cp_all() def getname(self, pkgname): - "returns file location for this particular package (DEPRECATED)" + """Deprecated. Use the portdbapi findname method instead.""" + warnings.warn("The getname method of " + "portage.dbapi.porttree.portagetree is deprecated. " + "Use the portdbapi findname method instead.", + DeprecationWarning, stacklevel=2) if not pkgname: return "" mysplit = pkgname.split("/") @@ -1459,11 +1471,11 @@ def _async_manifest_fetchlist(portdb, repo_config, cp, cpv_list=None, @param cpv_list: list of ebuild cpv values for a Manifest @type cpv_list: list @param max_jobs: max number of futures to process concurrently (default - is multiprocessing.cpu_count()) + is portage.util.cpuinfo.get_cpu_count()) @type max_jobs: int @param max_load: max load allowed when scheduling a new future, otherwise schedule no more than 1 future at a time (default - is multiprocessing.cpu_count()) + is portage.util.cpuinfo.get_cpu_count()) @type max_load: int or float @param loop: event loop @type loop: EventLoop diff --git a/lib/portage/locks.py b/lib/portage/locks.py index a4e7ec53f..609c8b2dc 100644 --- a/lib/portage/locks.py +++ b/lib/portage/locks.py @@ -1,5 +1,5 @@ # portage: Lock management code -# Copyright 2004-2014 Gentoo Foundation +# Copyright 2004-2019 Gentoo Authors # Distributed under the terms of the GNU General Public License v2 __all__ = ["lockdir", "unlockdir", "lockfile", "unlockfile", \ @@ -20,6 +20,7 @@ from portage.exception import (DirectoryNotFound, FileNotFound, InvalidData, TryAgain, OperationNotPermitted, PermissionDenied, ReadOnlyFileSystem) from portage.util import writemsg +from portage.util.install_mask import _raise_exc from portage.localization import _ if sys.hexversion >= 0x3000000: @@ -106,7 +107,34 @@ def lockfile(mypath, wantnewlockfile=0, unlinkfile=0, If wantnewlockfile is True then this creates a lockfile in the parent directory as the file: '.' + basename + '.portage_lockfile'. """ + lock = None + while lock is None: + lock = _lockfile_iteration(mypath, wantnewlockfile=wantnewlockfile, + unlinkfile=unlinkfile, waiting_msg=waiting_msg, flags=flags) + if lock is None: + writemsg(_("lockfile removed by previous lock holder, retrying\n"), 1) + return lock + +def _lockfile_iteration(mypath, wantnewlockfile=False, unlinkfile=False, + waiting_msg=None, flags=0): + """ + Acquire a lock on mypath, without retry. Return None if the lockfile + was removed by previous lock holder (caller must retry). + + @param mypath: lock file path + @type mypath: str + @param wantnewlockfile: use a separate new lock file + @type wantnewlockfile: bool + @param unlinkfile: remove lock file prior to unlock + @type unlinkfile: bool + @param waiting_msg: message to show before blocking + @type waiting_msg: str + @param flags: lock flags (only supports os.O_NONBLOCK) + @type flags: int + @rtype: bool + @return: unlockfile tuple on success, None if retry is needed + """ if not mypath: raise InvalidData(_("Empty path given")) @@ -148,18 +176,17 @@ def lockfile(mypath, wantnewlockfile=0, unlinkfile=0, preexisting = os.path.exists(lockfilename) old_mask = os.umask(000) try: - try: - myfd = os.open(lockfilename, os.O_CREAT|os.O_RDWR, 0o660) - except OSError as e: - func_call = "open('%s')" % lockfilename - if e.errno == OperationNotPermitted.errno: - raise OperationNotPermitted(func_call) - elif e.errno == PermissionDenied.errno: - raise PermissionDenied(func_call) - elif e.errno == ReadOnlyFileSystem.errno: - raise ReadOnlyFileSystem(func_call) + while True: + try: + myfd = os.open(lockfilename, os.O_CREAT|os.O_RDWR, 0o660) + except OSError as e: + if e.errno in (errno.ENOENT, errno.ESTALE) and os.path.isdir(os.path.dirname(lockfilename)): + # Retry required for NFS (see bug 636798). + continue + else: + _raise_exc(e) else: - raise + break if not preexisting: try: @@ -272,14 +299,18 @@ def lockfile(mypath, wantnewlockfile=0, unlinkfile=0, raise - if isinstance(lockfilename, basestring) and \ - myfd != HARDLINK_FD and _fstat_nlink(myfd) == 0: - # The file was deleted on us... Keep trying to make one... - os.close(myfd) - writemsg(_("lockfile recurse\n"), 1) - lockfilename, myfd, unlinkfile, locking_method = lockfile( - mypath, wantnewlockfile=wantnewlockfile, unlinkfile=unlinkfile, - waiting_msg=waiting_msg, flags=flags) + if isinstance(lockfilename, basestring) and myfd != HARDLINK_FD and unlinkfile: + try: + removed = _lockfile_was_removed(myfd, lockfilename) + except Exception: + # Do not leak the file descriptor here. + os.close(myfd) + raise + else: + if removed: + # Removed by previous lock holder... Caller will retry... + os.close(myfd) + return None if myfd != HARDLINK_FD: @@ -298,6 +329,85 @@ def lockfile(mypath, wantnewlockfile=0, unlinkfile=0, writemsg(str((lockfilename, myfd, unlinkfile)) + "\n", 1) return (lockfilename, myfd, unlinkfile, locking_method) + +def _lockfile_was_removed(lock_fd, lock_path): + """ + Check if lock_fd still refers to a file located at lock_path, since + the file may have been removed by a concurrent process that held the + lock earlier. This implementation includes support for NFS, where + stat is not reliable for removed files due to the default file + attribute cache behavior ('ac' mount option). + + @param lock_fd: an open file descriptor for a lock file + @type lock_fd: int + @param lock_path: path of lock file + @type lock_path: str + @rtype: bool + @return: True if lock_path exists and corresponds to lock_fd, False otherwise + """ + try: + fstat_st = os.fstat(lock_fd) + except OSError as e: + if e.errno not in (errno.ENOENT, errno.ESTALE): + _raise_exc(e) + return True + + # Since stat is not reliable for removed files on NFS with the default + # file attribute cache behavior ('ac' mount option), create a temporary + # hardlink in order to prove that the file path exists on the NFS server. + hardlink_path = hardlock_name(lock_path) + try: + os.unlink(hardlink_path) + except OSError as e: + if e.errno not in (errno.ENOENT, errno.ESTALE): + _raise_exc(e) + try: + try: + os.link(lock_path, hardlink_path) + except OSError as e: + if e.errno not in (errno.ENOENT, errno.ESTALE): + _raise_exc(e) + return True + + hardlink_stat = os.stat(hardlink_path) + if hardlink_stat.st_ino != fstat_st.st_ino or hardlink_stat.st_dev != fstat_st.st_dev: + # Create another hardlink in order to detect whether or not + # hardlink inode numbers are expected to match. For example, + # inode numbers are not expected to match for sshfs. + inode_test = hardlink_path + '-inode-test' + try: + os.unlink(inode_test) + except OSError as e: + if e.errno not in (errno.ENOENT, errno.ESTALE): + _raise_exc(e) + try: + os.link(hardlink_path, inode_test) + except OSError as e: + if e.errno not in (errno.ENOENT, errno.ESTALE): + _raise_exc(e) + return True + else: + if not os.path.samefile(hardlink_path, inode_test): + # This implies that inode numbers are not expected + # to match for this file system, so use a simple + # stat call to detect if lock_path has been removed. + return not os.path.exists(lock_path) + finally: + try: + os.unlink(inode_test) + except OSError as e: + if e.errno not in (errno.ENOENT, errno.ESTALE): + _raise_exc(e) + return True + finally: + try: + os.unlink(hardlink_path) + except OSError as e: + if e.errno not in (errno.ENOENT, errno.ESTALE): + _raise_exc(e) + return False + + def _fstat_nlink(fd): """ @param fd: an open file descriptor diff --git a/lib/portage/package/ebuild/_config/special_env_vars.py b/lib/portage/package/ebuild/_config/special_env_vars.py index 70a9c83c1..c16d4d644 100644 --- a/lib/portage/package/ebuild/_config/special_env_vars.py +++ b/lib/portage/package/ebuild/_config/special_env_vars.py @@ -1,4 +1,4 @@ -# Copyright 2010-2018 Gentoo Foundation +# Copyright 2010-2019 Gentoo Authors # Distributed under the terms of the GNU General Public License v2 from __future__ import unicode_literals @@ -208,7 +208,6 @@ global_only_vars = frozenset([ ]) default_globals = { - 'ACCEPT_LICENSE': '* -@EULA', 'ACCEPT_PROPERTIES': '*', 'PORTAGE_BZIP2_COMMAND': 'bzip2', } diff --git a/lib/portage/package/ebuild/config.py b/lib/portage/package/ebuild/config.py index 45dd5841e..8e9a74760 100644 --- a/lib/portage/package/ebuild/config.py +++ b/lib/portage/package/ebuild/config.py @@ -1,4 +1,4 @@ -# Copyright 2010-2018 Gentoo Foundation +# Copyright 2010-2019 Gentoo Authors # Distributed under the terms of the GNU General Public License v2 from __future__ import unicode_literals @@ -2284,7 +2284,7 @@ class config(object): for curdb in mydbs: mysplit.extend(curdb.get('ACCEPT_LICENSE', '').split()) mysplit = prune_incremental(mysplit) - accept_license_str = ' '.join(mysplit) + accept_license_str = ' '.join(mysplit) or '* -@EULA' self.configlist[-1]['ACCEPT_LICENSE'] = accept_license_str self._license_manager.set_accept_license_str(accept_license_str) else: diff --git a/lib/portage/package/ebuild/doebuild.py b/lib/portage/package/ebuild/doebuild.py index 8e3683452..a104a4953 100644 --- a/lib/portage/package/ebuild/doebuild.py +++ b/lib/portage/package/ebuild/doebuild.py @@ -112,7 +112,8 @@ _ipc_phases = frozenset([ ]) # phases which execute in the global PID namespace -_global_pid_phases = frozenset(['preinst', 'postinst', 'prerm', 'postrm', 'config']) +_global_pid_phases = frozenset([ + 'config', 'depend', 'preinst', 'prerm', 'postinst', 'postrm']) # phases in which networking access is allowed _networked_phases = frozenset([ diff --git a/lib/portage/process.py b/lib/portage/process.py index 37e96879e..8951a4f7f 100644 --- a/lib/portage/process.py +++ b/lib/portage/process.py @@ -1,5 +1,5 @@ # portage.py -- core Portage functionality -# Copyright 1998-2018 Gentoo Authors +# Copyright 1998-2019 Gentoo Authors # Distributed under the terms of the GNU General Public License v2 @@ -482,7 +482,7 @@ def _exec(binary, mycommand, opt_name, fd_pipes, @param gid: Group ID to run the process under @type gid: Integer @param groups: Groups the Process should be in. - @type groups: Integer + @type groups: List @param uid: User ID to run the process under @type uid: Integer @param umask: an int representing a unix umask (see man chmod for umask details) @@ -579,15 +579,37 @@ def _exec(binary, mycommand, opt_name, fd_pipes, noiselevel=-1) else: if unshare_pid: - # pid namespace requires us to become init - fork_ret = os.fork() - if fork_ret != 0: - os.execv(portage._python_interpreter, [ + main_child_pid = os.fork() + if main_child_pid == 0: + # pid namespace requires us to become init + binary, myargs = portage._python_interpreter, [ portage._python_interpreter, os.path.join(portage._bin_path, 'pid-ns-init'), - '%s' % fork_ret, - ]) + _unicode_encode('' if uid is None else str(uid)), + _unicode_encode('' if gid is None else str(gid)), + _unicode_encode('' if groups is None else ','.join(str(group) for group in groups)), + _unicode_encode('' if umask is None else str(umask)), + _unicode_encode(','.join(str(fd) for fd in fd_pipes)), + binary] + myargs + uid = None + gid = None + groups = None + umask = None + else: + # Execute a supervisor process which will forward + # signals to init and forward exit status to the + # parent process. The supervisor process runs in + # the global pid namespace, so skip /proc remount + # and other setup that's intended only for the + # init process. + binary, myargs = portage._python_interpreter, [ + portage._python_interpreter, + os.path.join(portage._bin_path, + 'pid-ns-init'), str(main_child_pid)] + + os.execve(binary, myargs, env) + if unshare_mount: # mark the whole filesystem as slave to avoid # mounts escaping the namespace diff --git a/lib/portage/tests/resolver/ResolverPlayground.py b/lib/portage/tests/resolver/ResolverPlayground.py index 670c2a845..b8f251e0c 100644 --- a/lib/portage/tests/resolver/ResolverPlayground.py +++ b/lib/portage/tests/resolver/ResolverPlayground.py @@ -86,6 +86,7 @@ class ResolverPlayground(object): "basename", "bzip2", "cat", + "chgrp", "chmod", "chown", "cp", @@ -99,6 +100,7 @@ class ResolverPlayground(object): "mkdir", "mktemp", "mv", + "readlink", "rm", "sed", "sort", diff --git a/lib/portage/tests/util/test_install_mask.py b/lib/portage/tests/util/test_install_mask.py index f651eb4b7..6a29db79a 100644 --- a/lib/portage/tests/util/test_install_mask.py +++ b/lib/portage/tests/util/test_install_mask.py @@ -119,6 +119,42 @@ class InstallMaskTestCase(TestCase): ), ) ), + ( + '/usr/share/locale ' + '-/usr/share/locale/en* ' + '-/usr/share/locale/kf5_all_languages ' + '-/usr/share/locale/locale.alias', + ( + ( + 'usr/share/locale/en', + False, + ), + ( + 'usr/share/locale/en_GB', + False, + ), + ( + 'usr/share/locale/en/kf5_all_languages', + False, + ), + ( + 'usr/share/locale/locale.alias', + False, + ), + ( + 'usr/share/locale/es', + True, + ), + ( + 'usr/share/locale/fr', + True, + ), + ( + 'usr/share/locale', + True, + ), + ) + ), ) for install_mask_str, paths in cases: diff --git a/lib/portage/tests/util/test_socks5.py b/lib/portage/tests/util/test_socks5.py new file mode 100644 index 000000000..5db85b0a6 --- /dev/null +++ b/lib/portage/tests/util/test_socks5.py @@ -0,0 +1,211 @@ +# Copyright 2019 Gentoo Authors +# Distributed under the terms of the GNU General Public License v2 + +import functools +import platform +import shutil +import socket +import struct +import sys +import tempfile +import time + +import portage +from portage.tests import TestCase +from portage.util._eventloop.global_event_loop import global_event_loop +from portage.util import socks5 +from portage.const import PORTAGE_BIN_PATH + +try: + from http.server import BaseHTTPRequestHandler, HTTPServer +except ImportError: + from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer + +try: + from urllib.request import urlopen +except ImportError: + from urllib import urlopen + + +class _Handler(BaseHTTPRequestHandler): + + def __init__(self, content, *args, **kwargs): + self.content = content + BaseHTTPRequestHandler.__init__(self, *args, **kwargs) + + def do_GET(self): + doc = self.send_head() + if doc is not None: + self.wfile.write(doc) + + def do_HEAD(self): + self.send_head() + + def send_head(self): + doc = self.content.get(self.path) + if doc is None: + self.send_error(404, "File not found") + return None + + self.send_response(200) + self.send_header("Content-type", "text/plain") + self.send_header("Content-Length", len(doc)) + self.send_header("Last-Modified", self.date_time_string(time.time())) + self.end_headers() + return doc + + def log_message(self, fmt, *args): + pass + + +class AsyncHTTPServer(object): + def __init__(self, host, content, loop): + self._host = host + self._content = content + self._loop = loop + self.server_port = None + self._httpd = None + + def __enter__(self): + httpd = self._httpd = HTTPServer((self._host, 0), functools.partial(_Handler, self._content)) + self.server_port = httpd.server_port + self._loop.add_reader(httpd.socket.fileno(), self._httpd._handle_request_noblock) + return self + + def __exit__(self, exc_type, exc_value, exc_traceback): + if self._httpd is not None: + self._loop.remove_reader(self._httpd.socket.fileno()) + self._httpd.socket.close() + self._httpd = None + + +class AsyncHTTPServerTestCase(TestCase): + + @staticmethod + def _fetch_directly(host, port, path): + # NOTE: python2.7 does not have context manager support here + try: + f = urlopen('http://{host}:{port}{path}'.format( # nosec + host=host, port=port, path=path)) + return f.read() + finally: + if f is not None: + f.close() + + def test_http_server(self): + host = '127.0.0.1' + content = b'Hello World!\n' + path = '/index.html' + loop = global_event_loop() + for i in range(2): + with AsyncHTTPServer(host, {path: content}, loop) as server: + for j in range(2): + result = loop.run_until_complete(loop.run_in_executor(None, + self._fetch_directly, host, server.server_port, path)) + self.assertEqual(result, content) + + +class _socket_file_wrapper(portage.proxy.objectproxy.ObjectProxy): + """ + A file-like object that wraps a socket and closes the socket when + closed. Since python2.7 does not support socket.detach(), this is a + convenient way to have a file attached to a socket that closes + automatically (without resource warnings about unclosed sockets). + """ + + __slots__ = ('_file', '_socket') + + def __init__(self, socket, f): + object.__setattr__(self, '_socket', socket) + object.__setattr__(self, '_file', f) + + def _get_target(self): + return object.__getattribute__(self, '_file') + + def __getattribute__(self, attr): + if attr == 'close': + return object.__getattribute__(self, 'close') + return super(_socket_file_wrapper, self).__getattribute__(attr) + + def __enter__(self): + return self + + def close(self): + object.__getattribute__(self, '_file').close() + object.__getattribute__(self, '_socket').close() + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + +def socks5_http_get_ipv4(proxy, host, port, path): + """ + Open http GET request via socks5 proxy listening on a unix socket, + and return a file to read the response body from. + """ + s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + f = _socket_file_wrapper(s, s.makefile('rb', 1024)) + try: + s.connect(proxy) + s.send(struct.pack('!BBB', 0x05, 0x01, 0x00)) + vers, method = struct.unpack('!BB', s.recv(2)) + s.send(struct.pack('!BBBB', 0x05, 0x01, 0x00, 0x01)) + s.send(socket.inet_pton(socket.AF_INET, host)) + s.send(struct.pack('!H', port)) + reply = struct.unpack('!BBB', s.recv(3)) + if reply != (0x05, 0x00, 0x00): + raise AssertionError(repr(reply)) + struct.unpack('!B4sH', s.recv(7)) # contains proxied address info + s.send("GET {} HTTP/1.1\r\nHost: {}:{}\r\nAccept: */*\r\nConnection: close\r\n\r\n".format( + path, host, port).encode()) + headers = [] + while True: + headers.append(f.readline()) + if headers[-1] == b'\r\n': + return f + except Exception: + f.close() + raise + + +class Socks5ServerTestCase(TestCase): + + @staticmethod + def _fetch_via_proxy(proxy, host, port, path): + with socks5_http_get_ipv4(proxy, host, port, path) as f: + return f.read() + + def test_socks5_proxy(self): + + loop = global_event_loop() + + host = '127.0.0.1' + content = b'Hello World!' + path = '/index.html' + proxy = None + tempdir = tempfile.mkdtemp() + + try: + with AsyncHTTPServer(host, {path: content}, loop) as server: + + settings = { + 'PORTAGE_TMPDIR': tempdir, + 'PORTAGE_BIN_PATH': PORTAGE_BIN_PATH, + } + + try: + proxy = socks5.get_socks5_proxy(settings) + except NotImplementedError: + # bug 658172 for python2.7 + self.skipTest('get_socks5_proxy not implemented for {} {}.{}'.format( + platform.python_implementation(), *sys.version_info[:2])) + else: + loop.run_until_complete(socks5.proxy.ready()) + + result = loop.run_until_complete(loop.run_in_executor(None, + self._fetch_via_proxy, proxy, host, server.server_port, path)) + + self.assertEqual(result, content) + finally: + socks5.proxy.stop() + shutil.rmtree(tempdir) diff --git a/lib/portage/util/cpuinfo.py b/lib/portage/util/cpuinfo.py index 669e707b5..78b969f47 100644 --- a/lib/portage/util/cpuinfo.py +++ b/lib/portage/util/cpuinfo.py @@ -1,15 +1,44 @@ -# Copyright 2015 Gentoo Foundation +# Copyright 2015-2019 Gentoo Authors # Distributed under the terms of the GNU General Public License v2 __all__ = ['get_cpu_count'] +# Before you set out to change this function, figure out what you're really +# asking: +# +# - How many CPUs exist in this system (e.g. that the kernel is aware of?) +# This is 'getconf _NPROCESSORS_CONF' / get_nprocs_conf(3) +# In modern Linux, implemented by counting CPUs in /sys/devices/system/cpu/ +# +# - How many CPUs in this system are ONLINE right now? +# This is 'getconf _NPROCESSORS_ONLN' / get_nprocs(3) +# In modern Linux, implemented by parsing /sys/devices/system/cpu/online +# +# - How many CPUs are available to this program? +# This is 'nproc' / sched_getaffinity(2), which is implemented in modern +# Linux kernels by querying the kernel scheduler; This might not be available +# in some non-Linux systems! +# +# - How many CPUs are available to this thread? +# This is pthread_getaffinity_np(3) +# +# As a further warning, the results returned by this function can differ +# between runs, if altered by the scheduler or other external factors. def get_cpu_count(): """ - Try to obtain the number of CPUs available. + Try to obtain the number of CPUs available to this process. @return: Number of CPUs or None if unable to obtain. """ + try: + import os + # This was introduced in Python 3.3 only, but exists in Linux + # all the way back to the 2.5.8 kernel. + # This NOT available in FreeBSD! + return len(os.sched_getaffinity(0)) + except (ImportError, NotImplementedError, AttributeError): + pass try: import multiprocessing diff --git a/lib/portage/util/futures/executor/fork.py b/lib/portage/util/futures/executor/fork.py index 72844403c..add7b3c9e 100644 --- a/lib/portage/util/futures/executor/fork.py +++ b/lib/portage/util/futures/executor/fork.py @@ -7,13 +7,13 @@ __all__ = ( import collections import functools -import multiprocessing import os import sys import traceback from portage.util._async.AsyncFunction import AsyncFunction from portage.util.futures import asyncio +from portage.util.cpuinfo import get_cpu_count class ForkExecutor(object): @@ -24,7 +24,7 @@ class ForkExecutor(object): This is entirely driven by an event loop. """ def __init__(self, max_workers=None, loop=None): - self._max_workers = max_workers or multiprocessing.cpu_count() + self._max_workers = max_workers or get_cpu_count() self._loop = asyncio._wrap_loop(loop) self._submit_queue = collections.deque() self._running_tasks = {} diff --git a/lib/portage/util/futures/iter_completed.py b/lib/portage/util/futures/iter_completed.py index 31b5e0c78..9554b4338 100644 --- a/lib/portage/util/futures/iter_completed.py +++ b/lib/portage/util/futures/iter_completed.py @@ -2,11 +2,11 @@ # Distributed under the terms of the GNU General Public License v2 import functools -import multiprocessing from portage.util._async.AsyncTaskFuture import AsyncTaskFuture from portage.util._async.TaskScheduler import TaskScheduler from portage.util.futures import asyncio +from portage.util.cpuinfo import get_cpu_count def iter_completed(futures, max_jobs=None, max_load=None, loop=None): @@ -18,11 +18,11 @@ def iter_completed(futures, max_jobs=None, max_load=None, loop=None): @param futures: iterator of asyncio.Future (or compatible) @type futures: iterator @param max_jobs: max number of futures to process concurrently (default - is multiprocessing.cpu_count()) + is portage.util.cpuinfo.get_cpu_count()) @type max_jobs: int @param max_load: max load allowed when scheduling a new future, otherwise schedule no more than 1 future at a time (default - is multiprocessing.cpu_count()) + is portage.util.cpuinfo.get_cpu_count()) @type max_load: int or float @param loop: event loop @type loop: EventLoop @@ -47,11 +47,11 @@ def async_iter_completed(futures, max_jobs=None, max_load=None, loop=None): @param futures: iterator of asyncio.Future (or compatible) @type futures: iterator @param max_jobs: max number of futures to process concurrently (default - is multiprocessing.cpu_count()) + is portage.util.cpuinfo.get_cpu_count()) @type max_jobs: int @param max_load: max load allowed when scheduling a new future, otherwise schedule no more than 1 future at a time (default - is multiprocessing.cpu_count()) + is portage.util.cpuinfo.get_cpu_count()) @type max_load: int or float @param loop: event loop @type loop: EventLoop @@ -61,8 +61,8 @@ def async_iter_completed(futures, max_jobs=None, max_load=None, loop=None): """ loop = asyncio._wrap_loop(loop) - max_jobs = max_jobs or multiprocessing.cpu_count() - max_load = max_load or multiprocessing.cpu_count() + max_jobs = max_jobs or get_cpu_count() + max_load = max_load or get_cpu_count() future_map = {} def task_generator(): @@ -120,11 +120,11 @@ def iter_gather(futures, max_jobs=None, max_load=None, loop=None): @param futures: iterator of asyncio.Future (or compatible) @type futures: iterator @param max_jobs: max number of futures to process concurrently (default - is multiprocessing.cpu_count()) + is portage.util.cpuinfo.get_cpu_count()) @type max_jobs: int @param max_load: max load allowed when scheduling a new future, otherwise schedule no more than 1 future at a time (default - is multiprocessing.cpu_count()) + is portage.util.cpuinfo.get_cpu_count()) @type max_load: int or float @param loop: event loop @type loop: EventLoop diff --git a/lib/portage/util/install_mask.py b/lib/portage/util/install_mask.py index 32627eb05..037fc8bc3 100644 --- a/lib/portage/util/install_mask.py +++ b/lib/portage/util/install_mask.py @@ -1,15 +1,18 @@ -# Copyright 2018 Gentoo Foundation +# Copyright 2018-2019 Gentoo Authors # 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) + OperationNotPermitted, PermissionDenied, ReadOnlyFileSystem, FileNotFound) from portage.util import normalize_path if sys.hexversion >= 0x3000000: @@ -18,13 +21,82 @@ 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 """ - self._install_mask = install_mask.split() + # 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): """ @@ -34,13 +106,11 @@ class InstallMask(object): @return: True if path matches INSTALL_MASK, False otherwise """ ret = False - for pattern in self._install_mask: - # if pattern starts with -, possibly exclude this path - is_inclusive = not pattern.startswith('-') - if not is_inclusive: - pattern = pattern[1:] + + for pattern_obj in self._iter_relevant_patterns(path): + is_inclusive, pattern = pattern_obj.is_inclusive, pattern_obj.pattern # absolute path pattern - if pattern.startswith('/'): + if pattern_obj.leading_slash: # handle trailing slash for explicit directory match if path.endswith('/'): pattern = pattern.rstrip('/') + '/' @@ -60,6 +130,7 @@ _exc_map = { errno.ENOENT: FileNotFound, errno.EPERM: OperationNotPermitted, errno.EACCES: PermissionDenied, + errno.EROFS: ReadOnlyFileSystem, } diff --git a/lib/portage/util/socks5.py b/lib/portage/util/socks5.py index 74b0714eb..59e6699ec 100644 --- a/lib/portage/util/socks5.py +++ b/lib/portage/util/socks5.py @@ -1,13 +1,18 @@ # SOCKSv5 proxy manager for network-sandbox -# Copyright 2015 Gentoo Foundation +# Copyright 2015-2019 Gentoo Authors # Distributed under the terms of the GNU General Public License v2 +import errno import os import signal +import socket +import portage.data from portage import _python_interpreter from portage.data import portage_gid, portage_uid, userpriv_groups from portage.process import atexit_register, spawn +from portage.util.futures.compat_coroutine import coroutine +from portage.util.futures import asyncio class ProxyManager(object): @@ -36,9 +41,16 @@ class ProxyManager(object): self.socket_path = os.path.join(settings['PORTAGE_TMPDIR'], '.portage.%d.net.sock' % os.getpid()) server_bin = os.path.join(settings['PORTAGE_BIN_PATH'], 'socks5-server.py') + spawn_kwargs = {} + # The portage_uid check solves EPERM failures in Travis CI. + if portage.data.secpass > 1 and os.geteuid() != portage_uid: + spawn_kwargs.update( + uid=portage_uid, + gid=portage_gid, + groups=userpriv_groups, + umask=0o077) self._pids = spawn([_python_interpreter, server_bin, self.socket_path], - returnpid=True, uid=portage_uid, gid=portage_gid, - groups=userpriv_groups, umask=0o077) + returnpid=True, **spawn_kwargs) def stop(self): """ @@ -60,6 +72,36 @@ class ProxyManager(object): return self.socket_path is not None + @coroutine + def ready(self): + """ + Wait for the proxy socket to become ready. This method is a coroutine. + """ + + while True: + try: + wait_retval = os.waitpid(self._pids[0], os.WNOHANG) + except OSError as e: + if e.errno == errno.EINTR: + continue + raise + + if wait_retval is not None and wait_retval != (0, 0): + raise OSError(3, 'No such process') + + try: + s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + s.connect(self.socket_path) + except EnvironmentError as e: + if e.errno != errno.ENOENT: + raise + yield asyncio.sleep(0.2) + else: + break + finally: + s.close() + + proxy = ProxyManager() diff --git a/man/make.conf.5 b/man/make.conf.5 index 523501300..35fe6a4ab 100644 --- a/man/make.conf.5 +++ b/man/make.conf.5 @@ -1,4 +1,4 @@ -.TH "MAKE.CONF" "5" "Nov 2018" "Portage VERSION" "Portage" +.TH "MAKE.CONF" "5" "Feb 2019" "Portage VERSION" "Portage" .SH "NAME" make.conf \- custom settings for Portage .SH "SYNOPSIS" @@ -64,7 +64,7 @@ file (see \fBportage\fR(5)). In addition to license and group names, the for further information: \fIhttps://www.gentoo.org/glep/glep-0023.html\fR. .br -Defaults to the value of * -@EULA. +Defaults to the value defined in the profile. .br .I Examples: .nf @@ -72,7 +72,7 @@ Defaults to the value of * -@EULA. ACCEPT_LICENSE="-* @FREE" # As before, but exclude the "Artistic" license ACCEPT_LICENSE="-* @FREE -Artistic" -# Accept any license except those in the EULA license group (default) +# Accept any license except those in the EULA license group ACCEPT_LICENSE="* -@EULA" .fi .TP @@ -662,7 +662,7 @@ class build_ext(_build_ext): setup( name = 'portage', - version = '2.3.55', + version = '2.3.62', url = 'https://wiki.gentoo.org/wiki/Project:Portage', author = 'Gentoo Portage Development Team', author_email = 'dev-portage@gentoo.org', |