Gentoo Archives: gentoo-portage-dev

From: Arfrever Frehtes Taifersar Arahesis <arfrever.fta@×××××.com>
To: Gentoo Portage Development <gentoo-portage-dev@l.g.o>
Cc: davidjames@××××××××.org
Subject: Re: [gentoo-portage-dev] [PATCH] prepstrip: rewrite in python
Date: Wed, 16 Oct 2013 23:27:38
Message-Id: 201310170126.05188.Arfrever.FTA@gmail.com
In Reply to: [gentoo-portage-dev] [PATCH] prepstrip: rewrite in python by Mike Frysinger
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

Attachments

File name MIME type
signature.asc application/pgp-signature

Replies

Subject Author
Re: [gentoo-portage-dev] [PATCH] prepstrip: rewrite in python Mike Frysinger <vapier@g.o>