1 |
2013-10-16 23:34 Mike Frysinger napisał(a): |
2 |
> This rewrites the prestrip logic from scratch in python. |
3 |
|
4 |
Some things are incompatible with Python 3, resulting at least in ImportErrors and a NameError. |
5 |
See other comments below. |
6 |
|
7 |
> The bash code was getting way too hairy, and the addition of parallel stripping was |
8 |
> (still) racy. |
9 |
> |
10 |
> The upside is this has unittests :). |
11 |
> |
12 |
> The downside is that this adds a dep on pyelftools in portage. But I |
13 |
> think that's fine as pax-utils has been pulling that in for some time. |
14 |
> --- |
15 |
> bin/ebuild-helpers/prepstrip | 387 +----------- |
16 |
> pym/portage/bin/__init__.py | 0 |
17 |
> pym/portage/bin/prepstrip.py | 682 +++++++++++++++++++++ |
18 |
> pym/portage/tests/bin/test_prepstrip.py | 253 ++++++++ |
19 |
> pym/portage/tests/bin/testdir/.gitignore | 2 + |
20 |
> .../bin/testdir/work/somepackage-1.2.3/Makefile | 65 ++ |
21 |
> .../testdir/work/somepackage-1.2.3/o/.gitignore | 1 + |
22 |
> .../bin/testdir/work/somepackage-1.2.3/src/main.c | 5 + |
23 |
> pym/portage/util/parallel.py | 598 ++++++++++++++++++ |
24 |
> 9 files changed, 1617 insertions(+), 376 deletions(-) |
25 |
> create mode 100644 pym/portage/bin/__init__.py |
26 |
> create mode 100644 pym/portage/bin/prepstrip.py |
27 |
> create mode 100644 pym/portage/tests/bin/test_prepstrip.py |
28 |
> create mode 100644 pym/portage/tests/bin/testdir/.gitignore |
29 |
> create mode 100644 pym/portage/tests/bin/testdir/work/somepackage-1.2.3/Makefile |
30 |
> create mode 100644 pym/portage/tests/bin/testdir/work/somepackage-1.2.3/o/.gitignore |
31 |
> create mode 100644 pym/portage/tests/bin/testdir/work/somepackage-1.2.3/src/main.c |
32 |
> create mode 100644 pym/portage/util/parallel.py |
33 |
> |
34 |
> diff --git a/bin/ebuild-helpers/prepstrip b/bin/ebuild-helpers/prepstrip |
35 |
> index 9b2d47c..c8df5a5 100755 |
36 |
> --- a/bin/ebuild-helpers/prepstrip |
37 |
> +++ b/bin/ebuild-helpers/prepstrip |
38 |
> @@ -1,386 +1,21 @@ |
39 |
> -#!/bin/bash |
40 |
> +#!/usr/bin/python |
41 |
> # Copyright 1999-2013 Gentoo Foundation |
42 |
> # Distributed under the terms of the GNU General Public License v2 |
43 |
> |
44 |
> -PORTAGE_PYM_PATH=${PORTAGE_PYM_PATH:-/usr/lib/portage/pym} |
45 |
> -source "${PORTAGE_BIN_PATH:-/usr/lib/portage/bin}"/helper-functions.sh |
46 |
> +"""Helper for stripping installed programs. |
47 |
> |
48 |
> -# avoid multiple calls to `has`. this creates things like: |
49 |
> -# FEATURES_foo=false |
50 |
> -# if "foo" is not in $FEATURES |
51 |
> -tf() { "$@" && echo true || echo false ; } |
52 |
> -exp_tf() { |
53 |
> - local flag var=$1 |
54 |
> - shift |
55 |
> - for flag in "$@" ; do |
56 |
> - eval ${var}_${flag}=$(tf has ${flag} ${!var}) |
57 |
> - done |
58 |
> -} |
59 |
> -exp_tf FEATURES compressdebug installsources nostrip splitdebug xattr |
60 |
> -exp_tf RESTRICT binchecks installsources splitdebug strip |
61 |
> +This handles all the fun things in addition to stripping like splitdebug, |
62 |
> +installsources, etc... |
63 |
> |
64 |
> -if ! ___eapi_has_prefix_variables; then |
65 |
> - EPREFIX= ED=${D} |
66 |
> -fi |
67 |
> +If no paths are specified, then $D is searched. |
68 |
> +""" |
69 |
> |
70 |
> -banner=false |
71 |
> -SKIP_STRIP=false |
72 |
> -if ${RESTRICT_strip} || ${FEATURES_nostrip} ; then |
73 |
> - SKIP_STRIP=true |
74 |
> - banner=true |
75 |
> - ${FEATURES_installsources} || exit 0 |
76 |
> -fi |
77 |
> +from __future__ import print_function |
78 |
> |
79 |
> -PRESERVE_XATTR=false |
80 |
> -if [[ ${KERNEL} == linux ]] && ${FEATURES_xattr} ; then |
81 |
> - PRESERVE_XATTR=true |
82 |
> - if type -P getfattr >/dev/null && type -P setfattr >/dev/null ; then |
83 |
> - dump_xattrs() { |
84 |
> - getfattr -d --absolute-names "$1" |
85 |
> - } |
86 |
> - restore_xattrs() { |
87 |
> - setfattr --restore=- |
88 |
> - } |
89 |
> - else |
90 |
> - dump_xattrs() { |
91 |
> - PYTHONPATH=${PORTAGE_PYTHONPATH:-${PORTAGE_PYM_PATH}} \ |
92 |
> - "${PORTAGE_PYTHON:-/usr/bin/python}" \ |
93 |
> - "${PORTAGE_BIN_PATH}/xattr-helper.py" --dump < <(echo -n "$1") |
94 |
> - } |
95 |
> - restore_xattrs() { |
96 |
> - PYTHONPATH=${PORTAGE_PYTHONPATH:-${PORTAGE_PYM_PATH}} \ |
97 |
> - "${PORTAGE_PYTHON:-/usr/bin/python}" \ |
98 |
> - "${PORTAGE_BIN_PATH}/xattr-helper.py" --restore |
99 |
> - } |
100 |
> - fi |
101 |
> -fi |
102 |
> +import sys |
103 |
> |
104 |
> -# look up the tools we might be using |
105 |
> -for t in STRIP:strip OBJCOPY:objcopy READELF:readelf ; do |
106 |
> - v=${t%:*} # STRIP |
107 |
> - t=${t#*:} # strip |
108 |
> - eval ${v}=\"${!v:-${CHOST}-${t}}\" |
109 |
> - type -P -- ${!v} >/dev/null || eval ${v}=${t} |
110 |
> -done |
111 |
> +from portage.bin import prepstrip |
112 |
> |
113 |
> -# Figure out what tool set we're using to strip stuff |
114 |
> -unset SAFE_STRIP_FLAGS DEF_STRIP_FLAGS SPLIT_STRIP_FLAGS |
115 |
> -case $(${STRIP} --version 2>/dev/null) in |
116 |
> -*elfutils*) # dev-libs/elfutils |
117 |
> - # elfutils default behavior is always safe, so don't need to specify |
118 |
> - # any flags at all |
119 |
> - SAFE_STRIP_FLAGS="" |
120 |
> - DEF_STRIP_FLAGS="--remove-comment" |
121 |
> - SPLIT_STRIP_FLAGS="-f" |
122 |
> - ;; |
123 |
> -*GNU*) # sys-devel/binutils |
124 |
> - # We'll leave out -R .note for now until we can check out the relevance |
125 |
> - # of the section when it has the ALLOC flag set on it ... |
126 |
> - SAFE_STRIP_FLAGS="--strip-unneeded" |
127 |
> - DEF_STRIP_FLAGS="-R .comment -R .GCC.command.line -R .note.gnu.gold-version" |
128 |
> - SPLIT_STRIP_FLAGS= |
129 |
> - ;; |
130 |
> -esac |
131 |
> -: ${PORTAGE_STRIP_FLAGS=${SAFE_STRIP_FLAGS} ${DEF_STRIP_FLAGS}} |
132 |
> |
133 |
> -prepstrip_sources_dir=${EPREFIX}/usr/src/debug/${CATEGORY}/${PF} |
134 |
> - |
135 |
> -type -P debugedit >/dev/null && debugedit_found=true || debugedit_found=false |
136 |
> -debugedit_warned=false |
137 |
> - |
138 |
> -__multijob_init |
139 |
> - |
140 |
> -# Setup $T filesystem layout that we care about. |
141 |
> -tmpdir="${T}/prepstrip" |
142 |
> -rm -rf "${tmpdir}" |
143 |
> -mkdir -p "${tmpdir}"/{inodes,splitdebug,sources} |
144 |
> - |
145 |
> -# Usage: save_elf_sources <elf> |
146 |
> -save_elf_sources() { |
147 |
> - ${FEATURES_installsources} || return 0 |
148 |
> - ${RESTRICT_installsources} && return 0 |
149 |
> - if ! ${debugedit_found} ; then |
150 |
> - if ! ${debugedit_warned} ; then |
151 |
> - debugedit_warned=true |
152 |
> - ewarn "FEATURES=installsources is enabled but the debugedit binary could not" |
153 |
> - ewarn "be found. This feature will not work unless debugedit is installed!" |
154 |
> - fi |
155 |
> - return 0 |
156 |
> - fi |
157 |
> - |
158 |
> - local x=$1 |
159 |
> - |
160 |
> - # since we're editing the ELF here, we should recompute the build-id |
161 |
> - # (the -i flag below). save that output so we don't need to recompute |
162 |
> - # it later on in the save_elf_debug step. |
163 |
> - buildid=$(debugedit -i \ |
164 |
> - -b "${WORKDIR}" \ |
165 |
> - -d "${prepstrip_sources_dir}" \ |
166 |
> - -l "${tmpdir}/sources/${x##*/}.${BASHPID}" \ |
167 |
> - "${x}") |
168 |
> -} |
169 |
> - |
170 |
> -# Usage: save_elf_debug <elf> [splitdebug file] |
171 |
> -save_elf_debug() { |
172 |
> - ${FEATURES_splitdebug} || return 0 |
173 |
> - ${RESTRICT_splitdebug} && return 0 |
174 |
> - |
175 |
> - # NOTE: Debug files must be installed in |
176 |
> - # ${EPREFIX}/usr/lib/debug/${EPREFIX} (note that ${EPREFIX} occurs |
177 |
> - # twice in this path) in order for gdb's debug-file-directory |
178 |
> - # lookup to work correctly. |
179 |
> - local x=$1 |
180 |
> - local inode_debug=$2 |
181 |
> - local splitdebug=$3 |
182 |
> - local y=${ED}usr/lib/debug/${x:${#D}}.debug |
183 |
> - |
184 |
> - # dont save debug info twice |
185 |
> - [[ ${x} == *".debug" ]] && return 0 |
186 |
> - |
187 |
> - mkdir -p "${y%/*}" |
188 |
> - |
189 |
> - if [ -f "${inode_debug}" ] ; then |
190 |
> - ln "${inode_debug}" "${y}" || die "ln failed unexpectedly" |
191 |
> - else |
192 |
> - if [[ -n ${splitdebug} ]] ; then |
193 |
> - mv "${splitdebug}" "${y}" |
194 |
> - else |
195 |
> - local objcopy_flags="--only-keep-debug" |
196 |
> - ${FEATURES_compressdebug} && objcopy_flags+=" --compress-debug-sections" |
197 |
> - ${OBJCOPY} ${objcopy_flags} "${x}" "${y}" |
198 |
> - ${OBJCOPY} --add-gnu-debuglink="${y}" "${x}" |
199 |
> - fi |
200 |
> - # Only do the following if the debug file was |
201 |
> - # successfully created (see bug #446774). |
202 |
> - if [ $? -eq 0 ] ; then |
203 |
> - local args="a-x,o-w" |
204 |
> - [[ -g ${x} || -u ${x} ]] && args+=",go-r" |
205 |
> - chmod ${args} "${y}" |
206 |
> - ln "${y}" "${inode_debug}" || die "ln failed unexpectedly" |
207 |
> - fi |
208 |
> - fi |
209 |
> - |
210 |
> - # if we don't already have build-id from debugedit, look it up |
211 |
> - if [[ -z ${buildid} ]] ; then |
212 |
> - # convert the readelf output to something useful |
213 |
> - buildid=$(${READELF} -n "${x}" 2>/dev/null | awk '/Build ID:/{ print $NF; exit }') |
214 |
> - fi |
215 |
> - if [[ -n ${buildid} ]] ; then |
216 |
> - local buildid_dir="${ED}usr/lib/debug/.build-id/${buildid:0:2}" |
217 |
> - local buildid_file="${buildid_dir}/${buildid:2}" |
218 |
> - mkdir -p "${buildid_dir}" |
219 |
> - [ -L "${buildid_file}".debug ] || ln -s "../../${x:${#D}}.debug" "${buildid_file}.debug" |
220 |
> - [ -L "${buildid_file}" ] || ln -s "/${x:${#D}}" "${buildid_file}" |
221 |
> - fi |
222 |
> -} |
223 |
> - |
224 |
> -# Usage: process_elf <elf> |
225 |
> -process_elf() { |
226 |
> - local x=$1 inode_link=$2 strip_flags=${*:3} |
227 |
> - local already_stripped lockfile xt_data |
228 |
> - |
229 |
> - __vecho " ${x:${#ED}}" |
230 |
> - |
231 |
> - # If two processes try to debugedit or strip the same hardlink at the |
232 |
> - # same time, it may corrupt files or cause loss of splitdebug info. |
233 |
> - # So, use a lockfile to prevent interference (easily observed with |
234 |
> - # dev-vcs/git which creates ~111 hardlinks to one file in |
235 |
> - # /usr/libexec/git-core). |
236 |
> - lockfile=${inode_link}_lockfile |
237 |
> - if ! ln "${inode_link}" "${lockfile}" 2>/dev/null ; then |
238 |
> - while [[ -f ${lockfile} ]] ; do |
239 |
> - sleep 1 |
240 |
> - done |
241 |
> - unset lockfile |
242 |
> - fi |
243 |
> - |
244 |
> - [ -f "${inode_link}_stripped" ] && already_stripped=true || already_stripped=false |
245 |
> - |
246 |
> - if ! ${already_stripped} ; then |
247 |
> - if ${PRESERVE_XATTR} ; then |
248 |
> - xt_data=$(dump_xattrs "${x}") |
249 |
> - fi |
250 |
> - save_elf_sources "${x}" |
251 |
> - fi |
252 |
> - |
253 |
> - if ${strip_this} ; then |
254 |
> - |
255 |
> - # see if we can split & strip at the same time |
256 |
> - if [[ -n ${SPLIT_STRIP_FLAGS} ]] ; then |
257 |
> - local shortname="${x##*/}.debug" |
258 |
> - local splitdebug="${tmpdir}/splitdebug/${shortname}.${BASHPID}" |
259 |
> - ${already_stripped} || \ |
260 |
> - ${STRIP} ${strip_flags} \ |
261 |
> - -f "${splitdebug}" \ |
262 |
> - -F "${shortname}" \ |
263 |
> - "${x}" |
264 |
> - save_elf_debug "${x}" "${inode_link}_debug" "${splitdebug}" |
265 |
> - else |
266 |
> - save_elf_debug "${x}" "${inode_link}_debug" |
267 |
> - ${already_stripped} || \ |
268 |
> - ${STRIP} ${strip_flags} "${x}" |
269 |
> - fi |
270 |
> - fi |
271 |
> - |
272 |
> - if ${already_stripped} ; then |
273 |
> - rm -f "${x}" || die "rm failed unexpectedly" |
274 |
> - ln "${inode_link}_stripped" "${x}" || die "ln failed unexpectedly" |
275 |
> - else |
276 |
> - ln "${x}" "${inode_link}_stripped" || die "ln failed unexpectedly" |
277 |
> - if [[ ${xt_data} ]] ; then |
278 |
> - restore_xattrs <<< "${xt_data}" |
279 |
> - fi |
280 |
> - fi |
281 |
> - |
282 |
> - [[ -n ${lockfile} ]] && rm -f "${lockfile}" |
283 |
> -} |
284 |
> - |
285 |
> -# The existance of the section .symtab tells us that a binary is stripped. |
286 |
> -# We want to log already stripped binaries, as this may be a QA violation. |
287 |
> -# They prevent us from getting the splitdebug data. |
288 |
> -if ! ${RESTRICT_binchecks} && ! ${RESTRICT_strip} ; then |
289 |
> - # We need to do the non-stripped scan serially first before we turn around |
290 |
> - # and start stripping the files ourselves. The log parsing can be done in |
291 |
> - # parallel though. |
292 |
> - log=${tmpdir}/scanelf-already-stripped.log |
293 |
> - scanelf -yqRBF '#k%F' -k '!.symtab' "$@" | sed -e "s#^${ED}##" > "${log}" |
294 |
> - ( |
295 |
> - __multijob_child_init |
296 |
> - qa_var="QA_PRESTRIPPED_${ARCH/-/_}" |
297 |
> - [[ -n ${!qa_var} ]] && QA_PRESTRIPPED="${!qa_var}" |
298 |
> - if [[ -n ${QA_PRESTRIPPED} && -s ${log} && \ |
299 |
> - ${QA_STRICT_PRESTRIPPED-unset} = unset ]] ; then |
300 |
> - shopts=$- |
301 |
> - set -o noglob |
302 |
> - for x in ${QA_PRESTRIPPED} ; do |
303 |
> - sed -e "s#^${x#/}\$##" -i "${log}" |
304 |
> - done |
305 |
> - set +o noglob |
306 |
> - set -${shopts} |
307 |
> - fi |
308 |
> - sed -e "/^\$/d" -e "s#^#/#" -i "${log}" |
309 |
> - if [[ -s ${log} ]] ; then |
310 |
> - __vecho -e "\n" |
311 |
> - eqawarn "QA Notice: Pre-stripped files found:" |
312 |
> - eqawarn "$(<"${log}")" |
313 |
> - else |
314 |
> - rm -f "${log}" |
315 |
> - fi |
316 |
> - ) & |
317 |
> - __multijob_post_fork |
318 |
> -fi |
319 |
> - |
320 |
> -# Since strip creates a new inode, we need to know the initial set of |
321 |
> -# inodes in advance, so that we can avoid interference due to trying |
322 |
> -# to strip the same (hardlinked) file multiple times in parallel. |
323 |
> -# See bug #421099. |
324 |
> -if [[ ${USERLAND} == BSD ]] ; then |
325 |
> - get_inode_number() { stat -f '%i' "$1"; } |
326 |
> -else |
327 |
> - get_inode_number() { stat -c '%i' "$1"; } |
328 |
> -fi |
329 |
> -cd "${tmpdir}/inodes" || die "cd failed unexpectedly" |
330 |
> -while read -r x ; do |
331 |
> - inode_link=$(get_inode_number "${x}") || die "stat failed unexpectedly" |
332 |
> - echo "${x}" >> "${inode_link}" || die "echo failed unexpectedly" |
333 |
> -done < <( |
334 |
> - # Use sort -u to eliminate duplicates for bug #445336. |
335 |
> - ( |
336 |
> - scanelf -yqRBF '#k%F' -k '.symtab' "$@" |
337 |
> - find "$@" -type f ! -type l -name '*.a' |
338 |
> - ) | LC_ALL=C sort -u |
339 |
> -) |
340 |
> - |
341 |
> -# Now we look for unstripped binaries. |
342 |
> -for inode_link in $(shopt -s nullglob; echo *) ; do |
343 |
> -while read -r x |
344 |
> -do |
345 |
> - |
346 |
> - if ! ${banner} ; then |
347 |
> - __vecho "strip: ${STRIP} ${PORTAGE_STRIP_FLAGS}" |
348 |
> - banner=true |
349 |
> - fi |
350 |
> - |
351 |
> - ( |
352 |
> - __multijob_child_init |
353 |
> - f=$(file "${x}") || exit 0 |
354 |
> - [[ -z ${f} ]] && exit 0 |
355 |
> - |
356 |
> - if ! ${SKIP_STRIP} ; then |
357 |
> - # The noglob funk is to support STRIP_MASK="/*/booga" and to keep |
358 |
> - # the for loop from expanding the globs. |
359 |
> - # The eval echo is to support STRIP_MASK="/*/{booga,bar}" sex. |
360 |
> - set -o noglob |
361 |
> - strip_this=true |
362 |
> - for m in $(eval echo ${STRIP_MASK}) ; do |
363 |
> - [[ /${x#${ED}} == ${m} ]] && strip_this=false && break |
364 |
> - done |
365 |
> - set +o noglob |
366 |
> - else |
367 |
> - strip_this=false |
368 |
> - fi |
369 |
> - |
370 |
> - # In Prefix we are usually an unprivileged user, so we can't strip |
371 |
> - # unwritable objects. Make them temporarily writable for the |
372 |
> - # stripping. |
373 |
> - was_not_writable=false |
374 |
> - if [[ ! -w ${x} ]] ; then |
375 |
> - was_not_writable=true |
376 |
> - chmod u+w "${x}" |
377 |
> - fi |
378 |
> - |
379 |
> - # only split debug info for final linked objects |
380 |
> - # or kernel modules as debuginfo for intermediatary |
381 |
> - # files (think crt*.o from gcc/glibc) is useless and |
382 |
> - # actually causes problems. install sources for all |
383 |
> - # elf types though cause that stuff is good. |
384 |
> - |
385 |
> - buildid= |
386 |
> - if [[ ${f} == *"current ar archive"* ]] ; then |
387 |
> - __vecho " ${x:${#ED}}" |
388 |
> - if ${strip_this} ; then |
389 |
> - # hmm, can we split debug/sources for .a ? |
390 |
> - ${STRIP} -g "${x}" |
391 |
> - fi |
392 |
> - elif [[ ${f} == *"SB executable"* || ${f} == *"SB shared object"* ]] ; then |
393 |
> - process_elf "${x}" "${inode_link}" ${PORTAGE_STRIP_FLAGS} |
394 |
> - elif [[ ${f} == *"SB relocatable"* ]] ; then |
395 |
> - process_elf "${x}" "${inode_link}" ${SAFE_STRIP_FLAGS} |
396 |
> - fi |
397 |
> - |
398 |
> - if ${was_not_writable} ; then |
399 |
> - chmod u-w "${x}" |
400 |
> - fi |
401 |
> - ) & |
402 |
> - __multijob_post_fork |
403 |
> - |
404 |
> -done < "${inode_link}" |
405 |
> -done |
406 |
> - |
407 |
> -# With a bit more work, we could run the rsync processes below in |
408 |
> -# parallel, but not sure that'd be an overall improvement. |
409 |
> -__multijob_finish |
410 |
> - |
411 |
> -cd "${tmpdir}"/sources/ && cat * > "${tmpdir}/debug.sources" 2>/dev/null |
412 |
> -if [[ -s ${tmpdir}/debug.sources ]] && \ |
413 |
> - ${FEATURES_installsources} && \ |
414 |
> - ! ${RESTRICT_installsources} && \ |
415 |
> - ${debugedit_found} |
416 |
> -then |
417 |
> - __vecho "installsources: rsyncing source files" |
418 |
> - [[ -d ${D}${prepstrip_sources_dir} ]] || mkdir -p "${D}${prepstrip_sources_dir}" |
419 |
> - grep -zv '/<[^/>]*>$' "${tmpdir}"/debug.sources | \ |
420 |
> - (cd "${WORKDIR}"; LANG=C sort -z -u | \ |
421 |
> - rsync -tL0 --chmod=ugo-st,a+r,go-w,Da+x,Fa-x --files-from=- "${WORKDIR}/" "${D}${prepstrip_sources_dir}/" ) |
422 |
> - |
423 |
> - # Preserve directory structure. |
424 |
> - # Needed after running save_elf_sources. |
425 |
> - # https://bugzilla.redhat.com/show_bug.cgi?id=444310 |
426 |
> - while read -r -d $'\0' emptydir |
427 |
> - do |
428 |
> - >> "${emptydir}"/.keepdir |
429 |
> - done < <(find "${D}${prepstrip_sources_dir}/" -type d -empty -print0) |
430 |
> -fi |
431 |
> - |
432 |
> -cd "${T}" |
433 |
> -rm -rf "${tmpdir}" |
434 |
> +if __name__ == '__main__': |
435 |
> + sys.exit(prepstrip.main(sys.argv[1:])) |
436 |
> diff --git a/pym/portage/bin/__init__.py b/pym/portage/bin/__init__.py |
437 |
> new file mode 100644 |
438 |
> index 0000000..e69de29 |
439 |
> diff --git a/pym/portage/bin/prepstrip.py b/pym/portage/bin/prepstrip.py |
440 |
> new file mode 100644 |
441 |
> index 0000000..0f6eb8d |
442 |
> --- /dev/null |
443 |
> +++ b/pym/portage/bin/prepstrip.py |
444 |
> @@ -0,0 +1,682 @@ |
445 |
> +#!/usr/bin/python |
446 |
> +# Copyright 1999-2013 Gentoo Foundation |
447 |
> +# Distributed under the terms of the GNU General Public License v2 |
448 |
> + |
449 |
> +"""Helper code for stripping installed programs. |
450 |
> + |
451 |
> +This handles all the fun things in addition to stripping like splitdebug, |
452 |
> +installsources, etc... |
453 |
> +""" |
454 |
> + |
455 |
> +from __future__ import print_function |
456 |
> + |
457 |
> +import contextlib |
458 |
> +import errno |
459 |
> +import fcntl |
460 |
> +import fnmatch |
461 |
> +import multiprocessing |
462 |
> +import re |
463 |
> +import shutil |
464 |
> +import stat |
465 |
> +import subprocess |
466 |
> +import sys |
467 |
> +import tempfile |
468 |
> + |
469 |
> +from elftools.elf.elffile import ELFFile |
470 |
> +from elftools.common import exceptions |
471 |
> + |
472 |
> +from portage import os, _shell_quote |
473 |
> +from portage.elog.messages import eqawarn, ewarn |
474 |
> +from portage.process import find_binary |
475 |
> +from portage.util import parallel, shlex |
476 |
> +from portage.util._argparse import ArgumentParser |
477 |
> +from portage.util.movefile import _copyxattr |
478 |
> +from portage.util._xattr import preserve_xattrs, xattr |
479 |
> + |
480 |
> + |
481 |
> +class Paths(object): |
482 |
> + """Object to hold (and cache) various paths""" |
483 |
> + |
484 |
> + _PATH_VARS = ('D', 'ED', 'EPREFIX', 'T', 'WORKDIR') |
485 |
> + _OTHER_VARS = ('CATEGORY', 'PN', 'PF') |
486 |
> + _VARS = _PATH_VARS + _OTHER_VARS |
487 |
> + D = '' |
488 |
> + ED = '' |
489 |
> + EPREFIX = '' |
490 |
> + CATEGORY = '' |
491 |
> + PN = '' |
492 |
> + PF = '' |
493 |
> + T = '' |
494 |
> + WORKDIR = '' |
495 |
> + SOURCES_DIR = '' |
496 |
> + DEBUG_DIR = '' |
497 |
> + BUILDID_DIR = '' |
498 |
> + |
499 |
> + @classmethod |
500 |
> + def cache(cls, environ=os.environ): |
501 |
> + for var in cls._VARS: |
502 |
> + val = environ.get(var, '') |
503 |
> + if var in cls._PATH_VARS: |
504 |
> + val = val.rstrip('/') |
505 |
> + setattr(cls, var, val) |
506 |
> + if cls.D and not cls.ED: |
507 |
> + cls.ED = cls.D |
508 |
> + if not cls.T: |
509 |
> + cls.T = tempfile.gettempdir() |
510 |
> + |
511 |
> + cls.SOURCES_DIR = os.path.normpath( |
512 |
> + '%s/usr/src/debug/%s/%s' % (cls.EPREFIX, cls.CATEGORY, cls.PF)) |
513 |
> + # NOTE: Debug files must be installed in |
514 |
> + # ${EPREFIX}/usr/lib/debug/${EPREFIX} (note that ${EPREFIX} occurs |
515 |
> + # twice in this path) in order for gdb's debug-file-directory |
516 |
> + # lookup to work correctly. |
517 |
> + cls.DEBUG_DIR = os.path.normpath('%s/usr/lib/debug' % (cls.EPREFIX)) |
518 |
> + cls.BUILDID_DIR = '%s/.build-id' % (cls.DEBUG_DIR) |
519 |
> + |
520 |
> + |
521 |
> +class Features(object): |
522 |
> + """Object to hold (and cache) FEATURES availability |
523 |
> + |
524 |
> + Once we've been cached, you can simply do: |
525 |
> + if Features.strip: |
526 |
> + ... do strip stuff ... |
527 |
> + """ |
528 |
> + |
529 |
> + # Some features are always enabled even if not explicitly so. |
530 |
> + IMPLICIT_FEATURES = frozenset(( |
531 |
> + 'binchecks', |
532 |
> + 'strip', |
533 |
> + )) |
534 |
> + |
535 |
> + # These are the features we deal with in this file. |
536 |
> + FEATURES = frozenset(IMPLICIT_FEATURES | set(( |
537 |
> + 'compressdebug', |
538 |
> + 'installsources', |
539 |
> + 'splitdebug', |
540 |
> + 'xattr', |
541 |
> + ))) |
542 |
> + |
543 |
> + |
544 |
> + @classmethod |
545 |
> + def cache(cls, features=None, environ=os.environ): |
546 |
> + """Cache |features| tests to avoid processing at runtime""" |
547 |
> + if features is None: |
548 |
> + features = cls.FEATURES |
549 |
> + |
550 |
> + # Portage should have done the incremental thing for us already |
551 |
> + # so we don't have to handle it ourselves (like 'foo -foo foo'). |
552 |
> + env_features = environ.get('FEATURES', '').split() |
553 |
> + env_restrict = environ.get('RESTRICT', '').split() |
554 |
> + env_features += list(cls.IMPLICIT_FEATURES) |
555 |
> + |
556 |
> + for f in features: |
557 |
> + setattr(cls, f, f in env_features and f not in env_restrict) |
558 |
> + |
559 |
> + # Backwards compat support for "nostrip" and such. |
560 |
> + if 'no' + f in env_features: |
561 |
> + setattr(cls, f, False) |
562 |
> + |
563 |
> + @classmethod |
564 |
> + def reset(cls): |
565 |
> + for f in cls.FEATURES: |
566 |
> + delattr(cls, f) |
567 |
> + |
568 |
> + @classmethod |
569 |
> + def __str__(cls): |
570 |
> + return ' '.join('%s=%s' % (f, getattr(cls, f)) for f in cls.FEATURES) |
571 |
> + |
572 |
> + |
573 |
> +class Tools(object): |
574 |
> + """Object to hold (and cache) toolchain tools that we'll need |
575 |
> + |
576 |
> + We also need to deal with things like env vars and compiler prefixes.""" |
577 |
> + |
578 |
> + TOOLS = frozenset(( |
579 |
> + 'debugedit', |
580 |
> + 'objcopy', |
581 |
> + 'strip', |
582 |
> + )) |
583 |
> + |
584 |
> + _strip_flags = {} |
585 |
> + _strip_type = None |
586 |
> + |
587 |
> + @staticmethod |
588 |
> + def find_toolchain_tool(tool, environ): |
589 |
> + """Given a toolchain |tool|, look it up via env vars |
590 |
> + |
591 |
> + e.g. We'll get "strip", so check for ${STRIP} and ${CHOST}-strip |
592 |
> + before falling back to "strip". |
593 |
> + """ |
594 |
> + # Look for $STRIP first. |
595 |
> + etool = environ.get(tool.upper()) |
596 |
> + if etool: |
597 |
> + path = find_binary(etool) |
598 |
> + if path: |
599 |
> + return path |
600 |
> + |
601 |
> + # Look for $CHOST-strip next. |
602 |
> + chost = environ.get('CHOST') |
603 |
> + if chost: |
604 |
> + path = find_binary('%s-%s' % (chost, tool)) |
605 |
> + if path: |
606 |
> + return path |
607 |
> + |
608 |
> + # Screw it, you just get `strip` now. |
609 |
> + return tool |
610 |
> + |
611 |
> + @classmethod |
612 |
> + def cache(cls, environ=os.environ): |
613 |
> + for t in cls.TOOLS: |
614 |
> + setattr(cls, t, cls.find_toolchain_tool(t, environ)) |
615 |
> + |
616 |
> + cls._cache_strip(environ=environ) |
617 |
> + try: |
618 |
> + cls.run('debugedit', '--help', stdout=open(os.devnull, 'w')) |
619 |
|
620 |
ResourceWarning |
621 |
|
622 |
> + except OSError as e: |
623 |
|
624 |
Unused variable e |
625 |
|
626 |
> + Features.installsources = None |
627 |
> + |
628 |
> + @classmethod |
629 |
> + def strip_type(cls): |
630 |
> + if not cls._strip_type: |
631 |
> + verinfo = subprocess.check_output([cls.strip, '--version'], |
632 |
> + stderr=subprocess.STDOUT) |
633 |
> + verinfo = verinfo.split('\n', 1)[0] |
634 |
> + cls._strip_type = 'elfutils' if 'elfutils' in verinfo else 'GNU' |
635 |
> + |
636 |
> + return cls._strip_type |
637 |
> + |
638 |
> + @classmethod |
639 |
> + def _cache_strip(cls, environ): |
640 |
> + """Handle various strip flags/behavior""" |
641 |
> + if cls.strip_type() == 'elfutils': |
642 |
> + cls._strip_flags = { |
643 |
> + 'safe': '', |
644 |
> + 'default': '--remove-comment', |
645 |
> + } |
646 |
> + elif cls.strip_type() == 'GNU': |
647 |
> + cls._strip_flags = { |
648 |
> + 'safe': '--strip-unneeded', |
649 |
> + 'default': '-R .comment -R .GCC.command.line -R .note.gnu.gold-version', |
650 |
> + } |
651 |
> + cls._strip_flags['debug'] = '-g' |
652 |
> + cls._strip_flags['portage'] = environ.get( |
653 |
> + 'PORTAGE_STRIP_FLAGS', '%s %s' % (cls._strip_flags.get('safe', ''), |
654 |
> + cls._strip_flags.get('default', ''))) |
655 |
> + |
656 |
> + for k, v in cls._strip_flags.iteritems(): |
657 |
> + cls._strip_flags[k] = tuple(cls._strip_flags[k].split()) |
658 |
> + |
659 |
> + @classmethod |
660 |
> + def strip_flags(cls, strip_class): |
661 |
> + return cls._strip_flags[strip_class] |
662 |
> + |
663 |
> + @classmethod |
664 |
> + def run(cls, tool, *args, **kwargs): |
665 |
> + cmd = [getattr(cls, tool)] + list(args) |
666 |
> + proc = subprocess.Popen(cmd, **kwargs) |
667 |
> + proc.wait() |
668 |
> + if proc.returncode: |
669 |
> + raise subprocess.CalledProcessError('command failed (ret=%i): %s' % ( |
670 |
> + proc.returncode, ' '.join(map(repr, cmd)))) |
671 |
|
672 |
A generator expression would look better than map(). |
673 |
|
674 |
> + return proc |
675 |
> + |
676 |
> + @classmethod |
677 |
> + def run_strip(cls, strip_class, *args): |
678 |
> + # If stripping is disabled, then there's nothing for us to do. |
679 |
> + if Features.strip: |
680 |
> + return cls.run('strip', *(cls.strip_flags(strip_class) + args)) |
681 |
> + |
682 |
> + @classmethod |
683 |
> + def __str__(cls): |
684 |
> + return ' '.join('%s=%s' % (t, getattr(cls, t)) for t in cls.TOOLS) |
685 |
> + |
686 |
> + |
687 |
> +class Qa(object): |
688 |
> + """Object to hold (and cache) QA settings""" |
689 |
> + |
690 |
> + QA_VARS = frozenset(( |
691 |
> + 'prestripped', |
692 |
> + )) |
693 |
> + |
694 |
> + _stripmask = [] |
695 |
> + _qa_vars = {} |
696 |
> + _qa_re = {} |
697 |
> + |
698 |
> + @classmethod |
699 |
> + def cache(cls, environ=os.environ): |
700 |
> + # Support an arch-specific override QA_XXX_${ARCH} for the QA_XXX var. |
701 |
> + # It's a regex, so precompile it here so we can just execute later on. |
702 |
> + arch = environ.get('ARCH') |
703 |
> + for v in cls.QA_VARS: |
704 |
> + val = None |
705 |
> + if arch: |
706 |
> + val = environ.get('QA_%s_%s' % (v.upper, arch)) |
707 |
> + if val is None: |
708 |
> + val = environ.get('QA_%s' % (v.upper,)) |
709 |
> + if val is None: |
710 |
> + val = '' |
711 |
> + val = val.split() |
712 |
> + cls._qa_vars[v] = val |
713 |
> + cls._qa_re[v] = re.compile(r'^(%s)$' % '|'.join(val)) |
714 |
> + |
715 |
> + # STRIP_MASK supports bash brace expansion (well, it supports all types |
716 |
> + # of expansion, but we only bother with brace). People can also use |
717 |
> + # globs, but we let fnmatch handle that. Paths should be anchored to /. |
718 |
> + brace_re = re.compile(r'([^{]*){([^}]*,[^}]*)}(.*)') |
719 |
> + stripmask = environ.get('STRIP_MASK', '') |
720 |
> + |
721 |
> + def expand(expr): |
722 |
> + # This isn't terribly intelligent, but since the usage in the tree |
723 |
> + # is low (and limited to one or two expansions), we don't have to |
724 |
> + # worry about the pathological cases. |
725 |
> + m = brace_re.match(expr) |
726 |
> + if m: |
727 |
> + for x in m.group(2).split(','): |
728 |
> + expand(m.group(1) + x + m.group(3)) |
729 |
> + else: |
730 |
> + cls._stripmask.append(expr) |
731 |
> + |
732 |
> + for mask in stripmask.split(): |
733 |
> + expand(mask) |
734 |
> + |
735 |
> + @classmethod |
736 |
> + def val(cls, name): |
737 |
> + return cls._qa_vars.get(name, '') |
738 |
> + |
739 |
> + @classmethod |
740 |
> + def regex(cls, name): |
741 |
> + return cls._qa_re.get(name) |
742 |
> + |
743 |
> + @classmethod |
744 |
> + def stripmask(cls, path): |
745 |
> + for mask in cls._stripmask: |
746 |
> + if fnmatch.fnmatchcase(path, mask): |
747 |
> + return True |
748 |
> + return False |
749 |
> + |
750 |
> + |
751 |
> +def CheckStripped(q_prestripped, elf, path): |
752 |
> + """Warn about ELF files already stripped |
753 |
> + |
754 |
> + The existance of the section .symtab tells us that a binary is stripped. |
755 |
|
756 |
s/existance/existence/ |
757 |
|
758 |
> + We want to log already stripped binaries, as this may be a QA violation. |
759 |
> + They prevent us from getting the splitdebug data. |
760 |
> + """ |
761 |
> + for section in elf.iter_sections(): |
762 |
> + if section.name == '.symtab': |
763 |
> + return False |
764 |
> + else: |
765 |
> + # No .symtab! |
766 |
> + if q_prestripped: |
767 |
> + regex = Qa.regex('prestripped') |
768 |
> + if not regex.match(path): |
769 |
> + q_prestripped.put(path) |
770 |
> + return True |
771 |
> + |
772 |
> + |
773 |
> +@××××××××××.contextmanager |
774 |
> +def PreserveFileMetadata(path, mode=0600): |
775 |
> + """Temporarily make |path| readable/writable if need be |
776 |
> + |
777 |
> + In Prefix we are usually an unprivileged user, so we can't strip |
778 |
> + unwritable objects. Make them temporarily writable for the |
779 |
> + stripping. |
780 |
> + """ |
781 |
> + st = os.stat(path) |
782 |
> + usable = ((st.st_mode & mode) == mode) |
783 |
> + if not usable: |
784 |
> + os.chmod(path, st.st_mode | mode) |
785 |
> + try: |
786 |
> + with preserve_xattrs(path): |
787 |
> + yield st.st_mode |
788 |
> + finally: |
789 |
> + if not usable: |
790 |
> + os.chmod(path, st.st_mode) |
791 |
> + |
792 |
> + |
793 |
> +def MkdirP(path): |
794 |
> + """Create |path|, but don't fail if it exists""" |
795 |
> + try: |
796 |
> + os.makedirs(path) |
797 |
> + except EnvironmentError as e: |
798 |
> + # We might be doing this in parallel, so don't complain |
799 |
> + # if another thread created the dir for us. |
800 |
> + if e.errno != errno.EEXIST or not os.path.isdir(path): |
801 |
> + raise |
802 |
> + |
803 |
> + |
804 |
> +def Relink(src, dsts): |
805 |
> + """Link all the |dsts| paths to |src|""" |
806 |
> + for dst in dsts: |
807 |
> + os.unlink(dst) |
808 |
> + os.link(src, dst) |
809 |
> + |
810 |
> + |
811 |
> +def GetBuildId(elf): |
812 |
> + """Extract the build id from |elf|""" |
813 |
> + # TODO: Should add PT_NOTE parsing. |
814 |
> + for section in elf.iter_sections(): |
815 |
> + if section.name == '.note.gnu.build-id': |
816 |
> + return ''.join('%02x' % ord(x) for x in section.data()[16:]) |
817 |
> + |
818 |
> + |
819 |
> +def InstallSourcesProc(): |
820 |
> + """Launch a process for copying source files to the right place""" |
821 |
> + if not Features.installsources: |
822 |
> + return |
823 |
> + |
824 |
> + d_sources_dir = '%s%s' % (Paths.D, Paths.SOURCES_DIR) |
825 |
> + MkdirP(d_sources_dir) |
826 |
> + proc = subprocess.Popen([ |
827 |
> + 'rsync', '-tL0', '--chmod=ugo-st,a+r,go-w,Da+x,Fa-x', |
828 |
> + '--files-from=-', Paths.WORKDIR, '%s/' % (d_sources_dir), |
829 |
> + ], cwd=Paths.WORKDIR, stdin=subprocess.PIPE) |
830 |
> + setattr(proc, 'stdin_lock', multiprocessing.Lock()) |
831 |
> + return proc |
832 |
> + |
833 |
> + |
834 |
> +def RewriteElfSources(proc_installsources, path): |
835 |
> + """Save the sources for this file""" |
836 |
> + if not Features.installsources: |
837 |
> + return |
838 |
> + |
839 |
> + # Since we're editing the ELF here, we should recompute the build-id |
840 |
> + # (the -i flag below). Save that output so we don't need to recompute |
841 |
> + # it later on in the save_elf_debug step. |
842 |
> + with tempfile.NamedTemporaryFile(dir=Paths.T) as tmpfile: |
843 |
> + proc = Tools.run('debugedit', |
844 |
> + '-i', |
845 |
> + '-b', Paths.WORKDIR, |
846 |
> + '-d', Paths.SOURCES_DIR, |
847 |
> + '-l', tmpfile.name, |
848 |
> + path, stdout=subprocess.PIPE) |
849 |
> + with open(tmpfile.name) as f: |
850 |
> + proc_installsources.stdin_lock.acquire() |
851 |
> + proc_installsources.stdin.write(f.read()) |
852 |
> + proc_installsources.stdin_lock.release() |
853 |
> + return proc.stdout.read().strip() |
854 |
> + |
855 |
> + |
856 |
> +def SaveElfDebug(elf, path, linked_paths, mode, buildid=None, splitdebug=None): |
857 |
> + """Split off the debugging info for this file""" |
858 |
> + if not Features.splitdebug: |
859 |
> + return |
860 |
> + |
861 |
> + # Don't save debug info twice. |
862 |
> + if os.path.splitext(path)[1] == '.debug': |
863 |
> + return |
864 |
> + |
865 |
> + def _paths(p): |
866 |
> + root_path = p[len(Paths.D):] |
867 |
> + root_debug_path = '%s.debug' % (root_path) |
868 |
> + d_debug_path = '%s%s%s' % (Paths.ED, Paths.DEBUG_DIR, root_debug_path) |
869 |
> + MkdirP(os.path.dirname(d_debug_path)) |
870 |
> + return (root_path, root_debug_path, d_debug_path) |
871 |
> + |
872 |
> + root_path, root_debug_path, d_debug_path = _paths(path) |
873 |
> + |
874 |
> + # Set up the .debug file in /usr/lib/debug/. |
875 |
> + if splitdebug: |
876 |
> + os.rename(splitdebug, d_debug_path) |
877 |
> + else: |
878 |
> + # Split out the .debug file. |
879 |
> + flags = ['--only-keep-debug'] |
880 |
> + if Features.compressdebug: |
881 |
> + flags += ['--compress-debug-sections'] |
882 |
> + flags += [path, d_debug_path] |
883 |
> + Tools.run('objcopy', *flags) |
884 |
> + |
885 |
> + # Now link the ELF to the .debug file. Strip out the |
886 |
> + # old section name in case there was one (otherwise |
887 |
> + # objcopy will crap itself). |
888 |
> + flags = [ |
889 |
> + '--remove-section', '.gnu_debuglink', |
890 |
> + '--add-gnu-debuglink', d_debug_path, |
891 |
> + path, |
892 |
> + ] |
893 |
> + Tools.run('objcopy', *flags) |
894 |
> + |
895 |
> + # If we don't already have build-id from debugedit, look it up |
896 |
> + if not buildid: |
897 |
> + buildid = GetBuildId(elf) |
898 |
> + if buildid: |
899 |
> + buildid_dir = '%s%s/%s' % (Paths.ED, Paths.BUILDID_DIR, buildid[0:2]) |
900 |
> + buildid_file = '%s/%s' % (buildid_dir, buildid[2:]) |
901 |
> + MkdirP(buildid_dir) |
902 |
> + os.symlink('../../%s' % (root_debug_path.lstrip('/')), |
903 |
> + '%s.debug' % (buildid_file)) |
904 |
> + os.symlink(root_debug_path, buildid_file) |
905 |
> + |
906 |
> + # Create links for all the .debug files. |
907 |
> + for dst_path in linked_paths: |
908 |
> + _, _, dst_d_debug_path = _paths(dst_path) |
909 |
> + os.link(d_debug_path, dst_d_debug_path) |
910 |
> + # Make sure the .debug file has same perms as the original. |
911 |
> + os.chmod(d_debug_path, mode) |
912 |
> + |
913 |
> + |
914 |
> +def _StripFile(q_stripped, proc_installsources, prestripped, elf, path, |
915 |
> + strip_class, linked_paths, mode): |
916 |
> + """Do the actual stripping/splitdebug/etc...""" |
917 |
> + buildid = RewriteElfSources(proc_installsources, path) |
918 |
> + |
919 |
> + if not prestripped: |
920 |
> + if Features.strip: |
921 |
> + q_stripped.put((path, '')) |
922 |
> + else: |
923 |
> + q_stripped.put((path, 'not stripping due to FEATURES=nostrip')) |
924 |
> + |
925 |
> + # We don't copy xattrs from the source file to the splitdebug. |
926 |
> + # This is most likely what we want since the debug file is not |
927 |
> + # executable ... |
928 |
> + |
929 |
> + # See if we can split & strip at the same time. |
930 |
> + if Tools.strip_type() == 'elfutils': |
931 |
> + splitdebug = tempfile.NamedTemporaryFile(dir=Paths.T) |
932 |
> + shortname = '%s.debug' % (os.path.basename(path),) |
933 |
> + args = [ |
934 |
> + '-f', splitdebug.name, |
935 |
> + '-F', shortname, |
936 |
> + path, |
937 |
> + ] |
938 |
> + if not prestripped: |
939 |
> + Tools.run_strip(strip_class, *args) |
940 |
> + SaveElfDebug(elf, path, linked_paths, mode, buildid=buildid, splitdebug=splitdebug.name) |
941 |
> + |
942 |
> + else: # GNU |
943 |
> + SaveElfDebug(elf, path, linked_paths, mode, buildid=buildid) |
944 |
> + if not prestripped: |
945 |
> + Tools.run_strip(strip_class, path) |
946 |
> + |
947 |
> + |
948 |
> +def StripFile(q_stripped, q_prestripped, proc_installsources, path, linked_paths): |
949 |
> + """Strip |path|""" |
950 |
> + with PreserveFileMetadata(path) as mode: |
951 |
> + if path.endswith('.a'): |
952 |
> + # hmm, can we split debug/sources for .a ? |
953 |
> + q_stripped.put((path, '')) |
954 |
> + if Features.strip: |
955 |
> + Tools.run_strip(path, 'debug') |
956 |
> + Relink(path, linked_paths) |
957 |
> + return |
958 |
> + |
959 |
> + with open(path, 'rb+') as f: |
960 |
> + # Make sure this open fd doesn't bleed into children (`strip`). |
961 |
> + fcntl.fcntl(f, fcntl.F_SETFD, |
962 |
> + fcntl.fcntl(f, fcntl.F_GETFD) | fcntl.FD_CLOEXEC) |
963 |
> + # Grab a lock on this file so we can handle hardlinks #421099. |
964 |
> + #fcntl.lockf(f, fcntl.LOCK_EX) |
965 |
> + |
966 |
> + try: |
967 |
> + elf = ELFFile(f) |
968 |
> + except exceptions.ELFError: |
969 |
> + # Skip non-ELF files. |
970 |
> + return |
971 |
> + |
972 |
> + # If it's already stripped, there's nothing for us to do. |
973 |
> + # Check it here before FEATURES for the QA aspect. |
974 |
> + prestripped = CheckStripped(q_prestripped, elf, path) |
975 |
> + |
976 |
> + # First see if this thing has been masked. |
977 |
> + if Qa.stripmask(path): |
978 |
> + # Should pass down for sources saving ... |
979 |
> + q_stripped.put((path, 'skipped due to $STRIP_MASK')) |
980 |
> + for p in linked_paths: |
981 |
> + q_stripped.put((p, 'skipped due to hardlink to %s' % path)) |
982 |
> + return |
983 |
> + |
984 |
> + e_type = elf.header.e_type |
985 |
> + if e_type == 'ET_EXEC' or e_type == 'ET_DYN': |
986 |
|
987 |
if e_type in ('ET_EXEC', 'ET_DYN'): |
988 |
|
989 |
> + strip_class = 'portage' |
990 |
> + elif e_type == 'ET_REL': |
991 |
> + strip_class = 'safe' |
992 |
> + else: |
993 |
> + strip_class = None |
994 |
> + q_stripped.put((path, 'unknown ELF type %s' % e_type)) |
995 |
> + |
996 |
> + if strip_class: |
997 |
> + _StripFile(q_stripped, proc_installsources, prestripped, elf, |
998 |
> + path, strip_class, linked_paths, mode) |
999 |
> + Relink(path, linked_paths) |
1000 |
> + |
1001 |
> + |
1002 |
> +def ProcessFile(queue, hardlinks, path, ignore_symlink=True): |
1003 |
> + """Queue |path| for stripping""" |
1004 |
> + # Now queue the file immediately if it has no hardlinks, else |
1005 |
> + # delay it in the hardlinks dict for later processing. |
1006 |
> + st = os.lstat(path) |
1007 |
> + if not stat.S_ISLNK(st.st_mode) or not ignore_symlink: |
1008 |
> + if st.st_nlink > 1: |
1009 |
> + hardlinks.setdefault(st.st_ino, []) |
1010 |
> + hardlinks[st.st_ino].append(path) |
1011 |
> + else: |
1012 |
> + queue.put((path, [])) |
1013 |
> + |
1014 |
> + |
1015 |
> +def ProcessDir(queue, hardlinks, path): |
1016 |
> + """Queue all files found in |path| for stripping |
1017 |
> + |
1018 |
> + Recursively descend into |path| and locate all files for stripping |
1019 |
> + (ignoring symlinks and such). |
1020 |
> + """ |
1021 |
> + for root, _, files in os.walk(path, topdown=False): |
1022 |
> + for f in files: |
1023 |
> + ProcessFile(queue, hardlinks, os.path.join(root, f)) |
1024 |
> + |
1025 |
> + |
1026 |
> +def ProcessPaths(queue, hardlinks, paths): |
1027 |
> + """Queue all files found in |paths| for stripping |
1028 |
> + |
1029 |
> + This accepts both directories (which will be walked) and files. |
1030 |
> + Symlinks to files are processed at this point. |
1031 |
> + """ |
1032 |
> + for p in paths: |
1033 |
> + if os.path.isdir(p): |
1034 |
> + ProcessDir(queue, hardlinks, p) |
1035 |
> + else: |
1036 |
> + ProcessFile(queue, hardlinks, p, ignore_symlink=False) |
1037 |
> + |
1038 |
> + |
1039 |
> +def Prepstrip(paths, jobs=None, out=None): |
1040 |
> + """Do the stripping on |paths| in parallel""" |
1041 |
> + q_stripped = multiprocessing.Queue() |
1042 |
> + q_prestripped = None |
1043 |
> + if Features.binchecks: |
1044 |
> + q_prestripped = multiprocessing.Queue() |
1045 |
> + |
1046 |
> + proc_installsources = InstallSourcesProc() |
1047 |
> + |
1048 |
> + # Now do the actual stripping. |
1049 |
> + with parallel.BackgroundTaskRunner(StripFile, q_stripped, q_prestripped, |
1050 |
> + proc_installsources, processes=jobs) as queue: |
1051 |
> + # First queue up all files that are not hardlinks and strip them |
1052 |
> + # in the background. Hardlinks will be processed specially. |
1053 |
> + hardlinks = {} |
1054 |
> + ProcessPaths(queue, hardlinks, paths) |
1055 |
> + |
1056 |
> + # Since strip creates a new inode, we need to know the initial set of |
1057 |
> + # inodes in advance, so that we can avoid interference due to trying |
1058 |
> + # to strip the same (hardlinked) file multiple times in parallel. |
1059 |
> + # See bug #421099. |
1060 |
> + for paths in hardlinks.itervalues(): |
1061 |
> + queue.put((paths[0], paths[1:])) |
1062 |
> + |
1063 |
> + # Print out the summary. |
1064 |
> + stripped = [] |
1065 |
> + align = 0 |
1066 |
> + while not q_stripped.empty(): |
1067 |
> + path, reason = q_stripped.get() |
1068 |
> + path = path[len(Paths.D) + 1:] |
1069 |
> + align = max([align, len(path)]) |
1070 |
> + stripped.append((path, reason)) |
1071 |
> + if stripped: |
1072 |
> + stripped.sort(key=lambda x: x[0]) |
1073 |
> + flags = ' '.join(_shell_quote(x) for x in Tools.strip_flags('portage')) |
1074 |
> + print('%s: %s' % (Tools.strip, flags), file=out) |
1075 |
> + for path, reason in stripped: |
1076 |
> + if not reason: |
1077 |
> + print(' %s' % path, file=out) |
1078 |
> + else: |
1079 |
> + print(' %-*s # %s' % (align, path, reason), file=out) |
1080 |
> + |
1081 |
> + prestripped = [] |
1082 |
> + if q_prestripped: |
1083 |
> + while not q_prestripped.empty(): |
1084 |
> + prestripped.append(q_prestripped.get()) |
1085 |
> + prestripped.sort() |
1086 |
> + if prestripped: |
1087 |
> + eqawarn('QA Notice: Pre-stripped files found:', out=out) |
1088 |
> + for p in prestripped: |
1089 |
> + eqawarn(p, out=out) |
1090 |
> + |
1091 |
> + if Features.installsources is None: |
1092 |
> + ewarn('FEATURES=installsources is enabled but the debugedit binary could not', out=out) |
1093 |
> + ewarn('be found. This feature will not work unless debugedit is installed!', out=out) |
1094 |
> + elif Features.installsources: |
1095 |
> + # Preserve directory structure. |
1096 |
> + # Needed after running save_elf_sources. |
1097 |
> + # https://bugzilla.redhat.com/show_bug.cgi?id=444310 |
1098 |
> + for root, dirs, files in os.walk('%s%s' % (Paths.D, Paths.SOURCES_DIR)): |
1099 |
> + if not files and not dirs: |
1100 |
> + open(os.path.join(root, '.keepdir'), 'w').close() |
1101 |
> + |
1102 |
> + proc_installsources.stdin.close() |
1103 |
> + proc_installsources.wait() |
1104 |
> + |
1105 |
> + |
1106 |
> +def main(argv, environ=os.environ, out=None): |
1107 |
> + parser = ArgumentParser(description=__doc__) |
1108 |
> + parser.add_argument('paths', nargs='*') |
1109 |
> + parser.add_argument('-j', '--jobs', default=None, type=int, |
1110 |
> + help='Number of jobs to run in parallel ' |
1111 |
> + '(default: -j flag in $MAKEOPTS, else 1)') |
1112 |
> + parser.add_argument('--clean-debugdir', default=False, action='store_true', |
1113 |
> + help='Delete /usr/lib/debug first (useful for testing)') |
1114 |
> + opts = parser.parse_args(argv) |
1115 |
> + |
1116 |
> + Paths.cache(environ=environ) |
1117 |
> + Features.cache(environ=environ) |
1118 |
> + Tools.cache(environ=environ) |
1119 |
> + Qa.cache(environ=environ) |
1120 |
> + |
1121 |
> + if not opts.paths: |
1122 |
> + opts.paths = [Paths.ED] |
1123 |
> + if not opts.paths: |
1124 |
> + parser.error('need some paths to strip') |
1125 |
> + |
1126 |
> + if opts.jobs is None: |
1127 |
> + # XXX: Use a common func for this. |
1128 |
> + for flag in environ.get('MAKEOPTS', '').split(): |
1129 |
> + if flag.startswith('-j'): |
1130 |
> + opts.jobs = int(flag[2:].strip()) |
1131 |
> + break |
1132 |
> + else: |
1133 |
> + opts.jobs = 1 |
1134 |
> + |
1135 |
> + if opts.clean_debugdir: |
1136 |
> + for d in (Paths.SOURCES_DIR, Paths.DEBUG_DIR, Paths.BUILDID_DIR): |
1137 |
> + shutil.rmtree('%s%s' % (Paths.ED, d), ignore_errors=True) |
1138 |
> + |
1139 |
> + Prepstrip(opts.paths, jobs=opts.jobs, out=out) |
1140 |
> + |
1141 |
> + return os.EX_OK |
1142 |
> diff --git a/pym/portage/tests/bin/test_prepstrip.py b/pym/portage/tests/bin/test_prepstrip.py |
1143 |
> new file mode 100644 |
1144 |
> index 0000000..0bdff62 |
1145 |
> --- /dev/null |
1146 |
> +++ b/pym/portage/tests/bin/test_prepstrip.py |
1147 |
> @@ -0,0 +1,253 @@ |
1148 |
> +# test_prepstrip.py -- Portage Unit Testing Functionality |
1149 |
> +# Copyright 2007-2013 Gentoo Foundation |
1150 |
> +# Distributed under the terms of the GNU General Public License v2 |
1151 |
> + |
1152 |
> +from copy import deepcopy |
1153 |
> +import cStringIO |
1154 |
> +import glob |
1155 |
> +import inspect |
1156 |
> +import shutil |
1157 |
> +import sys |
1158 |
> + |
1159 |
> +from portage import os |
1160 |
> +from portage.bin import prepstrip |
1161 |
> +from portage.process import find_binary |
1162 |
> +from portage.tests.bin.setup_env import BinTestCase, dobin, exists_in_D |
1163 |
> +from portage.tests import TestCase |
1164 |
> + |
1165 |
> + |
1166 |
> +class PrepStrip(BinTestCase): |
1167 |
> + """Simple/directed tests of the interface (as seen by ebuilds)""" |
1168 |
> + |
1169 |
> + def testPrepStrip(self): |
1170 |
> + self.init() |
1171 |
> + try: |
1172 |
> + dobin("/bin/bash") |
1173 |
> + exists_in_D("/usr/bin/bash") |
1174 |
> + finally: |
1175 |
> + self.cleanup() |
1176 |
> + |
1177 |
> + |
1178 |
> +class PrepStripFull(TestCase): |
1179 |
> + """Full integration tests of the interface (as seen by ebuilds)""" |
1180 |
> + |
1181 |
> + CATEGORY = 'cat' |
1182 |
> + PN = 'somepackage' |
1183 |
> + PV = '1.2.3' |
1184 |
> + P = '%s-%s' % (PN, PV) |
1185 |
> + PF = P |
1186 |
> + |
1187 |
> + TESTDIR = os.path.realpath(__file__ + '/../testdir') |
1188 |
> + WORKDIR = os.path.join(TESTDIR, 'work') |
1189 |
> + S = os.path.join(WORKDIR, P) |
1190 |
> + T = '' |
1191 |
> + D = '' |
1192 |
> + |
1193 |
> + # We'll join this to D during setup. |
1194 |
> + DEBUG_DIR = 'usr/lib/debug' |
1195 |
> + SOURCES_DIR = 'usr/src/debug' |
1196 |
> + |
1197 |
> + def _setUp(self): |
1198 |
> + """Install the files to a test-specific root""" |
1199 |
> + name = inspect.stack()[1][3] |
1200 |
> + for v, d in (('D', 'image'), ('T', 'temp')): |
1201 |
> + d = os.path.join(self.TESTDIR, '%s.%s' % (d, name)) |
1202 |
> + setattr(self, v, d) |
1203 |
> + shutil.rmtree(d, ignore_errors=True) |
1204 |
> + os.makedirs(d) |
1205 |
> + for v in ('DEBUG_DIR', 'SOURCES_DIR'): |
1206 |
> + setattr(self, v, os.path.join(self.D, getattr(self, v))) |
1207 |
> + self._make('install', 'DESTDIR=%s' % self.D) |
1208 |
> + |
1209 |
> + def _make(self, *args): |
1210 |
> + """Run make!""" |
1211 |
> + cmd = ( |
1212 |
> + os.environ.get('MAKE', 'make'), |
1213 |
> + '-s', '-C', self.S, |
1214 |
> + ) + args |
1215 |
> + os.system(' '.join(cmd)) |
1216 |
> + |
1217 |
> + def _prepstrip(self, args, features='', restrict=''): |
1218 |
> + """Run prepstrip""" |
1219 |
> + environ = { |
1220 |
> + 'MAKEOPTS': '-j1', |
1221 |
> + |
1222 |
> + 'CATEGORY': self.CATEGORY, |
1223 |
> + 'PN': self.PN, |
1224 |
> + 'PV': self.PV, |
1225 |
> + 'P': self.P, |
1226 |
> + 'PF': self.PF, |
1227 |
> + |
1228 |
> + 'WORKDIR': self.WORKDIR, |
1229 |
> + 'S': self.S, |
1230 |
> + 'D': self.D, |
1231 |
> + 'T': self.T, |
1232 |
> + |
1233 |
> + 'FEATURES': features, |
1234 |
> + 'RESTRICT': restrict, |
1235 |
> + } |
1236 |
> + output = cStringIO.StringIO() |
1237 |
> + prepstrip.main(args, environ=environ, out=output) |
1238 |
> + return output |
1239 |
> + |
1240 |
> + def _sizes(self): |
1241 |
> + d = os.path.join(self.D, 'bin') |
1242 |
> + return [os.path.getsize(os.path.join(d, x)) for x in os.listdir(d)] |
1243 |
> + |
1244 |
> + @staticmethod |
1245 |
> + def _inode(path): |
1246 |
> + """Return the inode number for |path|""" |
1247 |
> + return os.stat(path).st_ino |
1248 |
> + |
1249 |
> + def _assertHardlinks(self, debugdir=False): |
1250 |
> + """Make sure hardlinks are still hardlinks""" |
1251 |
> + inodes = set() |
1252 |
> + dinodes = set() |
1253 |
> + for sfx in ('', '-1', '-2'): |
1254 |
> + p = os.path.join(self.D, 'bin', 'debug-hardlinked%s' % sfx) |
1255 |
> + inodes.add(self._inode(p)) |
1256 |
> + if debugdir: |
1257 |
> + p = os.path.join(self.DEBUG_DIR, 'bin', 'debug-hardlinked%s.debug' % sfx) |
1258 |
> + dinodes.add(self._inode(p)) |
1259 |
> + self.assertEqual(len(inodes), 1) |
1260 |
> + if debugdir: |
1261 |
> + self.assertEqual(len(dinodes), 1) |
1262 |
> + |
1263 |
> + def testStripSimple(self): |
1264 |
> + """Only strip objects""" |
1265 |
> + self._setUp() |
1266 |
> + before = self._sizes() |
1267 |
> + output = self._prepstrip([]) |
1268 |
> + after = self._sizes() |
1269 |
> + # Verify things were stripped by checking the file size. |
1270 |
> + self.assertNotEqual(before, after) |
1271 |
> + # We didn't split debug, so the dir should not exist. |
1272 |
> + self.assertNotExists(self.DEBUG_DIR) |
1273 |
> + # Make sure hardlinks didn't get messed up. |
1274 |
> + self._assertHardlinks() |
1275 |
> + # Verify QA pre-stripped check kicks in. |
1276 |
> + self.assertIn('QA Notice: Pre-stripped', output.getvalue()) |
1277 |
> + |
1278 |
> + def testNoStrip(self): |
1279 |
> + """Verify FEATURES=nostrip behavior""" |
1280 |
> + self._setUp() |
1281 |
> + before = self._sizes() |
1282 |
> + self._prepstrip([], features='nostrip') |
1283 |
> + after = self._sizes() |
1284 |
> + # Verify nothing was stripped by checking the file size. |
1285 |
> + self.assertEqual(before, after) |
1286 |
> + # Make sure hardlinks didn't get messed up. |
1287 |
> + self._assertHardlinks() |
1288 |
> + |
1289 |
> + def testNoBinChecks(self): |
1290 |
> + """Verify RESTRICT=binchecks behavior""" |
1291 |
> + self._setUp() |
1292 |
> + output = self._prepstrip([], restrict='binchecks') |
1293 |
> + # Verify QA pre-stripped checks were skipped. |
1294 |
> + self.assertNotIn('QA Notice: Pre-stripped', output.getvalue()) |
1295 |
> + # Make sure hardlinks didn't get messed up. |
1296 |
> + self._assertHardlinks() |
1297 |
> + |
1298 |
> + def testSplitdebug(self): |
1299 |
> + """Strip objects and check splitdebug""" |
1300 |
> + self._setUp() |
1301 |
> + self._prepstrip([], features='splitdebug') |
1302 |
> + # Verify things got split. |
1303 |
> + self.assertExists(os.path.join(self.DEBUG_DIR, 'bin', 'debug-unreadable.debug')) |
1304 |
> + self.assertExists(os.path.join(self.DEBUG_DIR, '.build-id')) |
1305 |
> + # Make sure hardlinks didn't get messed up. |
1306 |
> + self._assertHardlinks(debugdir=True) |
1307 |
> + |
1308 |
> + def testInstallSources(self): |
1309 |
> + """Strip objects and check sources""" |
1310 |
> + self._setUp() |
1311 |
> + self._prepstrip([], features='installsources') |
1312 |
> + # We didn't split debug, so the dir should not exist. |
1313 |
> + self.assertNotExists(self.DEBUG_DIR) |
1314 |
> + # Verify sources got copied. |
1315 |
> + self.assertExists(os.path.join( |
1316 |
> + self.SOURCES_DIR, self.CATEGORY, self.PF, self.PF, 'src', 'main.c')) |
1317 |
> + # Make sure hardlinks didn't get messed up. |
1318 |
> + self._assertHardlinks() |
1319 |
> + |
1320 |
> + |
1321 |
> +class PrepStripApiFeatures(TestCase): |
1322 |
> + """Unittests for FEATURES logic""" |
1323 |
> + |
1324 |
> + def _cache(self, features, env_features, env_restrict): |
1325 |
> + features.cache(environ={ |
1326 |
> + 'FEATURES': ' '.join(env_features), |
1327 |
> + 'RESTRICT': ' '.join(env_restrict), |
1328 |
> + }) |
1329 |
> + |
1330 |
> + def testDefault(self): |
1331 |
> + """Verify default Features works""" |
1332 |
> + features = deepcopy(prepstrip.Features) |
1333 |
> + self._cache(features, [], []) |
1334 |
> + self.assertTrue(features.binchecks) |
1335 |
> + self.assertFalse(features.compressdebug) |
1336 |
> + self.assertTrue(features.strip) |
1337 |
> + |
1338 |
> + def testRestrict(self): |
1339 |
> + """Check RESTRICT handling""" |
1340 |
> + features = deepcopy(prepstrip.Features) |
1341 |
> + |
1342 |
> + self._cache(features, [], []) |
1343 |
> + self.assertFalse(features.xattr) |
1344 |
> + features.reset() |
1345 |
> + |
1346 |
> + self._cache(features, ['xattr'], []) |
1347 |
> + self.assertTrue(features.xattr) |
1348 |
> + features.reset() |
1349 |
> + |
1350 |
> + self._cache(features, ['xattr'], ['xattr']) |
1351 |
> + self.assertFalse(features.xattr) |
1352 |
> + |
1353 |
> + def testNegatives(self): |
1354 |
> + """Check handling of nostrip""" |
1355 |
> + features = deepcopy(prepstrip.Features) |
1356 |
> + |
1357 |
> + self._cache(features, ['strip'], ['']) |
1358 |
> + self.assertTrue(features.strip) |
1359 |
> + features.reset() |
1360 |
> + |
1361 |
> + self._cache(features, ['strip'], ['strip']) |
1362 |
> + self.assertFalse(features.strip) |
1363 |
> + features.reset() |
1364 |
> + |
1365 |
> + self._cache(features, ['nostrip'], ['']) |
1366 |
> + self.assertFalse(features.strip) |
1367 |
> + |
1368 |
> + |
1369 |
> +class PrepStripApiTools(TestCase): |
1370 |
> + """Unittests for helper tool logic""" |
1371 |
> + |
1372 |
> + def testDefault(self): |
1373 |
> + """Verify basic sanity""" |
1374 |
> + tools = deepcopy(prepstrip.Tools) |
1375 |
> + tools.cache(environ={}) |
1376 |
> + self.assertEqual(tools.strip, 'strip') |
1377 |
> + |
1378 |
> + def testChost(self): |
1379 |
> + """Check looking up by CHOST prefix""" |
1380 |
> + tools = deepcopy(prepstrip.Tools) |
1381 |
> + objcopy = glob.glob('/usr/bin/*-objcopy') |
1382 |
> + if not objcopy: |
1383 |
> + # Maybe we should mock this stuff out. |
1384 |
> + return |
1385 |
> + objcopy = objcopy[0] |
1386 |
> + tools.cache(environ={'CHOST': objcopy[:-8]}) |
1387 |
> + self.assertEqual(tools.objcopy, objcopy) |
1388 |
> + |
1389 |
> + def testEnv(self): |
1390 |
> + """Check overriding by specific env var names""" |
1391 |
> + tools = deepcopy(prepstrip.Tools) |
1392 |
> + tools.cache(environ={'STRIP': 'true'}) |
1393 |
> + true = find_binary('true') |
1394 |
> + self.assertEqual(tools.strip, true) |
1395 |
> + |
1396 |
> + def testMissing(self): |
1397 |
> + """Check we get a sane value when user gives us crap""" |
1398 |
> + tools = deepcopy(prepstrip.Tools) |
1399 |
> + tools.cache(environ={'DEBUGEDIT': 'asldk19sdfj*!@af'}) |
1400 |
> + self.assertEqual(tools.debugedit, 'debugedit') |
1401 |
> diff --git a/pym/portage/tests/bin/testdir/.gitignore b/pym/portage/tests/bin/testdir/.gitignore |
1402 |
> new file mode 100644 |
1403 |
> index 0000000..31dbb9d |
1404 |
> --- /dev/null |
1405 |
> +++ b/pym/portage/tests/bin/testdir/.gitignore |
1406 |
> @@ -0,0 +1,2 @@ |
1407 |
> +image*/ |
1408 |
> +temp*/ |
1409 |
> diff --git a/pym/portage/tests/bin/testdir/work/somepackage-1.2.3/Makefile b/pym/portage/tests/bin/testdir/work/somepackage-1.2.3/Makefile |
1410 |
> new file mode 100644 |
1411 |
> index 0000000..3e73f61 |
1412 |
> --- /dev/null |
1413 |
> +++ b/pym/portage/tests/bin/testdir/work/somepackage-1.2.3/Makefile |
1414 |
> @@ -0,0 +1,65 @@ |
1415 |
> +src = src/main.c |
1416 |
> +L = $(CC) $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) -o $@ $< |
1417 |
> + |
1418 |
> +C_NO_DBG = -g0 |
1419 |
> +C_DBG = -g -ggdb |
1420 |
> +C_STRIP = -s |
1421 |
> +L_NO_BILD = -Wl,--build-id=none |
1422 |
> +L_BILD = -Wl,--build-id |
1423 |
> + |
1424 |
> +_PROGS = \ |
1425 |
> + debug-buildid \ |
1426 |
> + debug-no-buildid \ |
1427 |
> + no-debug-buildid \ |
1428 |
> + no-debug-no-buildid \ |
1429 |
> + strip-buildid \ |
1430 |
> + strip-no-buildid \ |
1431 |
> + debug-hardlinked \ |
1432 |
> + debug-hardlinked-1 \ |
1433 |
> + debug-hardlinked-2 \ |
1434 |
> + debug-softlinked \ |
1435 |
> + debug-softlinked-1 \ |
1436 |
> + debug-softlinked-2 \ |
1437 |
> + debug-unreadable |
1438 |
> +PROGS = $(addprefix o/,$(_PROGS)) |
1439 |
> + |
1440 |
> +all: $(PROGS) |
1441 |
> +clean:; rm -f o/* |
1442 |
> +%: $(src); $(L) |
1443 |
> + |
1444 |
> +o/debug-buildid: CFLAGS += $(C_DBG) |
1445 |
> +o/debug-buildid: LDFLAGS += $(L_BILD) |
1446 |
> +o/debug-no-buildid: CFLAGS += $(C_DBG) |
1447 |
> +o/debug-no-buildid: LDFLAGS += $(L_NO_BILD) |
1448 |
> +o/no-debug-buildid: CFLAGS += $(C_NO_DBG) |
1449 |
> +o/no-debug-buildid: LDFLAGS += $(L_BILD) |
1450 |
> +o/no-debug-no-buildid: CFLAGS += $(C_NO_DBG) |
1451 |
> +o/no-debug-no-buildid: LDFLAGS += $(L_NO_BILD) |
1452 |
> +o/strip-buildid: CFLAGS += $(C_STRIP) |
1453 |
> +o/strip-buildid: LDFLAGS += $(L_BILD) |
1454 |
> +o/strip-no-buildid: CFLAGS += $(C_STRIP) |
1455 |
> +o/strip-no-buildid: LDFLAGS += $(L_NO_BILD) |
1456 |
> + |
1457 |
> +o/debug-hardlinked: CFLAGS += $(C_DBG) |
1458 |
> +o/debug-hardlinked-1: o/debug-hardlinked; ln -f $< $@ |
1459 |
> +o/debug-hardlinked-2: o/debug-hardlinked; ln -f $< $@ |
1460 |
> +o/debug-softlinked: CFLAGS += $(C_DBG) |
1461 |
> +o/debug-softlinked-1: o/debug-softlinked; ln -sf $(<F) $@ |
1462 |
> +o/debug-softlinked-2: o/debug-softlinked; ln -sf $(<F) $@ |
1463 |
> + |
1464 |
> +o/debug-unreadable: CFLAGS += $(C_DBG) |
1465 |
> +#debug-unreadable: $(src) |
1466 |
> +# $(L) |
1467 |
> +# chmod 000 $@ |
1468 |
> + |
1469 |
> +#gnulink-debug-no-buildid |
1470 |
> +#--add-gnu-debuglink=path-to-file |
1471 |
> + |
1472 |
> +DESTDIR = $(PWD)/../../image |
1473 |
> +install: $(PROGS) |
1474 |
> + rm -rf $(DESTDIR) |
1475 |
> + mkdir -p $(DESTDIR)/bin |
1476 |
> + rsync -aH $(PROGS) $(DESTDIR)/bin/ |
1477 |
> + chmod 000 $(DESTDIR)/bin/debug-unreadable |
1478 |
> + |
1479 |
> +.PHONY: all clean install |
1480 |
> diff --git a/pym/portage/tests/bin/testdir/work/somepackage-1.2.3/o/.gitignore b/pym/portage/tests/bin/testdir/work/somepackage-1.2.3/o/.gitignore |
1481 |
> new file mode 100644 |
1482 |
> index 0000000..72e8ffc |
1483 |
> --- /dev/null |
1484 |
> +++ b/pym/portage/tests/bin/testdir/work/somepackage-1.2.3/o/.gitignore |
1485 |
> @@ -0,0 +1 @@ |
1486 |
> +* |
1487 |
> diff --git a/pym/portage/tests/bin/testdir/work/somepackage-1.2.3/src/main.c b/pym/portage/tests/bin/testdir/work/somepackage-1.2.3/src/main.c |
1488 |
> new file mode 100644 |
1489 |
> index 0000000..9989d20 |
1490 |
> --- /dev/null |
1491 |
> +++ b/pym/portage/tests/bin/testdir/work/somepackage-1.2.3/src/main.c |
1492 |
> @@ -0,0 +1,5 @@ |
1493 |
> +#include <stdio.h> |
1494 |
> +int main() { |
1495 |
> + puts("hi"); |
1496 |
> + return 0; |
1497 |
> +} |
1498 |
> diff --git a/pym/portage/util/parallel.py b/pym/portage/util/parallel.py |
1499 |
> new file mode 100644 |
1500 |
> index 0000000..068f0ae |
1501 |
> --- /dev/null |
1502 |
> +++ b/pym/portage/util/parallel.py |
1503 |
> @@ -0,0 +1,598 @@ |
1504 |
> +# Copyright (c) 2012 The Chromium OS Authors. All rights reserved. |
1505 |
> +# Use of this source code is governed by a BSD-style license that can be |
1506 |
> +# found in the LICENSE file. |
1507 |
> + |
1508 |
> +"""Module for running cbuildbot stages in the background.""" |
1509 |
> + |
1510 |
> +import collections |
1511 |
> +import contextlib |
1512 |
> +import errno |
1513 |
> +import functools |
1514 |
> +import logging |
1515 |
> +import multiprocessing |
1516 |
> +import os |
1517 |
> +import Queue |
1518 |
> +import signal |
1519 |
> +import sys |
1520 |
> +import tempfile |
1521 |
> +import time |
1522 |
> +import traceback |
1523 |
> + |
1524 |
> + |
1525 |
> +_BUFSIZE = 1024 |
1526 |
> + |
1527 |
> +logger = logging.getLogger(__name__) |
1528 |
> + |
1529 |
> + |
1530 |
> +class BackgroundFailure(Exception): |
1531 |
> + """A failure happened in the background""" |
1532 |
> + |
1533 |
> + |
1534 |
> +class _BackgroundTask(multiprocessing.Process): |
1535 |
> + """Run a task in the background. |
1536 |
> + |
1537 |
> + This task may be the 'Run' function from a buildbot stage or just a plain |
1538 |
> + function. It will be run in the background. Output from this task is saved |
1539 |
> + to a temporary file and is printed when the 'Wait' function is called. |
1540 |
> + """ |
1541 |
> + |
1542 |
> + # The time we give Python to startup and exit. |
1543 |
> + STARTUP_TIMEOUT = 60 * 5 |
1544 |
> + EXIT_TIMEOUT = 60 * 10 |
1545 |
> + |
1546 |
> + # The time we allow processes to be silent. This must be greater than the |
1547 |
> + # hw_test_timeout set in cbuildbot_config.py, and less than the timeout set |
1548 |
> + # by buildbot itself (typically, 150 minutes.) |
1549 |
> + SILENT_TIMEOUT = 60 * 145 |
1550 |
> + |
1551 |
> + # The amount by which we reduce the SILENT_TIMEOUT every time we launch |
1552 |
> + # a subprocess. This helps ensure that children get a chance to enforce the |
1553 |
> + # SILENT_TIMEOUT prior to the parents enforcing it. |
1554 |
> + SILENT_TIMEOUT_STEP = 30 |
1555 |
> + MINIMUM_SILENT_TIMEOUT = 60 * 135 |
1556 |
> + |
1557 |
> + # The time before terminating or killing a task. |
1558 |
> + SIGTERM_TIMEOUT = 30 |
1559 |
> + SIGKILL_TIMEOUT = 60 |
1560 |
> + |
1561 |
> + # Interval we check for updates from print statements. |
1562 |
> + PRINT_INTERVAL = 1 |
1563 |
> + |
1564 |
> + def __init__(self, task, semaphore=None, task_args=None, task_kwargs=None): |
1565 |
> + """Create a new _BackgroundTask object. |
1566 |
> + |
1567 |
> + If semaphore is supplied, it will be acquired for the duration of the |
1568 |
> + steps that are run in the background. This can be used to limit the |
1569 |
> + number of simultaneous parallel tasks. |
1570 |
> + |
1571 |
> + Args: |
1572 |
> + task: The task (a functor) to run in the background. |
1573 |
> + semaphore: The lock to hold while |task| runs. |
1574 |
> + task_args: A list of args to pass to the |task|. |
1575 |
> + task_kwargs: A dict of optional args to pass to the |task|. |
1576 |
> + """ |
1577 |
> + multiprocessing.Process.__init__(self) |
1578 |
> + self._task = task |
1579 |
> + self._queue = multiprocessing.Queue() |
1580 |
> + self._semaphore = semaphore |
1581 |
> + self._started = multiprocessing.Event() |
1582 |
> + self._killing = multiprocessing.Event() |
1583 |
> + self._output = None |
1584 |
> + self._parent_pid = None |
1585 |
> + self._task_args = task_args if task_args else () |
1586 |
> + self._task_kwargs = task_kwargs if task_kwargs else {} |
1587 |
> + |
1588 |
> + def _WaitForStartup(self): |
1589 |
> + # TODO(davidjames): Use python-2.7 syntax to simplify this. |
1590 |
> + self._started.wait(self.STARTUP_TIMEOUT) |
1591 |
> + msg = 'Process failed to start in %d seconds' % self.STARTUP_TIMEOUT |
1592 |
> + assert self._started.is_set(), msg |
1593 |
> + |
1594 |
> + def Kill(self, sig, log_level): |
1595 |
> + """Kill process with signal, ignoring if the process is dead. |
1596 |
> + |
1597 |
> + Args: |
1598 |
> + sig: Signal to send. |
1599 |
> + log_level: The log level of log messages. |
1600 |
> + """ |
1601 |
> + self._killing.set() |
1602 |
> + self._WaitForStartup() |
1603 |
> + if logger.isEnabledFor(log_level): |
1604 |
> + logger.log(log_level, 'Killing %r (sig=%r)', self.pid, sig) |
1605 |
> + |
1606 |
> + try: |
1607 |
> + os.kill(self.pid, sig) |
1608 |
> + except OSError as ex: |
1609 |
> + if ex.errno != errno.ESRCH: |
1610 |
> + raise |
1611 |
> + |
1612 |
> + def Cleanup(self, silent=False): |
1613 |
> + """Wait for a process to exit.""" |
1614 |
> + if os.getpid() != self._parent_pid or self._output is None: |
1615 |
> + return |
1616 |
> + try: |
1617 |
> + # Print output from subprocess. |
1618 |
> + if not silent and logger.isEnabledFor(logging.DEBUG): |
1619 |
> + with open(self._output.name, 'r') as f: |
1620 |
> + for line in f: |
1621 |
> + logging.debug(line.rstrip('\n')) |
1622 |
> + finally: |
1623 |
> + # Clean up our temporary file. |
1624 |
> + os.unlink(self._output.name) |
1625 |
> + self._output.close() |
1626 |
> + self._output = None |
1627 |
> + |
1628 |
> + def Wait(self): |
1629 |
> + """Wait for the task to complete. |
1630 |
> + |
1631 |
> + Output from the task is printed as it runs. |
1632 |
> + |
1633 |
> + If an exception occurs, return a string containing the traceback. |
1634 |
> + """ |
1635 |
> + try: |
1636 |
> + # Flush stdout and stderr to be sure no output is interleaved. |
1637 |
> + sys.stdout.flush() |
1638 |
> + sys.stderr.flush() |
1639 |
> + |
1640 |
> + # File position pointers are shared across processes, so we must open |
1641 |
> + # our own file descriptor to ensure output is not lost. |
1642 |
> + self._WaitForStartup() |
1643 |
> + silent_death_time = time.time() + self.SILENT_TIMEOUT |
1644 |
> + results = [] |
1645 |
> + with open(self._output.name, 'r') as output: |
1646 |
> + pos = 0 |
1647 |
> + running, exited_cleanly, msg, error = (True, False, None, None) |
1648 |
> + while running: |
1649 |
> + # Check whether the process is still alive. |
1650 |
> + running = self.is_alive() |
1651 |
> + |
1652 |
> + try: |
1653 |
> + error = self._queue.get(True, self.PRINT_INTERVAL)[0] |
1654 |
> + running = False |
1655 |
> + exited_cleanly = True |
1656 |
> + except Queue.Empty: |
1657 |
> + pass |
1658 |
> + |
1659 |
> + if not running: |
1660 |
> + # Wait for the process to actually exit. If the child doesn't exit |
1661 |
> + # in a timely fashion, kill it. |
1662 |
> + self.join(self.EXIT_TIMEOUT) |
1663 |
> + if self.exitcode is None: |
1664 |
> + msg = '%r hung for %r seconds' % (self, self.EXIT_TIMEOUT) |
1665 |
> + self._KillChildren([self]) |
1666 |
> + elif not exited_cleanly: |
1667 |
> + msg = ('%r exited unexpectedly with code %s' % |
1668 |
> + (self, self.EXIT_TIMEOUT)) |
1669 |
> + # Read output from process. |
1670 |
> + output.seek(pos) |
1671 |
> + buf = output.read(_BUFSIZE) |
1672 |
> + |
1673 |
> + if len(buf) > 0: |
1674 |
> + silent_death_time = time.time() + self.SILENT_TIMEOUT |
1675 |
> + elif running and time.time() > silent_death_time: |
1676 |
> + msg = ('No output from %r for %r seconds' % |
1677 |
> + (self, self.SILENT_TIMEOUT)) |
1678 |
> + self._KillChildren([self]) |
1679 |
> + |
1680 |
> + # Read remaining output from the process. |
1681 |
> + output.seek(pos) |
1682 |
> + buf = output.read(_BUFSIZE) |
1683 |
> + running = False |
1684 |
> + |
1685 |
> + # Print output so far. |
1686 |
> + while len(buf) > 0: |
1687 |
> + sys.stdout.write(buf) |
1688 |
> + pos += len(buf) |
1689 |
> + if len(buf) < _BUFSIZE: |
1690 |
> + break |
1691 |
> + buf = output.read(_BUFSIZE) |
1692 |
> + |
1693 |
> + # Print error messages if anything exceptional occurred. |
1694 |
> + if msg: |
1695 |
> + error = '\n'.join(x for x in (error, msg) if x) |
1696 |
> + logger.warning(error) |
1697 |
> + traceback.print_stack() |
1698 |
> + |
1699 |
> + sys.stdout.flush() |
1700 |
> + sys.stderr.flush() |
1701 |
> + |
1702 |
> + finally: |
1703 |
> + self.Cleanup(silent=True) |
1704 |
> + |
1705 |
> + # If a traceback occurred, return it. |
1706 |
> + return error |
1707 |
> + |
1708 |
> + def start(self): |
1709 |
> + """Invoke multiprocessing.Process.start after flushing output/err.""" |
1710 |
> + if self.SILENT_TIMEOUT < self.MINIMUM_SILENT_TIMEOUT: |
1711 |
> + raise AssertionError('Maximum recursion depth exceeded in %r' % self) |
1712 |
> + |
1713 |
> + sys.stdout.flush() |
1714 |
> + sys.stderr.flush() |
1715 |
> + self._output = tempfile.NamedTemporaryFile(delete=False, bufsize=0, |
1716 |
> + prefix='chromite-parallel-') |
1717 |
> + self._parent_pid = os.getpid() |
1718 |
> + return multiprocessing.Process.start(self) |
1719 |
> + |
1720 |
> + def run(self): |
1721 |
> + """Run the list of steps.""" |
1722 |
> + if self._semaphore is not None: |
1723 |
> + self._semaphore.acquire() |
1724 |
> + |
1725 |
> + error = 'Unexpected exception in %r' % self |
1726 |
> + pid = os.getpid() |
1727 |
> + try: |
1728 |
> + error = self._Run() |
1729 |
> + finally: |
1730 |
> + if not self._killing.is_set() and os.getpid() == pid: |
1731 |
> + self._queue.put((error,)) |
1732 |
> + if self._semaphore is not None: |
1733 |
> + self._semaphore.release() |
1734 |
> + |
1735 |
> + def _Run(self): |
1736 |
> + """Internal method for running the list of steps.""" |
1737 |
> + |
1738 |
> + # The default handler for SIGINT sometimes forgets to actually raise the |
1739 |
> + # exception (and we can reproduce this using unit tests), so we define a |
1740 |
> + # custom one instead. |
1741 |
> + def kill_us(_sig_num, _frame): |
1742 |
> + raise KeyboardInterrupt('SIGINT received') |
1743 |
> + signal.signal(signal.SIGINT, kill_us) |
1744 |
> + |
1745 |
> + sys.stdout.flush() |
1746 |
> + sys.stderr.flush() |
1747 |
> + # Send all output to a named temporary file. |
1748 |
> + with open(self._output.name, 'w', 0) as output: |
1749 |
> + # Back up sys.std{err,out}. These aren't used, but we keep a copy so |
1750 |
> + # that they aren't garbage collected. We intentionally don't restore |
1751 |
> + # the old stdout and stderr at the end, because we want shutdown errors |
1752 |
> + # to also be sent to the same log file. |
1753 |
> + _orig_stdout, _orig_stderr = sys.stdout, sys.stderr |
1754 |
> + |
1755 |
> + # Replace std{out,err} with unbuffered file objects. |
1756 |
> + os.dup2(output.fileno(), sys.__stdout__.fileno()) |
1757 |
> + os.dup2(output.fileno(), sys.__stderr__.fileno()) |
1758 |
> + sys.stdout = os.fdopen(sys.__stdout__.fileno(), 'w', 0) |
1759 |
> + sys.stderr = os.fdopen(sys.__stderr__.fileno(), 'w', 0) |
1760 |
> + |
1761 |
> + error = None |
1762 |
> + try: |
1763 |
> + self._started.set() |
1764 |
> + |
1765 |
> + # Reduce the silent timeout by the prescribed amount. |
1766 |
> + cls = self.__class__ |
1767 |
> + cls.SILENT_TIMEOUT -= cls.SILENT_TIMEOUT_STEP |
1768 |
> + |
1769 |
> + # Actually launch the task. |
1770 |
> + self._task(*self._task_args, **self._task_kwargs) |
1771 |
> + except BaseException as ex: |
1772 |
|
1773 |
Unused variable ex |
1774 |
|
1775 |
> + error = traceback.format_exc() |
1776 |
> + if self._killing.is_set(): |
1777 |
> + traceback.print_exc() |
1778 |
> + finally: |
1779 |
> + sys.stdout.flush() |
1780 |
> + sys.stderr.flush() |
1781 |
> + |
1782 |
> + return error |
1783 |
> + |
1784 |
> + @classmethod |
1785 |
> + def _KillChildren(cls, bg_tasks, log_level=logging.WARNING): |
1786 |
> + """Kill a deque of background tasks. |
1787 |
> + |
1788 |
> + This is needed to prevent hangs in the case where child processes refuse |
1789 |
> + to exit. |
1790 |
> + |
1791 |
> + Arguments: |
1792 |
> + bg_tasks: A list filled with _BackgroundTask objects. |
1793 |
> + log_level: The log level of log messages. |
1794 |
> + """ |
1795 |
> + logger.log(log_level, 'Killing tasks: %r', bg_tasks) |
1796 |
> + signals = ((signal.SIGINT, cls.SIGTERM_TIMEOUT), |
1797 |
> + (signal.SIGTERM, cls.SIGKILL_TIMEOUT), |
1798 |
> + (signal.SIGKILL, None)) |
1799 |
> + for sig, timeout in signals: |
1800 |
> + # Send signal to all tasks. |
1801 |
> + for task in bg_tasks: |
1802 |
> + task.Kill(sig, log_level) |
1803 |
> + |
1804 |
> + # Wait for all tasks to exit, if requested. |
1805 |
> + if timeout is None: |
1806 |
> + for task in bg_tasks: |
1807 |
> + task.join() |
1808 |
> + task.Cleanup() |
1809 |
> + break |
1810 |
> + |
1811 |
> + # Wait until timeout expires. |
1812 |
> + end_time = time.time() + timeout |
1813 |
> + while bg_tasks: |
1814 |
> + time_left = end_time - time.time() |
1815 |
> + if time_left <= 0: |
1816 |
> + break |
1817 |
> + task = bg_tasks[-1] |
1818 |
> + task.join(time_left) |
1819 |
> + if task.exitcode is not None: |
1820 |
> + task.Cleanup() |
1821 |
> + bg_tasks.pop() |
1822 |
> + |
1823 |
> + @classmethod |
1824 |
> + @contextlib.contextmanager |
1825 |
> + def ParallelTasks(cls, steps, max_parallel=None, halt_on_error=False): |
1826 |
> + """Run a list of functions in parallel. |
1827 |
> + |
1828 |
> + This function launches the provided functions in the background, yields, |
1829 |
> + and then waits for the functions to exit. |
1830 |
> + |
1831 |
> + The output from the functions is saved to a temporary file and printed as if |
1832 |
> + they were run in sequence. |
1833 |
> + |
1834 |
> + If exceptions occur in the steps, we join together the tracebacks and print |
1835 |
> + them after all parallel tasks have finished running. Further, a |
1836 |
> + BackgroundFailure is raised with full stack traces of all exceptions. |
1837 |
> + |
1838 |
> + Args: |
1839 |
> + steps: A list of functions to run. |
1840 |
> + max_parallel: The maximum number of simultaneous tasks to run in parallel. |
1841 |
> + By default, run all tasks in parallel. |
1842 |
> + halt_on_error: After the first exception occurs, halt any running steps, |
1843 |
> + and squelch any further output, including any exceptions that might |
1844 |
> + occur. |
1845 |
> + """ |
1846 |
> + |
1847 |
> + semaphore = None |
1848 |
> + if max_parallel is not None: |
1849 |
> + semaphore = multiprocessing.Semaphore(max_parallel) |
1850 |
> + |
1851 |
> + # First, start all the steps. |
1852 |
> + bg_tasks = collections.deque() |
1853 |
> + for step in steps: |
1854 |
> + task = cls(step, semaphore=semaphore) |
1855 |
> + task.start() |
1856 |
> + bg_tasks.append(task) |
1857 |
> + |
1858 |
> + try: |
1859 |
> + yield |
1860 |
> + finally: |
1861 |
> + # Wait for each step to complete. |
1862 |
> + tracebacks = [] |
1863 |
> + while bg_tasks: |
1864 |
> + task = bg_tasks.popleft() |
1865 |
> + error = task.Wait() |
1866 |
> + if error is not None: |
1867 |
> + tracebacks.append(error) |
1868 |
> + if halt_on_error: |
1869 |
> + break |
1870 |
> + |
1871 |
> + # If there are still tasks left, kill them. |
1872 |
> + if bg_tasks: |
1873 |
> + cls._KillChildren(bg_tasks, log_level=logging.DEBUG) |
1874 |
> + |
1875 |
> + # Propagate any exceptions. |
1876 |
> + if tracebacks: |
1877 |
> + raise BackgroundFailure('\n' + ''.join(tracebacks)) |
1878 |
> + |
1879 |
> + @staticmethod |
1880 |
> + def TaskRunner(queue, task, onexit=None, task_args=None, task_kwargs=None): |
1881 |
> + """Run task(*input) for each input in the queue. |
1882 |
> + |
1883 |
> + Returns when it encounters an _AllTasksComplete object on the queue. |
1884 |
> + If exceptions occur, save them off and re-raise them as a |
1885 |
> + BackgroundFailure once we've finished processing the items in the queue. |
1886 |
> + |
1887 |
> + Args: |
1888 |
> + queue: A queue of tasks to run. Add tasks to this queue, and they will |
1889 |
> + be run. |
1890 |
> + task: Function to run on each queued input. |
1891 |
> + onexit: Function to run after all inputs are processed. |
1892 |
> + task_args: A list of args to pass to the |task|. |
1893 |
> + task_kwargs: A dict of optional args to pass to the |task|. |
1894 |
> + """ |
1895 |
> + if task_args is None: |
1896 |
> + task_args = [] |
1897 |
> + elif not isinstance(task_args, list): |
1898 |
> + task_args = list(task_args) |
1899 |
> + if task_kwargs is None: |
1900 |
> + task_kwargs = {} |
1901 |
> + |
1902 |
> + tracebacks = [] |
1903 |
> + while True: |
1904 |
> + # Wait for a new item to show up on the queue. This is a blocking wait, |
1905 |
> + # so if there's nothing to do, we just sit here. |
1906 |
> + x = queue.get() |
1907 |
> + if isinstance(x, _AllTasksComplete): |
1908 |
> + # All tasks are complete, so we should exit. |
1909 |
> + break |
1910 |
> + elif not isinstance(x, list): |
1911 |
> + x = task_args + list(x) |
1912 |
> + else: |
1913 |
> + x = task_args + x |
1914 |
> + |
1915 |
> + # If no tasks failed yet, process the remaining tasks. |
1916 |
> + if not tracebacks: |
1917 |
> + try: |
1918 |
> + task(*x, **task_kwargs) |
1919 |
> + except BaseException: |
1920 |
> + tracebacks.append(traceback.format_exc()) |
1921 |
> + |
1922 |
> + # Run exit handlers. |
1923 |
> + if onexit: |
1924 |
> + onexit() |
1925 |
> + |
1926 |
> + # Propagate any exceptions. |
1927 |
> + if tracebacks: |
1928 |
> + raise BackgroundFailure('\n' + ''.join(tracebacks)) |
1929 |
> + |
1930 |
> + |
1931 |
> +def RunParallelSteps(steps, max_parallel=None, halt_on_error=False, |
1932 |
> + return_values=False): |
1933 |
> + """Run a list of functions in parallel. |
1934 |
> + |
1935 |
> + This function blocks until all steps are completed. |
1936 |
> + |
1937 |
> + The output from the functions is saved to a temporary file and printed as if |
1938 |
> + they were run in sequence. |
1939 |
> + |
1940 |
> + If exceptions occur in the steps, we join together the tracebacks and print |
1941 |
> + them after all parallel tasks have finished running. Further, a |
1942 |
> + BackgroundFailure is raised with full stack traces of all exceptions. |
1943 |
> + |
1944 |
> + Args: |
1945 |
> + steps: A list of functions to run. |
1946 |
> + max_parallel: The maximum number of simultaneous tasks to run in parallel. |
1947 |
> + By default, run all tasks in parallel. |
1948 |
> + halt_on_error: After the first exception occurs, halt any running steps, |
1949 |
> + and squelch any further output, including any exceptions that might occur. |
1950 |
> + return_values: If set to True, RunParallelSteps returns a list containing |
1951 |
> + the return values of the steps. Defaults to False. |
1952 |
> + |
1953 |
> + Returns: |
1954 |
> + If |return_values| is True, the function will return a list containing the |
1955 |
> + return values of the steps. |
1956 |
> + |
1957 |
> + Example: |
1958 |
> + # This snippet will execute in parallel: |
1959 |
> + # somefunc() |
1960 |
> + # anotherfunc() |
1961 |
> + # funcfunc() |
1962 |
> + steps = [somefunc, anotherfunc, funcfunc] |
1963 |
> + RunParallelSteps(steps) |
1964 |
> + # Blocks until all calls have completed. |
1965 |
> + """ |
1966 |
> + def ReturnWrapper(queue, fn): |
1967 |
> + """A function that """ |
1968 |
> + queue.put(fn()) |
1969 |
> + |
1970 |
> + full_steps = [] |
1971 |
> + queues = [] |
1972 |
> + manager = None |
1973 |
> + if return_values: |
1974 |
> + # We use a managed queue here, because the child process will wait for the |
1975 |
> + # queue(pipe) to be flushed (i.e., when items are read from the queue) |
1976 |
> + # before exiting, and with a regular queue this may result in hangs for |
1977 |
> + # large return values. But with a managed queue, the manager process will |
1978 |
> + # read the items and hold on to them until the managed queue goes out of |
1979 |
> + # scope and is cleaned up. |
1980 |
> + manager = multiprocessing.Manager() |
1981 |
> + for step in steps: |
1982 |
> + # pylint: disable=E1101 |
1983 |
> + queue = manager.Queue() |
1984 |
> + queues.append(queue) |
1985 |
> + full_steps.append(functools.partial(ReturnWrapper, queue, step)) |
1986 |
> + else: |
1987 |
> + full_steps = steps |
1988 |
> + |
1989 |
> + with _BackgroundTask.ParallelTasks(full_steps, max_parallel=max_parallel, |
1990 |
> + halt_on_error=halt_on_error): |
1991 |
> + pass |
1992 |
> + |
1993 |
> + if return_values: |
1994 |
> + return [queue.get_nowait() for queue in queues] |
1995 |
> + |
1996 |
> + |
1997 |
> +class _AllTasksComplete(object): |
1998 |
> + """Sentinel object to indicate that all tasks are complete.""" |
1999 |
> + |
2000 |
> + |
2001 |
> +@××××××××××.contextmanager |
2002 |
> +def BackgroundTaskRunner(task, *args, **kwargs): |
2003 |
> + """Run the specified task on each queued input in a pool of processes. |
2004 |
> + |
2005 |
> + This context manager starts a set of workers in the background, who each |
2006 |
> + wait for input on the specified queue. For each input on the queue, these |
2007 |
> + workers run task(*args + *input, **kwargs). Note that certain kwargs will |
2008 |
> + not pass through to the task (see Args below for the list). |
2009 |
> + |
2010 |
> + The output from these tasks is saved to a temporary file. When control |
2011 |
> + returns to the context manager, the background output is printed in order, |
2012 |
> + as if the tasks were run in sequence. |
2013 |
> + |
2014 |
> + If exceptions occur in the steps, we join together the tracebacks and print |
2015 |
> + them after all parallel tasks have finished running. Further, a |
2016 |
> + BackgroundFailure is raised with full stack traces of all exceptions. |
2017 |
> + |
2018 |
> + Example: |
2019 |
> + # This will run somefunc(1, 'small', 'cow', foo='bar' in the background |
2020 |
> + # while "more random stuff" is being executed. |
2021 |
> + |
2022 |
> + def somefunc(arg1, arg2, arg3, foo=None): |
2023 |
> + ... |
2024 |
> + ... |
2025 |
> + with BackgroundTaskRunner(somefunc, 1, foo='bar') as queue: |
2026 |
> + ... do random stuff ... |
2027 |
> + queue.put(['small', 'cow']) |
2028 |
> + ... do more random stuff ... |
2029 |
> + # Exiting the with statement will block until all calls have completed. |
2030 |
> + |
2031 |
> + Args: |
2032 |
> + task: Function to run on each queued input. |
2033 |
> + queue: A queue of tasks to run. Add tasks to this queue, and they will |
2034 |
> + be run in the background. If None, one will be created on the fly. |
2035 |
> + processes: Number of processes to launch. |
2036 |
> + onexit: Function to run in each background process after all inputs are |
2037 |
> + processed. |
2038 |
> + """ |
2039 |
> + |
2040 |
> + queue = kwargs.pop('queue', None) |
2041 |
> + processes = kwargs.pop('processes', None) |
2042 |
> + onexit = kwargs.pop('onexit', None) |
2043 |
> + |
2044 |
> + if queue is None: |
2045 |
> + queue = multiprocessing.Queue() |
2046 |
> + |
2047 |
> + if not processes: |
2048 |
> + processes = multiprocessing.cpu_count() |
2049 |
> + |
2050 |
> + child = functools.partial(_BackgroundTask.TaskRunner, queue, task, |
2051 |
> + onexit=onexit, task_args=args, |
2052 |
> + task_kwargs=kwargs) |
2053 |
> + steps = [child] * processes |
2054 |
> + with _BackgroundTask.ParallelTasks(steps): |
2055 |
> + try: |
2056 |
> + yield queue |
2057 |
> + finally: |
2058 |
> + for _ in xrange(processes): |
2059 |
> + queue.put(_AllTasksComplete()) |
2060 |
> + |
2061 |
> + |
2062 |
> +def RunTasksInProcessPool(task, inputs, processes=None, onexit=None): |
2063 |
> + """Run the specified function with each supplied input in a pool of processes. |
2064 |
> + |
2065 |
> + This function runs task(*x) for x in inputs in a pool of processes. This |
2066 |
> + function blocks until all tasks are completed. |
2067 |
> + |
2068 |
> + The output from these tasks is saved to a temporary file. When control |
2069 |
> + returns to the context manager, the background output is printed in order, |
2070 |
> + as if the tasks were run in sequence. |
2071 |
> + |
2072 |
> + If exceptions occur in the steps, we join together the tracebacks and print |
2073 |
> + them after all parallel tasks have finished running. Further, a |
2074 |
> + BackgroundFailure is raised with full stack traces of all exceptions. |
2075 |
> + |
2076 |
> + Example: |
2077 |
> + # This snippet will execute in parallel: |
2078 |
> + # somefunc('hi', 'fat', 'code') |
2079 |
> + # somefunc('foo', 'bar', 'cow') |
2080 |
> + |
2081 |
> + def somefunc(arg1, arg2, arg3): |
2082 |
> + ... |
2083 |
> + ... |
2084 |
> + inputs = [ |
2085 |
> + ['hi', 'fat', 'code'], |
2086 |
> + ['foo', 'bar', 'cow'], |
2087 |
> + ] |
2088 |
> + RunTasksInProcessPool(somefunc, inputs) |
2089 |
> + # Blocks until all calls have completed. |
2090 |
> + |
2091 |
> + Args: |
2092 |
> + task: Function to run on each input. |
2093 |
> + inputs: List of inputs. |
2094 |
> + processes: Number of processes, at most, to launch. |
2095 |
> + onexit: Function to run in each background process after all inputs are |
2096 |
> + processed. |
2097 |
> + """ |
2098 |
> + |
2099 |
> + if not processes: |
2100 |
> + processes = min(multiprocessing.cpu_count(), len(inputs)) |
2101 |
> + |
2102 |
> + with BackgroundTaskRunner(task, processes=processes, onexit=onexit) as queue: |
2103 |
> + for x in inputs: |
2104 |
> + queue.put(x) |
2105 |
|
2106 |
-- |
2107 |
Arfrever Frehtes Taifersar Arahesis |