Bug #200313 - Detect and report when an ebuild phase
authorZac Medico <zmedico@gentoo.org>
Mon, 26 Nov 2007 11:23:17 +0000 (11:23 -0000)
committerZac Medico <zmedico@gentoo.org>
Mon, 26 Nov 2007 11:23:17 +0000 (11:23 -0000)
exits unexpectedly. This is type of behavior is known
to be triggered by things such as failed variable
assignments (bug #190128) or bad substitution errors
(bug #200313).

We use a EBUILD_EXIT_STATUS_FILE environment variable
to specify a file that the shell code is supposed to
create when it exits in a normal manner. If the file
does not get created like it's supposed to be then we
can conclude that the shell has exited in some
unexpected way.

svn path=/main/trunk/; revision=8682

bin/ebuild.sh
bin/isolated-functions.sh
bin/misc-functions.sh
pym/portage/__init__.py

index 46651e76aa5ea0be7d0ac6f76a5923249abe8837..a6969c9d86bd108f00b9242723a8f0bd4001a738 100755 (executable)
@@ -1726,7 +1726,7 @@ if [ -n "${EBUILD_SH_ARGS}" ] ; then
                        9>&-
                fi
                set +f
-               #make sure it is writable by our group:
+               touch "${EBUILD_EXIT_STATUS_FILE}" &>/dev/null
                exit 0
                ;;
        *)
@@ -1737,6 +1737,7 @@ if [ -n "${EBUILD_SH_ARGS}" ] ; then
                exit 1
                ;;
        esac
+       touch "${EBUILD_EXIT_STATUS_FILE}" &>/dev/null
 fi
 
 # Save the env only for relevant phases.
index ae418389d15871078d54c03e8c55944642009eac..9eaf9bffb61dd4b4e00f37108cd304afb8dd3f41 100755 (executable)
@@ -124,6 +124,8 @@ diefunc() {
                done
        fi
 
+       touch "${EBUILD_EXIT_STATUS_FILE}" &>/dev/null
+
        # subshell die support
        kill -s SIGTERM ${EBUILD_MASTER_PID}
        exit 1
@@ -416,7 +418,8 @@ save_ebuild_env() {
 
                # portage config variables and variables set directly by portage
                unset BAD BRACKET BUILD_PREFIX COLS \
-                       DISTCC_DIR DISTDIR DOC_SYMLINKS_DIR EBUILD_MASTER_PID \
+                       DISTCC_DIR DISTDIR DOC_SYMLINKS_DIR \
+                       EBUILD_EXIT_STATUS_FILE EBUILD_MASTER_PID \
                        ECLASSDIR ECLASS_DEPTH ENDCOL FAKEROOTKEY FEATURES \
                        GOOD HILITE HOME IMAGE \
                        KV LAST_E_CMD LAST_E_LEN LD_PRELOAD MOPREFIX \
index 2e860453e6ab4fd04fb6b810c789e7c4dc396850..450e71631beb301eccec8d9ec1f6f90b02c9cbbd 100755 (executable)
@@ -593,4 +593,6 @@ if [ -n "${MISC_FUNCTIONS_ARGS}" ]; then
        done
 fi
 
+touch "${EBUILD_EXIT_STATUS_FILE}" &>/dev/null
+
 :
index 28d5eeed5ac11f666aa280271371a65bdd358aaa..04b73b1b8e49ea79fd0de6c1715ac9bc31b1551b 100644 (file)
@@ -3560,8 +3560,18 @@ def spawnebuild(mydo,actionmap,mysettings,debug,alwaysdep=0,logfile=None):
                                return retval
        kwargs = actionmap[mydo]["args"]
        mysettings["EBUILD_PHASE"] = mydo
+       _doebuild_exit_status_unlink(
+               mysettings.get("EBUILD_EXIT_STATUS_FILE"))
        phase_retval = spawn(actionmap[mydo]["cmd"] % mydo, mysettings, debug=debug, logfile=logfile, **kwargs)
        mysettings["EBUILD_PHASE"] = ""
+       msg = _doebuild_exit_status_check(
+               mydo, mysettings.get("EBUILD_EXIT_STATUS_FILE"))
+       if msg:
+               phase_retval = 1
+               from textwrap import wrap
+               from portage.elog.messages import eerror
+               for l in wrap(msg, 72):
+                       eerror(l, phase=mydo, key=mysettings.mycpv)
 
        if "userpriv" in mysettings.features and \
                not kwargs["droppriv"] and secpass >= 2:
@@ -3744,6 +3754,8 @@ def doebuild_environment(myebuild, mydo, myroot, mysettings, debug, use_cache, m
 
        mysettings["PORTAGE_BASHRC"] = os.path.join(
                mysettings["PORTAGE_CONFIGROOT"], EBUILD_SH_ENV_FILE.lstrip(os.path.sep))
+       mysettings["EBUILD_EXIT_STATUS_FILE"] = os.path.join(
+               mysettings["PORTAGE_BUILDDIR"], ".exit_status")
 
        #set up KV variable -- DEP SPEEDUP :: Don't waste time. Keep var persistent.
        if (mydo!="depend") or not mysettings.has_key("KV"):
@@ -3947,6 +3959,37 @@ def prepare_build_dirs(myroot, mysettings, cleanup):
                        mysettings["PORTAGE_LOG_FILE"] = os.path.join(
                                mysettings["T"], "build.log")
 
+def _doebuild_exit_status_check(mydo, exit_status_file):
+       """
+       Returns an error string if the shell appeared
+       to exit unsuccessfully, None otherwise.
+       """
+       if not exit_status_file or \
+               os.path.exists(exit_status_file):
+               return None
+       msg = ("The ebuild phase '%s' has exited " % mydo) + \
+       "unexpectedly. This is type of behavior " + \
+       "is known to be triggered " + \
+       "by things such as failed variable " + \
+       "assignments (bug #190128) or bad substitution " + \
+       "errors (bug #200313)."
+       return msg
+
+def _doebuild_exit_status_unlink(exit_status_file):
+       """
+       Double check to make sure it really doesn't exist
+       and raise an OSError if it still does (it shouldn't).
+       OSError if necessary.
+       """
+       if not exit_status_file:
+               return
+       try:
+               os.unlink(exit_status_file)
+       except OSError:
+               pass
+       if os.path.exists(exit_status_file):
+               os.unlink(exit_status_file)
+
 _doebuild_manifest_exempt_depend = 0
 _doebuild_manifest_checked = None
 
@@ -4089,6 +4132,19 @@ def doebuild(myebuild, mydo, myroot, mysettings, debug=0, listonly=0,
                                        return 1
                        _doebuild_manifest_checked = manifest_path
 
+       def exit_status_check(retval):
+               if retval != os.EX_OK:
+                       return retval
+               msg = _doebuild_exit_status_check(
+                       mydo, mysettings.get("EBUILD_EXIT_STATUS_FILE"))
+               if msg:
+                       retval = 1
+                       from textwrap import wrap
+                       from portage.elog.messages import eerror
+                       for l in wrap(msg, 72):
+                               eerror(l, phase=mydo, key=mysettings.mycpv)
+               return retval
+
        logfile=None
        builddir_lock = None
        tmpdir = None
@@ -4209,6 +4265,12 @@ def doebuild(myebuild, mydo, myroot, mysettings, debug=0, listonly=0,
                        logfile = mysettings.get("PORTAGE_LOG_FILE")
                        if logfile and not os.access(os.path.dirname(logfile), os.W_OK):
                                logfile = None
+                               
+               if have_build_dirs:
+                       _doebuild_exit_status_unlink(
+                               mysettings.get("EBUILD_EXIT_STATUS_FILE"))
+               else:
+                       mysettings.pop("EBUILD_EXIT_STATUS_FILE", None)
                if mydo == "unmerge":
                        return unmerge(mysettings["CATEGORY"],
                                mysettings["PF"], myroot, mysettings, vartree=vartree)
@@ -4230,6 +4292,7 @@ def doebuild(myebuild, mydo, myroot, mysettings, debug=0, listonly=0,
                                mysettings.load_infodir(infodir)
                        retval = spawn(EBUILD_SH_BINARY + " " + mydo, mysettings,
                                debug=debug, free=1, logfile=logfile)
+                       retval = exit_status_check(retval)
                        if secpass >= 2:
                                """ Privileged phases may have left files that need to be made
                                writable to a less privileged user."""
@@ -4240,6 +4303,7 @@ def doebuild(myebuild, mydo, myroot, mysettings, debug=0, listonly=0,
                elif mydo == "preinst":
                        phase_retval = spawn(" ".join((EBUILD_SH_BINARY, mydo)),
                                mysettings, debug=debug, free=1, logfile=logfile)
+                       phase_retval = exit_status_check(phase_retval)
                        if phase_retval == os.EX_OK:
                                # Post phase logic and tasks that have been factored out of
                                # ebuild.sh.
@@ -4257,6 +4321,7 @@ def doebuild(myebuild, mydo, myroot, mysettings, debug=0, listonly=0,
                        mysettings.load_infodir(mysettings["O"])
                        phase_retval = spawn(" ".join((EBUILD_SH_BINARY, mydo)),
                                mysettings, debug=debug, free=1, logfile=logfile)
+                       phase_retval = exit_status_check(phase_retval)
                        if phase_retval == os.EX_OK:
                                # Post phase logic and tasks that have been factored out of
                                # ebuild.sh.
@@ -4270,8 +4335,10 @@ def doebuild(myebuild, mydo, myroot, mysettings, debug=0, listonly=0,
                        return phase_retval
                elif mydo in ("prerm", "postrm", "config", "info"):
                        mysettings.load_infodir(mysettings["O"])
-                       return spawn(EBUILD_SH_BINARY + " " + mydo,
+                       retval =  spawn(EBUILD_SH_BINARY + " " + mydo,
                                mysettings, debug=debug, free=1, logfile=logfile)
+                       retval = exit_status_check(retval)
+                       return retval
 
                mycpv = "/".join((mysettings["CATEGORY"], mysettings["PF"]))
 
@@ -4449,7 +4516,9 @@ def doebuild(myebuild, mydo, myroot, mysettings, debug=0, listonly=0,
                elif mydo=="merge":
                        retval = spawnebuild("install", actionmap, mysettings, debug,
                                alwaysdep=1, logfile=logfile)
-                       if retval != os.EX_OK:
+                       if retval == os.EX_OK:
+                               retval = exit_status_check(retval)
+                       else:
                                # The merge phase handles this already.  Callers don't know how
                                # far this function got, so we have to call elog_process() here
                                # so that it's only called once.