From 6d50b03e6c57cd6263d185237eefd351d9b82d87 Mon Sep 17 00:00:00 2001 From: Fabian Groffen Date: Sat, 29 Sep 2007 20:29:54 +0000 Subject: [PATCH] Merged from trunk 7838:7877 | 7839 | someone seriously screwed the pooch on whitespacing here so | | SpankMan | clean it all up | | 7840 | Fix a typo. | | zmedico | | | 7842 | align the filename and line numbers in the output of the | | SpankMan | stack dump so that it is much easier to read | | 7844 | Display a short notification when an existing | | zmedico | ${T}/environment sourced since it is easy to ignore and | | | ignorance of it often leads to mysterious problems and | | | general confusion. This message should not show in cases | | | where it is obviously irrelevant, FEATURES=noauto, or | | | PORTAGE_QUIET=1. | | 7846 | Do the stale env check/warning after the digest/manifest | | zmedico | phase when necessary. | | 7848 | Bug #193486 - Use xargs to avoid 'bash: /bin/rm: Argument | | zmedico | list too long' errors. This patch uses a weird tr '\001' | | | '\000' workaround since bash doesn't echo null bytes like | | | one might expect. | | 7850 | Bug #193486 - Use echo -n and properly quote the array | | zmedico | argument. | | 7852 | with help from Donnie and Ulrich, use sed to extract the | | SpankMan | statement that led to the call to die so we can print it | | | out in our error message | | 7853 | Bug #194025 - Lock /var/db/pkg in post_merge() during the | | zmedico | "Regenerating GNU info directory index" routine. | | 7856 | Ignore ENOTDIR from unmerge unlink calls. | | zmedico | | | 7858 | Move a newline to fix formatting. | | zmedico | | | 7861 | Simplify update_eclasses() a little. | | zmedico | | | 7864 | Move prelink tempfile cleanup to the finally block and | | zmedico | remove unused locking code. | | 7866 | Handle KeyError thrown from aux_get(). | | zmedico | | | 7869 | Bug #194081 - Add EBUSY to the list of ignored errnos for | | zmedico | unlink and rmdir calls during unmerge. | | 7871 | Bug #193548 - When emerge spawns find to search for config | | zmedico | updates, show the find output if there is an error. | | 7873 | When scanning for config updates, treat a symlink to an | | zmedico | existing directory as if it's just a normal directory. | | 7875 | Bug #193548 - When scanning for config updates and an error | | zmedico | occurs, send find's stdout to /dev/null so that only an | | | error message is shown. | | 7877 | Bug #194095 - Validate dispatch-conf input in order to | | zmedico | prevent spurious input characters from being interpreted as | | | user input (which can be quite confusing and gives an "out | | | of control" feeling). | svn path=/main/branches/prefix/; revision=7883 --- bin/dispatch-conf | 119 ++++++++++++++------------ bin/ebuild | 28 ++++++ bin/ecompress | 3 +- bin/isolated-functions.sh | 159 +++++++++++++++++++++-------------- man/make.conf.5 | 2 +- pym/emerge/__init__.py | 38 +++++++-- pym/portage/checksum.py | 8 +- pym/portage/dbapi/vartree.py | 12 ++- pym/portage/eclass_cache.py | 13 ++- 9 files changed, 235 insertions(+), 147 deletions(-) diff --git a/bin/dispatch-conf b/bin/dispatch-conf index 439af5d45..3701f587a 100755 --- a/bin/dispatch-conf +++ b/bin/dispatch-conf @@ -202,8 +202,10 @@ class dispatch: # Interactively process remaining # - for conf in confs: - count = count + 1 + valid_input = "qhtnmlezu" + + for conf in confs: + count = count + 1 newconf = conf['new'] mrgconf = re.sub(r'\._cfg', '._mrg', newconf) @@ -222,58 +224,67 @@ class dispatch: print '>> (%i of %i) -- %s' % (count, len(confs), conf ['current']) print '>> q quit, h help, n next, e edit-new, z zap-new, u use-new\n m merge, t toggle-merge, l look-merge: ', - c = getch () - - if c == 'q': - sys.exit (0) - if c == 'h': - self.do_help () - continue - elif c == 't': - if newconf == mrgconf: - newconf = conf['new'] - elif os.path.exists(mrgconf): - newconf = mrgconf - continue - elif c == 'n': - break - elif c == 'm': - merged = SCRATCH_DIR+"/"+os.path.basename(conf['current']) - print - ret = os.system (self.options['merge'] % (merged, conf ['current'], newconf)) - if ret: - print "Failure running 'merge' command" - continue - shutil.copyfile(merged, mrgconf) - os.remove(merged) - mystat = os.lstat(conf['new']) - os.chmod(mrgconf, mystat[ST_MODE]) - os.chown(mrgconf, mystat[ST_UID], mystat[ST_GID]) - newconf = mrgconf - continue - elif c == 'l': - show_new_diff = 1 - continue - elif c == 'e': - if not os.environ.has_key('EDITOR'): - os.environ['EDITOR']='nano' - os.system(os.environ['EDITOR'] + ' ' + newconf) - continue - elif c == 'z': - os.unlink(conf['new']) - if os.path.exists(mrgconf): - os.unlink(mrgconf) - break - elif c == 'u': - self.replace(newconf, conf ['current']) - self.post_process(conf['current']) - if newconf == mrgconf: - os.unlink(conf['new']) - elif os.path.exists(mrgconf): - os.unlink(mrgconf) - break - else: - continue + # In some cases getch() will return some spurious characters + # that do not represent valid input. If we don't validate the + # input then the spurious characters can cause us to jump + # back into the above "diff" command immediatly after the user + # has exited it (which can be quite confusing and gives an + # "out of control" feeling). + while True: + c = getch() + if c in valid_input: + break + + if c == 'q': + sys.exit (0) + if c == 'h': + self.do_help () + continue + elif c == 't': + if newconf == mrgconf: + newconf = conf['new'] + elif os.path.exists(mrgconf): + newconf = mrgconf + continue + elif c == 'n': + break + elif c == 'm': + merged = SCRATCH_DIR+"/"+os.path.basename(conf['current']) + print + ret = os.system (self.options['merge'] % (merged, conf ['current'], newconf)) + if ret: + print "Failure running 'merge' command" + continue + shutil.copyfile(merged, mrgconf) + os.remove(merged) + mystat = os.lstat(conf['new']) + os.chmod(mrgconf, mystat[ST_MODE]) + os.chown(mrgconf, mystat[ST_UID], mystat[ST_GID]) + newconf = mrgconf + continue + elif c == 'l': + show_new_diff = 1 + continue + elif c == 'e': + if not os.environ.has_key('EDITOR'): + os.environ['EDITOR']='nano' + os.system(os.environ['EDITOR'] + ' ' + newconf) + continue + elif c == 'z': + os.unlink(conf['new']) + if os.path.exists(mrgconf): + os.unlink(mrgconf) + break + elif c == 'u': + self.replace(newconf, conf ['current']) + self.post_process(conf['current']) + if newconf == mrgconf: + os.unlink(conf['new']) + elif os.path.exists(mrgconf): + os.unlink(mrgconf) + break + else: + raise AssertionError("Invalid Input: %s" % c) if auto_zapped: print diff --git a/bin/ebuild b/bin/ebuild index d29333ef1..2dd02fe7c 100755 --- a/bin/ebuild +++ b/bin/ebuild @@ -138,8 +138,36 @@ if "test" in pargs and "test" not in tmpsettings.features: tmpsettings["FEATURES"] = " ".join(tmpsettings.features) tmpsettings.backup_changes("FEATURES") +build_dir_phases = set(["setup", "unpack", "compile", + "test", "install", "package", "rpm"]) + +def stale_env_warning(): + if "clean" not in pargs and \ + "noauto" not in tmpsettings.features and \ + tmpsettings.get("PORTAGE_QUIET") != "1" and \ + build_dir_phases.intersection(pargs): + portage.doebuild_environment(ebuild, "setup", portage.root, + tmpsettings, debug, 1, portage.portdb) + env_filename = os.path.join(tmpsettings["T"], "environment") + if os.path.exists(env_filename): + msg = ("Existing ${T}/environment for '%s' will be sourced. " + \ + "Run 'clean' to start with a fresh environment.") % \ + (tmpsettings["PF"], ) + from textwrap import wrap + msg = wrap(msg, 70) + for x in msg: + portage.writemsg(">>> %s\n" % x) + +checked_for_stale_env = False + for arg in pargs: try: + if not checked_for_stale_env and arg not in ("digest","manifest"): + # This has to go after manifest generation since otherwise + # aux_get() might fail due to invalid ebuild digests. + stale_env_warning() + checked_for_stale_env = True + if arg == "digest" and force: discard_digests(ebuild, tmpsettings, portage.portdb) a = portage.doebuild(ebuild, arg, portage.root, tmpsettings, diff --git a/bin/ecompress b/bin/ecompress index aeb16eab8..13d6bd09d 100755 --- a/bin/ecompress +++ b/bin/ecompress @@ -67,7 +67,8 @@ case $1 in # delete it so that the compressor doesn't whine (bzip2 will # complain and skip, gzip will prompt for input) suffix=$(ecompress --suffix) - [[ -n ${suffix} ]] && rm -f "${@/%/${suffix}}" + [[ -n ${suffix} ]] && echo -n "${@/%/${suffix}$'\001'}" | \ + tr '\001' '\000' | ${XARGS} -0 rm -f # Finally, let's actually do some real work exec "${PORTAGE_COMPRESS}" ${PORTAGE_COMPRESS_FLAGS} "$@" ;; diff --git a/bin/isolated-functions.sh b/bin/isolated-functions.sh index 15fc17580..c1636b215 100755 --- a/bin/isolated-functions.sh +++ b/bin/isolated-functions.sh @@ -12,72 +12,102 @@ alias restore_IFS='if [ "${old_IFS:-unset}" != "unset" ]; then IFS="${old_IFS}"; shopt -s extdebug -# usage- first arg is the number of funcs on the stack to ignore. -# defaults to 1 (ignoring dump_trace) +# dump_trace([number of funcs on stack to skip], +# [whitespacing for filenames], +# [whitespacing for line numbers]) dump_trace() { - local funcname="" sourcefile="" lineno="" n e s="yes" - - declare -i strip=1 - - if [[ -n $1 ]]; then - strip=$(( $1 )) - fi - - eerror "Call stack:" - for (( n = ${#FUNCNAME[@]} - 1, p = ${#BASH_ARGV[@]} ; n > $strip ; n-- )) ; do - funcname=${FUNCNAME[${n} - 1]} - sourcefile=$(basename ${BASH_SOURCE[${n}]}) - lineno=${BASH_LINENO[${n} - 1]} - # Display function arguments - args= - if [[ -n "${BASH_ARGV[@]}" ]]; then - for (( j = 1 ; j <= ${BASH_ARGC[${n} - 1]} ; ++j )); do - newarg=${BASH_ARGV[$(( p - j - 1 ))]} - args="${args:+${args} }'${newarg}'" - done - (( p -= ${BASH_ARGC[${n} - 1]} )) - fi - eerror " ${sourcefile}, line ${lineno}: Called ${funcname}${args:+ ${args}}" - done + local funcname="" sourcefile="" lineno="" n e s="yes" + declare -i strip=${1:-1} + local filespacing=$2 linespacing=$3 + + eerror "Call stack:" + for (( n = ${#FUNCNAME[@]} - 1, p = ${#BASH_ARGV[@]} ; n > ${strip} ; n-- )) ; do + funcname=${FUNCNAME[${n} - 1]} + sourcefile=$(basename ${BASH_SOURCE[${n}]}) + lineno=${BASH_LINENO[${n} - 1]} + # Display function arguments + args= + if [[ -n "${BASH_ARGV[@]}" ]]; then + for (( j = 1 ; j <= ${BASH_ARGC[${n} - 1]} ; ++j )); do + newarg=${BASH_ARGV[$(( p - j - 1 ))]} + args="${args:+${args} }'${newarg}'" + done + (( p -= ${BASH_ARGC[${n} - 1]} )) + fi + eerror " $(printf "%${filespacing}s" "${sourcefile}"), line $(printf "%${linespacing}s" "${lineno}"): Called ${funcname}${args:+ ${args}}" + done } diefunc() { - local funcname="$1" lineno="$2" exitcode="$3" - shift 3 - eerror - eerror "ERROR: $CATEGORY/$PF failed." - dump_trace 2 - eerror " ${BASH_SOURCE[1]##*/}, line ${BASH_LINENO[0]}: Called die" - eerror - eerror "${*:-(no error message)}" - eerror "If you need support, post the topmost build error, and the call stack if relevant." - [ -n "${PORTAGE_LOG_FILE}" ] && \ - eerror "A complete build log is located at '${PORTAGE_LOG_FILE}'." - if [ -n "${EBUILD_OVERLAY_ECLASSES}" ] ; then - eerror "This ebuild used the following eclasses from overlays:" - local x - for x in ${EBUILD_OVERLAY_ECLASSES} ; do - eerror " ${x}" - done - fi - if [ "${EBUILD#${PORTDIR}/}" == "${EBUILD}" ] ; then - local overlay=${EBUILD%/*} - overlay=${overlay%/*} - overlay=${overlay%/*} - eerror "This ebuild is from an overlay: '${overlay}/'" - fi - eerror - - if [ "${EBUILD_PHASE/depend}" == "${EBUILD_PHASE}" ]; then - local x - for x in $EBUILD_DEATH_HOOKS; do - ${x} "$@" >&2 1>&2 - done - fi - - # subshell die support - kill -s SIGTERM ${EBUILD_MASTER_PID} - exit 1 + local funcname="$1" lineno="$2" exitcode="$3" + shift 3 + + local n filespacing=0 linespacing=0 + # setup spacing to make output easier to read + for ((n = ${#FUNCNAME[@]} - 1; n >= 0; --n)); do + sourcefile=${BASH_SOURCE[${n}]} sourcefile=${sourcefile##*/} + lineno=${BASH_LINENO[${n}]} + ((filespacing < ${#sourcefile})) && filespacing=${#sourcefile} + ((linespacing < ${#lineno})) && linespacing=${#lineno} + done + + eerror + eerror "ERROR: $CATEGORY/$PF failed." + dump_trace 2 ${filespacing} ${linespacing} + eerror " $(printf "%${filespacing}s" "${BASH_SOURCE[1]##*/}"), line $(printf "%${linespacing}s" "${BASH_LINENO[0]}"): Called die" + eerror "The specific snippet of code:" + # This scans the file that called die and prints out the logic that + # ended in the call to die. This really only handles lines that end + # with '|| die' and any preceding lines with line continuations (\). + # This tends to be the most common usage though, so let's do it. + # Due to the usage of appending to the hold space (even when empty), + # we always end up with the first line being a blank (thus the 2nd sed). + sed -n \ + -e "# When we get to the line that failed, append it to the + # hold space, move the hold space to the pattern space, + # then print out the pattern space and quit immediately + ${BASH_LINENO[0]}{H;g;p;q}" \ + -e '# If this line ends with a line continuation, append it + # to the hold space + /\\$/H' \ + -e '# If this line does not end with a line continuation, + # erase the line and set the hold buffer to it (thus + # erasing the hold buffer in the process) + /[^\]$/{s:^.*$::;h}' \ + ${BASH_SOURCE[1]} \ + | sed -e '1d' -e 's:^:RETAIN-LEADING-SPACE:' \ + | while read -r n ; do eerror " ${n#RETAIN-LEADING-SPACE}" ; done + eerror " The die message:" + eerror " ${*:-(no error message)}" + eerror + eerror "If you need support, post the topmost build error, and the call stack if relevant." + [[ -n ${PORTAGE_LOG_FILE} ]] \ + && eerror "A complete build log is located at '${PORTAGE_LOG_FILE}'." + if [[ -n ${EBUILD_OVERLAY_ECLASSES} ]] ; then + eerror "This ebuild used the following eclasses from overlays:" + local x + for x in ${EBUILD_OVERLAY_ECLASSES} ; do + eerror " ${x}" + done + fi + if [[ "${EBUILD#${PORTDIR}/}" == "${EBUILD}" ]] ; then + local overlay=${EBUILD%/*} + overlay=${overlay%/*} + overlay=${overlay%/*} + eerror "This ebuild is from an overlay: '${overlay}/'" + fi + eerror + + if [[ "${EBUILD_PHASE/depend}" == "${EBUILD_PHASE}" ]] ; then + local x + for x in $EBUILD_DEATH_HOOKS; do + ${x} "$@" >&2 1>&2 + done + fi + + # subshell die support + kill -s SIGTERM ${EBUILD_MASTER_PID} + exit 1 } quiet_mode() { @@ -289,8 +319,9 @@ set_colors() { # Adjust COLS so that eend works properly on a standard BSD console. [ "${TERM}" = "cons25" ] && COLS=$((${COLS} - 1)) - ENDCOL=$'\e[A\e['${COLS}'C' # Now, ${ENDCOL} will move us to the end of the - # column; irregardless of character width + # Now, ${ENDCOL} will move us to the end of the + # column; irregardless of character width + ENDCOL=$'\e[A\e['${COLS}'C' if [ -n "${PORTAGE_COLORMAP}" ] ; then eval ${PORTAGE_COLORMAP} else diff --git a/man/make.conf.5 b/man/make.conf.5 index 02bc326f1..ee7493bd1 100644 --- a/man/make.conf.5 +++ b/man/make.conf.5 @@ -138,7 +138,7 @@ SRC_URI digests are correct. This feature also affects digest generation via \fIdigest\fR feature is enabled). Existing digests for files that do not exist in ${DISTDIR} will be automatically assumed even when \fIassume\-digests\fR is not enabled. If a file exists in ${DISTDIR} but it's size does not match the -existing digest, the digest will be regenerated regardless of the whether or +existing digest, the digest will be regenerated regardless of whether or not \fIassume\-digests\fR is enabled. The \fBebuild\fR(1) \fBdigest\fR command has a \fB\-\-force\fR option that can be used to force regeneration of digests. .TP diff --git a/pym/emerge/__init__.py b/pym/emerge/__init__.py index eff84886f..66c7c480c 100644 --- a/pym/emerge/__init__.py +++ b/pym/emerge/__init__.py @@ -1924,6 +1924,7 @@ class depgraph(object): print "For more information, see MASKED PACKAGES section in the emerge man page or " print "refer to the Gentoo Handbook." else: + print alleb = bindb.match(x) if alleb: chost = pkgsettings["CHOST"] @@ -1933,7 +1934,7 @@ class depgraph(object): if chost != pkg_chost: mreasons.append("CHOST: %s" % pkg_chost) print "- "+p+" (masked by: "+", ".join(mreasons)+")" - print "\n!!! "+red("There are no packages available to satisfy: ")+green(xinfo) + print "!!! "+red("There are no packages available to satisfy: ")+green(xinfo) print "!!! Either add a suitable binary package or compile from an ebuild." else: print "\nemerge: there are no ebuilds to satisfy "+green(xinfo)+"." @@ -4504,9 +4505,17 @@ def post_emerge(trees, mtimedb, retval): else: mod_echo.finalize() - if "noinfo" not in settings.features: - chk_updated_info_files(normalize_path(target_root + EPREFIX), infodirs, info_mtimes, retval) - chk_updated_cfg_files(normalize_path(target_root + EPREFIX), config_protect) + vdb_path = os.path.join(target_root, portage.VDB_PATH) + portage.util.ensure_dirs(vdb_path) + vdb_lock = portage.locks.lockdir(vdb_path) + try: + if "noinfo" not in settings.features: + chk_updated_info_files(target_root + EPREFIX, infodirs, info_mtimes, retval) + mtimedb.commit() + finally: + portage.locks.unlockdir(vdb_lock) + + chk_updated_cfg_files(target_root + EPREFIX, config_protect) display_news_notification(trees) @@ -4519,8 +4528,7 @@ def post_emerge(trees, mtimedb, retval): print colorize("WARN", " * ") + " - %s" % f print "Use " + colorize("GOOD", "revdep-rebuild") + " to rebuild packages using these libraries" print "and then remerge the packages listed above." - - mtimedb.commit() + sys.exit(retval) @@ -4534,15 +4542,27 @@ def chk_updated_cfg_files(target_root, config_protect): mymode = os.lstat(x).st_mode except OSError: continue + if stat.S_ISLNK(mymode): + # We want to treat it like a directory if it + # is a symlink to an existing directory. + try: + real_mode = os.stat(x).st_mode + if stat.S_ISDIR(real_mode): + mymode = real_mode + except OSError: + pass if stat.S_ISDIR(mymode): mycommand = "find '%s' -iname '._cfg????_*'" % x else: mycommand = "find '%s' -maxdepth 1 -iname '._cfg????_%s'" % \ os.path.split(x.rstrip(os.path.sep)) - a = commands.getstatusoutput(mycommand + \ - " ! -iname '.*~' ! -iname '.*.bak' -print0") + mycommand += " ! -iname '.*~' ! -iname '.*.bak' -print0" + a = commands.getstatusoutput(mycommand) if a[0] != 0: - print >> sys.stderr, " " + bad("*")+ " error scanning '%s'" % x + sys.stderr.write(" %s error scanning '%s': " % (bad("*"), x)) + sys.stderr.flush() + # Show the error message alone, sending stdout to /dev/null. + os.system(mycommand + " 1>/dev/null") else: files = a[1].split('\0') # split always produces an empty string as the last element diff --git a/pym/portage/checksum.py b/pym/portage/checksum.py index c663b68d9..77716aefc 100644 --- a/pym/portage/checksum.py +++ b/pym/portage/checksum.py @@ -10,7 +10,6 @@ import stat import tempfile import portage.exception import portage.process -import portage.locks import commands import md5, sha @@ -200,7 +199,6 @@ def perform_checksum(filename, hashname="MD5", calc_prelink=0): global prelink_capable myfilename = filename[:] prelink_tmpfile = None - mylock = None try: if calc_prelink and prelink_capable: # Create non-prelinked temporary file to checksum. @@ -226,6 +224,8 @@ def perform_checksum(filename, hashname="MD5", calc_prelink=0): if e.errno == errno.ENOENT: raise portage.exception.FileNotFound(myfilename) raise + return myhash, mysize + finally: if prelink_tmpfile: try: os.unlink(prelink_tmpfile) @@ -233,10 +233,6 @@ def perform_checksum(filename, hashname="MD5", calc_prelink=0): if e.errno != errno.ENOENT: raise del e - return myhash, mysize - finally: - if mylock: - portage.locks.unlockfile(mylock) def perform_multiple_checksums(filename, hashes=["MD5"], calc_prelink=0): """ diff --git a/pym/portage/dbapi/vartree.py b/pym/portage/dbapi/vartree.py index 064db4d2f..dc1c23f14 100644 --- a/pym/portage/dbapi/vartree.py +++ b/pym/portage/dbapi/vartree.py @@ -1184,7 +1184,13 @@ class dblink(object): #process symlinks second-to-last, directories last. mydirs = [] - ignored_unlink_errnos = (errno.ENOENT, errno.EISDIR) + ignored_unlink_errnos = ( + errno.EBUSY, errno.ENOENT, + errno.ENOTDIR, errno.EISDIR) + ignored_rmdir_errnos = ( + errno.EEXIST, errno.ENOTEMPTY, + errno.EBUSY, errno.ENOENT, + errno.ENOTDIR, errno.EISDIR) modprotect = os.path.join(self.vartree.root, "lib/modules/") def unlink(file_name, lstatobj): @@ -1349,9 +1355,7 @@ class dblink(object): bsd_chflags.chflags(parent_name, pflags) show_unmerge("<<<", "", "dir", obj) except EnvironmentError, e: - if e.errno not in (errno.ENOENT, - errno.EEXIST, errno.ENOTEMPTY, - errno.ENOTDIR): + if e.errno not in ignored_rmdir_errnos: raise if e.errno != errno.ENOENT: show_unmerge("---", "!empty", "dir", obj) diff --git a/pym/portage/eclass_cache.py b/pym/portage/eclass_cache.py index 8fac8aec4..806505757 100644 --- a/pym/portage/eclass_cache.py +++ b/pym/portage/eclass_cache.py @@ -42,23 +42,20 @@ class cache(object): self.eclasses = {} self._eclass_locations = {} eclass_len = len(".eclass") + ignored_listdir_errnos = (errno.ENOENT, errno.ENOTDIR) for x in [normalize_path(os.path.join(y,"eclass")) for y in self.porttrees]: - eclass_filenames = [] try: - for y in os.listdir(x): - if y.endswith(".eclass"): - eclass_filenames.append(y) + eclass_filenames = os.listdir(x) except OSError, e: - if e.errno == errno.ENOENT: - del e - continue - elif e.errno == errno.ENOTDIR: + if e.errno in ignored_listdir_errnos: del e continue elif e.errno == PermissionDenied.errno: raise PermissionDenied(x) raise for y in eclass_filenames: + if not y.endswith(".eclass"): + continue try: mtime = long(os.stat(os.path.join(x, y)).st_mtime) except OSError: -- 2.26.2