I'm cooking up some eclass functions and it occurred to me that perhaps
I had some responsibility regarding EAPI4's new error handling
semantics. After looking into it, it seems that, superficially, the
answer to my question is "no, the EAPI4 changes only apply to ebuild
helpers."
Even so, this got me thinking: if ebuild helpers are going to warn(*) in
EAPI<4 and die in EAPI>=4 (unless somebody used "nonfatal"), then, those
error-handling semantics are going to bubble up through my eclass to its
heirs (assuming my eclass uses helpers).
The problem I see with that is that a consumer of my eclass function is
effectively required to anticipate these EAPI helper-function outcomes,
which is fair-enough if s/he's consuming them directly, but pretty
unreasonable if they're just consuming my eclass.... In effect, this
seems to create a situation where they can't know what to expect,
without wading through my code, unless I go in and document "in EAPI4+
it does X, but in EAPI3, it does Y". Which, let's face it, most eclass
authors probably won't get around to doing.
For example, suppose poop.eclass has a function "epoop" which performs
the following steps:
o Downloads the latest "post" from the
http://www.unnecessaryquotes.com/ "website" using "wget"
o Strips out the top 3 posts from piped wget output using
a bunch of sed and awk magic. Saves each post-processed post as
"${S}/$(mktemp -u $( printf %100s |tr " " "X" )).xml" and
remembers these filenames in three hard-coded variables.
o Installs the posts as documentation using dodoc statements
o Returns "$(( $(date +'%s') % 3 ))"
Let's say I started writing this code before I knew anything about EAPI
4, and the current epoop documentation states that:
o errors in the wget and postprocessing steps will result in a return
code of '3'
o errors from dodoc will result in a return code of '3' + the
return-code of dodoc
o epoop() succeeds every third second and otherwise fails, returning
the number of seconds ago that the API could have been invoked
successfully as the error code (WTF: this is tounge-in-cheek:
$(( $(date +'%s') % 3 )) is the current UNIX time in
seconds, modulo 3, an extremely pointless and arbitrary success
criterion for my hypothetical function that will cause it to
randomly fail 66% of the times it's invoked and runs to completion.
If you don't find this amusing, that's OK; it's not terribly
important to the matter at hand.)
Well, now I have an awkward situation. In EAPI's 0-3, the function
works as documented. In EAPI 4+, however, epoop() will die if any error
occurs in dodoc. I can fix this by replacing the dodoc code -- let's
say it originally was coded like:
dodoc ${docfile1} || { r=$?; eerror dodoc1; return $((r+3)); }
dodoc ${docfile2} || { r=$?; eerror dodoc2; return $((r+3)); }
dodoc ${docfile3} || { r=$?; eerror dodoc3; return $((r+3)); }
-- with code like:
.
.
if has "${EAPI:-0}" 0 1 2 3 ; then
nonfatal dodoc ${docfile2}
else
dodoc ${docfile2}
fi || { r=$?; eerror dodoc2; return $((r+3)); }
.
.
but I will have to do something like this for each of the three dodoc
statements. Seems like a real PITA.
Obviously, I've chosen a very silly example, and perhaps I've
cherry-picked a solution that illustrates my point -- but it's not hard
to imagine having to make fairly significant changes to an eclass in
order to preserve its pre-EAPI4 semantics in EAPI4+.
The conclusion I'm tempted to draw is that it makes more sense to bring
my eclass error-handling behavior into line with the EAPI-specific
behavior of helper-functions than it does to gum up helper usage in my
eclass with a bunch of EAPI-specific error-handling spaghetti-code.
i.e., returning to epoop: instead of the above solution, I might leave
the dodoc code alone, and instead implement something vaguely like
__helpers_die, i.e.:
# note: stupidly clever -- not how i'd implement this irl
_poop_die_maybe() {
local ret="$?" eapi="${EAPI:-0}"
eapi=${eapi::1}
(( (3-eapi)*(PORTAGE_NONFATAL-1) > 0 )) && die "$@"
eerror "$@"
return ${ret} # pass through pre-invocation $? unchanged
}
With this in place, it's not terribly difficult to bring the
error-handling/EAPI semantics of epoop() into line with those of the
helper functions across all EAPIs. For example, where we had
return $(( $(date +'%s') % 3 ))
we might now put
ret="$(( $(date +'%s') % 3 ))"
(( ret == 0 )) || \
_poop_die_maybe "epoop: invoked ${ret} seconds too late"
return "${ret}"
Or, if it was the last statement in the epoop function, just:
( exit $(( $(date +'%s') % 3 )) ) || \
_poop_die_maybe "epoop: invoked $? seconds too late"
I hope my gist isn't getting lost in the particulars of this silly
example. My point is: EAPI4 changes the behavior of eclasses if those
eclasses consume helper functions, which could lead to some pretty
muddled error-handling semantics without some kind of clarification or
reimplementation. Rather than dance around EAPI4 in our eclasses,
perhaps it's more elegant to try to approximate the helper-function
error handling semantics at the eclass level, across all EAPI's, which
would mean dying for "any kind of error" in EAPI4+ and warning and
returning failure codes in EAPI3-.
Stated more strongly, perhaps EAPI auto-die effectively sets us on a
course where eventually "everything else" really ought to work the same
way as the PMS mandates for helpers, avoiding an otherwise confusing
situation where ebuild authors constantly need to second-guess eclass
behavior: "yes, but specifically how does it behave in EAPI4+/3-, so I
can put die() in the right places?"
If so, this raises a second question: should portage provide some kind
of facility to uh.. facilitate the needs of eclass authors looking to
implement these error-handling patterns, rather than leave the correct
implementation of something like _epoop_die_maybe() "as an exercise to
the coder"?
... does anyone see a better approach? Perhaps this a non-issue for
some reason I'm not grokking? I'd love to hear your thoughts -- the
exact "right" answer isn't so clear to me, and, frankly, I'd like to
move on to my eclass writing.
Thanks,
-gmt
--------
* Hey, can someone explain why, when __helpers_die decides die()ing is
not called-for (due either to EAPI<4 or PORTAGE_NONFATAL==1), it
unceremoniously dumps the error message to stderr, rather than, i.e.,
calling one of the __elog_base-consuming functions? I really don't get
that.