1 |
Hello, |
2 |
|
3 |
This email follows a discussion on #gentoo yesterday. I'll start with |
4 |
an executive summary of what follows since this may become long-winded. |
5 |
Feedback and comments are welcome. |
6 |
|
7 |
Summary |
8 |
------- |
9 |
try() (in ebuild.sh) attempts to provide ebuilds with an easy |
10 |
mechanism for aborting the build in case a single step fails. Its |
11 |
arguments need to be interpreted as a bash command, including |
12 |
overriding environment variables, bash builtins, and external |
13 |
programs. Quoting needs to be preserved. |
14 |
|
15 |
To accomplish this, it needs to use either bash "eval" or the external |
16 |
program "env". However each of these methods has shortcomings, and |
17 |
neither handles pipelines or redirection well. As a result, I propose |
18 |
an alternate method that is more suited to bash syntax: die() and |
19 |
assert_true_or_die(). |
20 |
|
21 |
Defining the Problem |
22 |
-------------------- |
23 |
Ebuild authors need an easy method for "throwing an exception" in an |
24 |
ebuild so that ebuilds can be robust. Non-zero exit status from |
25 |
a command needs to detected, a standard error message given to the |
26 |
user, and the build aborted. |
27 |
|
28 |
Constraints: |
29 |
|
30 |
(1) The "command" should be written as a normal bash command, with |
31 |
normal quoting, etc. In other words, it should be possible to |
32 |
tack the method onto a command with minimal effort. |
33 |
|
34 |
(2) The "command" can be a bash builtin or external program (or |
35 |
function for that matter), and should be able to contain |
36 |
overridden environment variables as bash normally allows. |
37 |
|
38 |
Initial solution: try() |
39 |
----------------------- |
40 |
try() was written to follow the pattern of languages with exception |
41 |
handling. It's mission seems simple enough: Interpret the arguments |
42 |
as a bash command, execute the command, abort with an error message if |
43 |
the command fails. |
44 |
|
45 |
The problem with try() is the incompatibility of the two constraints |
46 |
in bash. Consider the following list of problems with some |
47 |
skeleton implementations of try(): |
48 |
|
49 |
* If try() { eval "$@"; } is used, then the arguments to try() |
50 |
must be escaped so that the quoting is preserved. |
51 |
|
52 |
* If try() { env "$@"; } is used, then the command can't be a bash |
53 |
builtin, because env doesn't understand bash builtins. |
54 |
|
55 |
* If try() { bash -c "$@"; } is used, then the effects of the |
56 |
builtin won't affect the parent shell, e.g. cd and setting of |
57 |
variables. |
58 |
|
59 |
* If try() { "$@"; } is used, then bash doesn't evaluate the line |
60 |
for prefixed setting of environment variables. |
61 |
|
62 |
However this is only the tip of the iceberg! Here are some more |
63 |
problems that use the following try(), functionally similar to the |
64 |
one in ebuild.sh: |
65 |
|
66 |
try() { env "$@"; if [ $? != 0 ]; then echo "ERROR"; exit 1; fi; } |
67 |
|
68 |
* Since redirections occur *outside* of the try() function call |
69 |
(programs executed inside of the function inherit the file |
70 |
descriptors), they won't cause try() to fail. For example: |
71 |
|
72 |
$ try cat nonexistent_file |
73 |
ERROR (then shell exits) |
74 |
|
75 |
$ try cat < nonexistent_file |
76 |
bash: nonexistent_file: No such file or directory |
77 |
|
78 |
* Continuing on the redirections thread, consider what happens when |
79 |
you redirect the stderr of the command... The ebuild will bomb |
80 |
out, but the error message will be lost because try() no longer |
81 |
has access to the output terminal. (Yes, it could use /dev/tty, |
82 |
but shouldn't because that would pose problems for automated |
83 |
builds.) |
84 |
|
85 |
$ try grep patt * >/dev/null 2>&1 |
86 |
[no error message from try] |
87 |
|
88 |
* try() only applies to the first element of a pipeline. As |
89 |
a result, the following would not be caught by try(). |
90 |
|
91 |
$ try cat bogus_but_existing_patch | patch -p1 |
92 |
patch: **** Only garbage was found in the patch input. |
93 |
|
94 |
I think try() is a good idea, but the limitations of bash syntax make |
95 |
it difficult to implement well. Therefore I propose a different |
96 |
solution... |
97 |
|
98 |
Proposed replacement: die() and assert_true_or_die() |
99 |
---------------------------------------------------- |
100 |
diefunc() { |
101 |
local funcname="$1" lineno="$2" exitcode="$3" |
102 |
shift 3 |
103 |
echo &>2 |
104 |
echo "!!! ERROR: The ebuild did not complete successfully." &>2 |
105 |
echo "!!! Function $funcname, Line $lineno, Exitcode $exitcode" &>2 |
106 |
echo "!!! ${*:-(no error message)}" &>2 |
107 |
echo &>2 |
108 |
exit 1 |
109 |
} |
110 |
alias die='diefunc "$FUNCNAME" "$LINENO" "$?"' |
111 |
alias assert_true_or_die='_retval=$?; [ $_retval = 0 ] || \ |
112 |
diefunc "$FUNCNAME" "$LINENO" "$_retval"' |
113 |
|
114 |
The aliases are necessary to get FUNCNAME and LINENO from the |
115 |
caller rather than the callee (diefunc). |
116 |
|
117 |
Note one thing (other than usage) that changes between try() and |
118 |
die(). The error output of try() shows the actual command that was |
119 |
attempted. That isn't possible with die() because it isn't being |
120 |
passed the command. However it attempts to make up the shortcoming by |
121 |
including the function name, the line number, the exitcode, and |
122 |
(bonus) a custom message that can be passed by the caller. |
123 |
|
124 |
To convert try-lines to die-lines would be as follows (each line |
125 |
followed by its conversion). These examples are all from the Linux |
126 |
kernel ebuild. |
127 |
|
128 |
try mv linux linux-${KV} |
129 |
|
130 |
mv linux linux-${KV} || die |
131 |
|
132 |
---------------------------------------- |
133 |
|
134 |
try bzip2 -dc ${DISTDIR}/patch-${KV}.bz2 | patch -p1 |
135 |
|
136 |
bzip2 -dc ${DISTDIR}/patch-${KV}.bz2 | patch -p1 || die |
137 |
|
138 |
# or for checking the first half of the pipeline as well as the |
139 |
# second half, since bash only reports exit status from the last |
140 |
# element on a pipeline... |
141 |
(bzip2 -dc ${DISTDIR}/patch-${KV}.bz2 || die) | patch -p1 || die |
142 |
|
143 |
---------------------------------------- |
144 |
|
145 |
try cd LVM/${LVMV} |
146 |
|
147 |
# include a custom message |
148 |
cd LVM/${LVMV} || die "Looks like LVM didn't unpack correctly." |
149 |
|
150 |
---------------------------------------- |
151 |
|
152 |
try CFLAGS="${CFLAGS} -I${S}/include" ./configure --prefix=/ \ |
153 |
--mandir=/usr/share/man --with-kernel_dir="${S}" |
154 |
|
155 |
# use the assert_true_or_die() version for clarity |
156 |
CFLAGS="${CFLAGS} -I${S}/include" ./configure --prefix=/ \ |
157 |
--mandir=/usr/share/man --with-kernel_dir="${S}" |
158 |
assert_true_or_die "Kernel configuration failed" |
159 |
|
160 |
---------------------------------------- |
161 |
|
162 |
try make KERNEL_VERSION=${KV} KERNEL_DIR=${S} |
163 |
|
164 |
make KERNEL_VERSION=${KV} KERNEL_DIR=${S} || die |
165 |
|
166 |
Conclusion |
167 |
---------- |
168 |
I believe that die() and assert_true_or_die() fulfill the constraints, |
169 |
provide a workable replacement for try(), and are more bashish all |
170 |
around. Note there is no need to immediately convert all existing |
171 |
ebuilds to die() and assert_true_or_die(). If this proposal is |
172 |
accepted (after discussion and potential modifications), then the new |
173 |
functions should be used in new ebuilds, and old ebuilds can be |
174 |
gradually changed to the new scheme. |
175 |
|
176 |
Feedback/questions/comments are welcome! Thanks for reading. |
177 |
|
178 |
Aron |