Gentoo Archives: gentoo-portage-dev

From: Zac Medico <zmedico@g.o>
To: gentoo-portage-dev@l.g.o
Cc: Zac Medico <zmedico@g.o>
Subject: [gentoo-portage-dev] [PATCH 3/3] CONFIG_PROTECT: protect symlinks, bug #485598
Date: Mon, 27 Oct 2014 20:35:44
Message-Id: 1414442119-28600-1-git-send-email-zmedico@gentoo.org
In Reply to: Re: [gentoo-portage-dev] [PATCH 3/3] CONFIG_PROTECT: protect symlinks, bug #485598 by Alexander Berntsen
1 Users may not want some symlinks to get clobbered, so protect them
2 with CONFIG_PROTECT. Changes were required in the dblink.mergeme method
3 and the new_protect_filename function.
4
5 The changes to dblink.mergeme do 3 things:
6
7 * Move the bulk of config protection logic from dblink.mergeme to a
8 new dblink._protect method. The new method only returns 3 variables,
9 which makes it easier to understand how config protection interacts
10 with the dblink.mergeme code that uses those variables. This is
11 important, since dblink.mergeme has so many variables.
12
13 * Initialize more variables at the beginning of dblink.mergeme, since
14 those variables are used by the dblink._protect method.
15
16 * Use the variables returned from dblink._protect to trigger
17 appropriate behavior later in dblink.mergeme.
18
19 The new_protect_filename changes are required since this function
20 compares the new file to old ._cfg* files that may already exist, in
21 order to avoid creating duplicate ._cfg* files. In these comparisons,
22 it needs to handle symlinks differently from regular files.
23
24 The unit tests demonstrate operation in many different scenarios,
25 including:
26
27 * regular file replaces regular file
28 * regular file replaces symlink
29 * regular file replaces directory
30 * symlink replaces symlink
31 * symlink replaces regular file
32 * symlink replaces directory
33 * directory replaces regular file
34 * directory replaces symlink
35
36 X-Gentoo-Bug: 485598
37 X-Gentoo-Bug-URL: https://bugs.gentoo.org/show_bug.cgi?id=485598
38 ---
39 This updated patch only adds to the commit message in order to provide some
40 information that may be helpful to reviewers of the patch. There are no
41 changes to the code.
42
43 pym/portage/dbapi/vartree.py | 255 ++++++++++++---------
44 pym/portage/tests/emerge/test_config_protect.py | 292 ++++++++++++++++++++++++
45 pym/portage/util/__init__.py | 35 ++-
46 3 files changed, 463 insertions(+), 119 deletions(-)
47 create mode 100644 pym/portage/tests/emerge/test_config_protect.py
48
49 diff --git a/pym/portage/dbapi/vartree.py b/pym/portage/dbapi/vartree.py
50 index e21135a..219ca16 100644
51 --- a/pym/portage/dbapi/vartree.py
52 +++ b/pym/portage/dbapi/vartree.py
53 @@ -4461,21 +4461,17 @@ class dblink(object):
54 # stat file once, test using S_* macros many times (faster that way)
55 mystat = os.lstat(mysrc)
56 mymode = mystat[stat.ST_MODE]
57 - # handy variables; mydest is the target object on the live filesystems;
58 - # mysrc is the source object in the temporary install dir
59 - try:
60 - mydstat = os.lstat(mydest)
61 - mydmode = mydstat.st_mode
62 - except OSError as e:
63 - if e.errno != errno.ENOENT:
64 - raise
65 - del e
66 - #dest file doesn't exist
67 - mydstat = None
68 - mydmode = None
69 + mymd5 = None
70 + myto = None
71
72 - if stat.S_ISLNK(mymode):
73 - # we are merging a symbolic link
74 + if sys.hexversion >= 0x3030000:
75 + mymtime = mystat.st_mtime_ns
76 + else:
77 + mymtime = mystat[stat.ST_MTIME]
78 +
79 + if stat.S_ISREG(mymode):
80 + mymd5 = perform_md5(mysrc, calc_prelink=calc_prelink)
81 + elif stat.S_ISLNK(mymode):
82 # The file name of mysrc and the actual file that it points to
83 # will have earlier been forcefully converted to the 'merge'
84 # encoding if necessary, but the content of the symbolic link
85 @@ -4495,6 +4491,69 @@ class dblink(object):
86 os.unlink(mysrc)
87 os.symlink(myto, mysrc)
88
89 + mymd5 = portage.checksum._new_md5(
90 + _unicode_encode(myto)).hexdigest()
91 +
92 + protected = False
93 + if stat.S_ISLNK(mymode) or stat.S_ISREG(mymode):
94 + protected = self.isprotected(mydest)
95 +
96 + if stat.S_ISREG(mymode) and \
97 + mystat.st_size == 0 and \
98 + os.path.basename(mydest).startswith(".keep"):
99 + protected = False
100 +
101 + destmd5 = None
102 + mydest_link = None
103 + # handy variables; mydest is the target object on the live filesystems;
104 + # mysrc is the source object in the temporary install dir
105 + try:
106 + mydstat = os.lstat(mydest)
107 + mydmode = mydstat.st_mode
108 + if protected:
109 + if stat.S_ISLNK(mydmode):
110 + # Read symlink target as bytes, in case the
111 + # target path has a bad encoding.
112 + mydest_link = _os.readlink(
113 + _unicode_encode(mydest,
114 + encoding=_encodings['merge'],
115 + errors='strict'))
116 + mydest_link = _unicode_decode(mydest_link,
117 + encoding=_encodings['merge'],
118 + errors='replace')
119 +
120 + # For protection of symlinks, the md5
121 + # of the link target path string is used
122 + # for cfgfiledict (symlinks are
123 + # protected since bug #485598).
124 + destmd5 = portage.checksum._new_md5(
125 + _unicode_encode(mydest_link)).hexdigest()
126 +
127 + elif stat.S_ISREG(mydmode):
128 + destmd5 = perform_md5(mydest,
129 + calc_prelink=calc_prelink)
130 + except (FileNotFound, OSError) as e:
131 + if isinstance(e, OSError) and e.errno != errno.ENOENT:
132 + raise
133 + #dest file doesn't exist
134 + mydstat = None
135 + mydmode = None
136 + mydest_link = None
137 + destmd5 = None
138 +
139 + moveme = True
140 + if protected:
141 + mydest, protected, moveme = self._protect(cfgfiledict,
142 + protect_if_modified, mymd5, myto, mydest,
143 + myrealdest, mydmode, destmd5, mydest_link)
144 +
145 + zing = "!!!"
146 + if not moveme:
147 + # confmem rejected this update
148 + zing = "---"
149 +
150 + if stat.S_ISLNK(mymode):
151 + # we are merging a symbolic link
152 # Pass in the symlink target in order to bypass the
153 # os.readlink() call inside abssymlink(), since that
154 # call is unsafe if the merge encoding is not ascii
155 @@ -4510,9 +4569,8 @@ class dblink(object):
156 # myrealto contains the path of the real file to which this symlink points.
157 # we can simply test for existence of this file to see if the target has been merged yet
158 myrealto = normalize_path(os.path.join(destroot, myabsto))
159 - if mydmode!=None:
160 - #destination exists
161 - if stat.S_ISDIR(mydmode):
162 + if mydmode is not None and stat.S_ISDIR(mydmode):
163 + if not protected:
164 # we can't merge a symlink over a directory
165 newdest = self._new_backup_path(mydest)
166 msg = []
167 @@ -4525,22 +4583,6 @@ class dblink(object):
168 self._eerror("preinst", msg)
169 mydest = newdest
170
171 - elif not stat.S_ISLNK(mydmode):
172 - if os.path.exists(mysrc) and stat.S_ISDIR(os.stat(mysrc)[stat.ST_MODE]):
173 - # Kill file blocking installation of symlink to dir #71787
174 - pass
175 - elif self.isprotected(mydest):
176 - # Use md5 of the target in ${D} if it exists...
177 - try:
178 - newmd5 = perform_md5(join(srcroot, myabsto))
179 - except FileNotFound:
180 - # Maybe the target is merged already.
181 - try:
182 - newmd5 = perform_md5(myrealto)
183 - except FileNotFound:
184 - newmd5 = None
185 - mydest = new_protect_filename(mydest, newmd5=newmd5)
186 -
187 # if secondhand is None it means we're operating in "force" mode and should not create a second hand.
188 if (secondhand != None) and (not os.path.exists(myrealto)):
189 # either the target directory doesn't exist yet or the target file doesn't exist -- or
190 @@ -4549,9 +4591,11 @@ class dblink(object):
191 secondhand.append(mysrc[len(srcroot):])
192 continue
193 # unlinking no longer necessary; "movefile" will overwrite symlinks atomically and correctly
194 - mymtime = movefile(mysrc, mydest, newmtime=thismtime,
195 - sstat=mystat, mysettings=self.settings,
196 - encoding=_encodings['merge'])
197 + if moveme:
198 + zing = ">>>"
199 + mymtime = movefile(mysrc, mydest, newmtime=thismtime,
200 + sstat=mystat, mysettings=self.settings,
201 + encoding=_encodings['merge'])
202
203 try:
204 self._merged_path(mydest, os.lstat(mydest))
205 @@ -4567,7 +4611,7 @@ class dblink(object):
206 [_("QA Notice: Symbolic link /%s points to /%s which does not exist.")
207 % (relative_path, myabsto)])
208
209 - showMessage(">>> %s -> %s\n" % (mydest, myto))
210 + showMessage("%s %s -> %s\n" % (zing, mydest, myto))
211 if sys.hexversion >= 0x3030000:
212 outfile.write("sym "+myrealdest+" -> "+myto+" "+str(mymtime // 1000000000)+"\n")
213 else:
214 @@ -4589,7 +4633,8 @@ class dblink(object):
215 if dflags != 0:
216 bsd_chflags.lchflags(mydest, 0)
217
218 - if not os.access(mydest, os.W_OK):
219 + if not stat.S_ISLNK(mydmode) and \
220 + not os.access(mydest, os.W_OK):
221 pkgstuff = pkgsplit(self.pkg)
222 writemsg(_("\n!!! Cannot write to '%s'.\n") % mydest, noiselevel=-1)
223 writemsg(_("!!! Please check permissions and directories for broken symlinks.\n"))
224 @@ -4678,14 +4723,8 @@ class dblink(object):
225
226 elif stat.S_ISREG(mymode):
227 # we are merging a regular file
228 - mymd5 = perform_md5(mysrc, calc_prelink=calc_prelink)
229 - # calculate config file protection stuff
230 - mydestdir = os.path.dirname(mydest)
231 - moveme = 1
232 - zing = "!!!"
233 - mymtime = None
234 - protected = self.isprotected(mydest)
235 - if mydmode is not None and stat.S_ISDIR(mydmode):
236 + if not protected and \
237 + mydmode is not None and stat.S_ISDIR(mydmode):
238 # install of destination is blocked by an existing directory with the same name
239 newdest = self._new_backup_path(mydest)
240 msg = []
241 @@ -4698,73 +4737,6 @@ class dblink(object):
242 self._eerror("preinst", msg)
243 mydest = newdest
244
245 - elif mydmode is None or stat.S_ISREG(mydmode) or \
246 - (stat.S_ISLNK(mydmode) and os.path.exists(mydest)
247 - and stat.S_ISREG(os.stat(mydest)[stat.ST_MODE])):
248 - # install of destination is blocked by an existing regular file,
249 - # or by a symlink to an existing regular file;
250 - # now, config file management may come into play.
251 - # we only need to tweak mydest if cfg file management is in play.
252 - destmd5 = None
253 - if protected and mydmode is not None:
254 - destmd5 = perform_md5(mydest, calc_prelink=calc_prelink)
255 - if protect_if_modified:
256 - contents_key = \
257 - self._installed_instance._match_contents(myrealdest)
258 - if contents_key:
259 - inst_info = self._installed_instance.getcontents()[contents_key]
260 - if inst_info[0] == "obj" and inst_info[2] == destmd5:
261 - protected = False
262 -
263 - if protected:
264 - # we have a protection path; enable config file management.
265 - cfgprot = 0
266 - cfgprot_force = False
267 - if mydmode is None:
268 - if self._installed_instance is not None and \
269 - self._installed_instance._match_contents(
270 - myrealdest) is not False:
271 - # If the file doesn't exist, then it may
272 - # have been deleted or renamed by the
273 - # admin. Therefore, force the file to be
274 - # merged with a ._cfg name, so that the
275 - # admin will be prompted for this update
276 - # (see bug #523684).
277 - cfgprot_force = True
278 - moveme = True
279 - cfgprot = True
280 - elif mymd5 == destmd5:
281 - #file already in place; simply update mtimes of destination
282 - moveme = 1
283 - else:
284 - if mymd5 == cfgfiledict.get(myrealdest, [None])[0]:
285 - """ An identical update has previously been
286 - merged. Skip it unless the user has chosen
287 - --noconfmem."""
288 - moveme = cfgfiledict["IGNORE"]
289 - cfgprot = cfgfiledict["IGNORE"]
290 - if not moveme:
291 - zing = "---"
292 - if sys.hexversion >= 0x3030000:
293 - mymtime = mystat.st_mtime_ns
294 - else:
295 - mymtime = mystat[stat.ST_MTIME]
296 - else:
297 - moveme = 1
298 - cfgprot = 1
299 - if moveme:
300 - # Merging a new file, so update confmem.
301 - cfgfiledict[myrealdest] = [mymd5]
302 - elif destmd5 == cfgfiledict.get(myrealdest, [None])[0]:
303 - """A previously remembered update has been
304 - accepted, so it is removed from confmem."""
305 - del cfgfiledict[myrealdest]
306 -
307 - if cfgprot:
308 - mydest = new_protect_filename(mydest,
309 - newmd5=mymd5,
310 - force=cfgprot_force)
311 -
312 # whether config protection or not, we merge the new file the
313 # same way. Unless moveme=0 (blocking directory)
314 if moveme:
315 @@ -4820,6 +4792,63 @@ class dblink(object):
316 outfile.write("dev %s\n" % myrealdest)
317 showMessage(zing + " " + mydest + "\n")
318
319 + def _protect(self, cfgfiledict, protect_if_modified, mymd5, myto,
320 + mydest, myrealdest, mydmode, destmd5, mydest_link):
321 +
322 + moveme = True
323 + protected = True
324 + force = False
325 + k = False
326 + if self._installed_instance is not None:
327 + k = self._installed_instance._match_contents(myrealdest)
328 + if k is not False:
329 + if mydmode is None:
330 + # If the file doesn't exist, then it may
331 + # have been deleted or renamed by the
332 + # admin. Therefore, force the file to be
333 + # merged with a ._cfg name, so that the
334 + # admin will be prompted for this update
335 + # (see bug #523684).
336 + force = True
337 +
338 + elif protect_if_modified:
339 + data = self._installed_instance.getcontents()[k]
340 + if data[0] == "obj" and data[2] == destmd5:
341 + protected = False
342 + elif data[0] == "sym" and data[2] == mydest_link:
343 + protected = False
344 +
345 + if protected and mydmode is not None:
346 + # we have a protection path; enable config file management.
347 + if mymd5 != destmd5 and \
348 + mymd5 == cfgfiledict.get(myrealdest, [None])[0]:
349 + # An identical update has previously been
350 + # merged. Skip it unless the user has chosen
351 + # --noconfmem.
352 + moveme = protected = bool(cfgfiledict["IGNORE"])
353 +
354 + if protected and \
355 + (mydest_link is not None or myto is not None) and \
356 + mydest_link != myto:
357 + # If either one is a symlink, and they are not
358 + # identical symlinks, then force config protection.
359 + force = True
360 +
361 + if moveme:
362 + # Merging a new file, so update confmem.
363 + cfgfiledict[myrealdest] = [mymd5]
364 + elif destmd5 == cfgfiledict.get(myrealdest, [None])[0]:
365 + # A previously remembered update has been
366 + # accepted, so it is removed from confmem.
367 + del cfgfiledict[myrealdest]
368 +
369 + if protected and moveme:
370 + mydest = new_protect_filename(mydest,
371 + newmd5=(mydest_link or mymd5),
372 + force=force)
373 +
374 + return mydest, protected, moveme
375 +
376 def _merged_path(self, path, lstatobj, exists=True):
377 previous_path = self._device_path_map.get(lstatobj.st_dev)
378 if previous_path is None or previous_path is False or \
379 diff --git a/pym/portage/tests/emerge/test_config_protect.py b/pym/portage/tests/emerge/test_config_protect.py
380 new file mode 100644
381 index 0000000..5d7d8e9
382 --- /dev/null
383 +++ b/pym/portage/tests/emerge/test_config_protect.py
384 @@ -0,0 +1,292 @@
385 +# Copyright 2014 Gentoo Foundation
386 +# Distributed under the terms of the GNU General Public License v2
387 +
388 +from __future__ import unicode_literals
389 +
390 +import io
391 +from functools import partial
392 +import shutil
393 +import stat
394 +import subprocess
395 +import sys
396 +import time
397 +
398 +import portage
399 +from portage import os
400 +from portage import _encodings, _unicode_decode
401 +from portage.const import BASH_BINARY, PORTAGE_PYM_PATH
402 +from portage.process import find_binary
403 +from portage.tests import TestCase
404 +from portage.tests.resolver.ResolverPlayground import ResolverPlayground
405 +from portage.util import (ensure_dirs, find_updated_config_files,
406 + shlex_split)
407 +
408 +class ConfigProtectTestCase(TestCase):
409 +
410 + def testConfigProtect(self):
411 + """
412 + Demonstrates many different scenarios. For example:
413 +
414 + * regular file replaces regular file
415 + * regular file replaces symlink
416 + * regular file replaces directory
417 + * symlink replaces symlink
418 + * symlink replaces regular file
419 + * symlink replaces directory
420 + * directory replaces regular file
421 + * directory replaces symlink
422 + """
423 +
424 + debug = False
425 +
426 + content_A_1 = """
427 +S="${WORKDIR}"
428 +
429 +src_install() {
430 + insinto /etc/A
431 + keepdir /etc/A/dir_a
432 + keepdir /etc/A/symlink_replaces_dir
433 + keepdir /etc/A/regular_replaces_dir
434 + echo regular_a_1 > "${T}"/regular_a
435 + doins "${T}"/regular_a
436 + echo regular_b_1 > "${T}"/regular_b
437 + doins "${T}"/regular_b
438 + dosym regular_a /etc/A/regular_replaces_symlink
439 + dosym regular_b /etc/A/symlink_replaces_symlink
440 + echo regular_replaces_regular_1 > \
441 + "${T}"/regular_replaces_regular
442 + doins "${T}"/regular_replaces_regular
443 + echo symlink_replaces_regular > \
444 + "${T}"/symlink_replaces_regular
445 + doins "${T}"/symlink_replaces_regular
446 +}
447 +
448 +"""
449 +
450 + content_A_2 = """
451 +S="${WORKDIR}"
452 +
453 +src_install() {
454 + insinto /etc/A
455 + keepdir /etc/A/dir_a
456 + dosym dir_a /etc/A/symlink_replaces_dir
457 + echo regular_replaces_dir > "${T}"/regular_replaces_dir
458 + doins "${T}"/regular_replaces_dir
459 + echo regular_a_2 > "${T}"/regular_a
460 + doins "${T}"/regular_a
461 + echo regular_b_2 > "${T}"/regular_b
462 + doins "${T}"/regular_b
463 + echo regular_replaces_symlink > \
464 + "${T}"/regular_replaces_symlink
465 + doins "${T}"/regular_replaces_symlink
466 + dosym regular_b /etc/A/symlink_replaces_symlink
467 + echo regular_replaces_regular_2 > \
468 + "${T}"/regular_replaces_regular
469 + doins "${T}"/regular_replaces_regular
470 + dosym regular_a /etc/A/symlink_replaces_regular
471 +}
472 +
473 +"""
474 +
475 + ebuilds = {
476 + "dev-libs/A-1": {
477 + "EAPI" : "5",
478 + "IUSE" : "+flag",
479 + "KEYWORDS": "x86",
480 + "LICENSE": "GPL-2",
481 + "MISC_CONTENT": content_A_1,
482 + },
483 + "dev-libs/A-2": {
484 + "EAPI" : "5",
485 + "IUSE" : "+flag",
486 + "KEYWORDS": "x86",
487 + "LICENSE": "GPL-2",
488 + "MISC_CONTENT": content_A_2,
489 + },
490 + }
491 +
492 + playground = ResolverPlayground(
493 + ebuilds=ebuilds, debug=debug)
494 + settings = playground.settings
495 + eprefix = settings["EPREFIX"]
496 + eroot = settings["EROOT"]
497 + var_cache_edb = os.path.join(eprefix, "var", "cache", "edb")
498 +
499 + portage_python = portage._python_interpreter
500 + dispatch_conf_cmd = (portage_python, "-b", "-Wd",
501 + os.path.join(self.sbindir, "dispatch-conf"))
502 + emerge_cmd = (portage_python, "-b", "-Wd",
503 + os.path.join(self.bindir, "emerge"))
504 + etc_update_cmd = (BASH_BINARY,
505 + os.path.join(self.sbindir, "etc-update"))
506 + etc_update_auto = etc_update_cmd + ("--automode", "-5",)
507 +
508 + config_protect = "/etc"
509 +
510 + def modify_files(dir_path):
511 + for name in os.listdir(dir_path):
512 + path = os.path.join(dir_path, name)
513 + st = os.lstat(path)
514 + if stat.S_ISREG(st.st_mode):
515 + with io.open(path, mode='a',
516 + encoding=_encodings["stdio"]) as f:
517 + f.write("modified at %d\n" % time.time())
518 + elif stat.S_ISLNK(st.st_mode):
519 + old_dest = os.readlink(path)
520 + os.unlink(path)
521 + os.symlink(old_dest +
522 + " modified at %d" % time.time(), path)
523 +
524 + def updated_config_files(count):
525 + self.assertEqual(count,
526 + sum(len(x[1]) for x in find_updated_config_files(eroot,
527 + shlex_split(config_protect))))
528 +
529 + test_commands = (
530 + etc_update_cmd,
531 + dispatch_conf_cmd,
532 + emerge_cmd + ("-1", "=dev-libs/A-1"),
533 + partial(updated_config_files, 0),
534 + emerge_cmd + ("-1", "=dev-libs/A-2"),
535 + partial(updated_config_files, 2),
536 + etc_update_auto,
537 + partial(updated_config_files, 0),
538 + emerge_cmd + ("-1", "=dev-libs/A-2"),
539 + partial(updated_config_files, 0),
540 + # Test bug #523684, where a file renamed or removed by the
541 + # admin forces replacement files to be merged with config
542 + # protection.
543 + partial(shutil.rmtree,
544 + os.path.join(eprefix, "etc", "A")),
545 + emerge_cmd + ("-1", "=dev-libs/A-2"),
546 + partial(updated_config_files, 8),
547 + etc_update_auto,
548 + partial(updated_config_files, 0),
549 + # Modify some config files, and verify that it triggers
550 + # config protection.
551 + partial(modify_files,
552 + os.path.join(eroot, "etc", "A")),
553 + emerge_cmd + ("-1", "=dev-libs/A-2"),
554 + partial(updated_config_files, 6),
555 + etc_update_auto,
556 + partial(updated_config_files, 0),
557 + # Modify some config files, downgrade to A-1, and verify
558 + # that config protection works properly when the file
559 + # types are changing.
560 + partial(modify_files,
561 + os.path.join(eroot, "etc", "A")),
562 + emerge_cmd + ("-1", "--noconfmem", "=dev-libs/A-1"),
563 + partial(updated_config_files, 6),
564 + etc_update_auto,
565 + partial(updated_config_files, 0),
566 + )
567 +
568 + distdir = playground.distdir
569 + fake_bin = os.path.join(eprefix, "bin")
570 + portage_tmpdir = os.path.join(eprefix, "var", "tmp", "portage")
571 +
572 + path = os.environ.get("PATH")
573 + if path is not None and not path.strip():
574 + path = None
575 + if path is None:
576 + path = ""
577 + else:
578 + path = ":" + path
579 + path = fake_bin + path
580 +
581 + pythonpath = os.environ.get("PYTHONPATH")
582 + if pythonpath is not None and not pythonpath.strip():
583 + pythonpath = None
584 + if pythonpath is not None and \
585 + pythonpath.split(":")[0] == PORTAGE_PYM_PATH:
586 + pass
587 + else:
588 + if pythonpath is None:
589 + pythonpath = ""
590 + else:
591 + pythonpath = ":" + pythonpath
592 + pythonpath = PORTAGE_PYM_PATH + pythonpath
593 +
594 + env = {
595 + "PORTAGE_OVERRIDE_EPREFIX" : eprefix,
596 + "CLEAN_DELAY" : "0",
597 + "CONFIG_PROTECT": config_protect,
598 + "DISTDIR" : distdir,
599 + "EMERGE_DEFAULT_OPTS": "-v",
600 + "EMERGE_WARNING_DELAY" : "0",
601 + "INFODIR" : "",
602 + "INFOPATH" : "",
603 + "PATH" : path,
604 + "PORTAGE_INST_GID" : str(portage.data.portage_gid),
605 + "PORTAGE_INST_UID" : str(portage.data.portage_uid),
606 + "PORTAGE_PYTHON" : portage_python,
607 + "PORTAGE_REPOSITORIES" : settings.repositories.config_string(),
608 + "PORTAGE_TMPDIR" : portage_tmpdir,
609 + "PYTHONPATH" : pythonpath,
610 + "__PORTAGE_TEST_PATH_OVERRIDE" : fake_bin,
611 + }
612 +
613 + if "__PORTAGE_TEST_HARDLINK_LOCKS" in os.environ:
614 + env["__PORTAGE_TEST_HARDLINK_LOCKS"] = \
615 + os.environ["__PORTAGE_TEST_HARDLINK_LOCKS"]
616 +
617 + dirs = [distdir, fake_bin, portage_tmpdir,
618 + var_cache_edb]
619 + etc_symlinks = ("dispatch-conf.conf", "etc-update.conf")
620 + # Override things that may be unavailable, or may have portability
621 + # issues when running tests in exotic environments.
622 + # prepstrip - bug #447810 (bash read builtin EINTR problem)
623 + true_symlinks = ["prepstrip", "scanelf"]
624 + true_binary = find_binary("true")
625 + self.assertEqual(true_binary is None, False,
626 + "true command not found")
627 + try:
628 + for d in dirs:
629 + ensure_dirs(d)
630 + for x in true_symlinks:
631 + os.symlink(true_binary, os.path.join(fake_bin, x))
632 + for x in etc_symlinks:
633 + os.symlink(os.path.join(self.cnf_etc_path, x),
634 + os.path.join(eprefix, "etc", x))
635 + with open(os.path.join(var_cache_edb, "counter"), 'wb') as f:
636 + f.write(b"100")
637 +
638 + if debug:
639 + # The subprocess inherits both stdout and stderr, for
640 + # debugging purposes.
641 + stdout = None
642 + else:
643 + # The subprocess inherits stderr so that any warnings
644 + # triggered by python -Wd will be visible.
645 + stdout = subprocess.PIPE
646 +
647 + for args in test_commands:
648 +
649 + if hasattr(args, '__call__'):
650 + args()
651 + continue
652 +
653 + if isinstance(args[0], dict):
654 + local_env = env.copy()
655 + local_env.update(args[0])
656 + args = args[1:]
657 + else:
658 + local_env = env
659 +
660 + proc = subprocess.Popen(args,
661 + env=local_env, stdout=stdout)
662 +
663 + if debug:
664 + proc.wait()
665 + else:
666 + output = proc.stdout.readlines()
667 + proc.wait()
668 + proc.stdout.close()
669 + if proc.returncode != os.EX_OK:
670 + for line in output:
671 + sys.stderr.write(_unicode_decode(line))
672 +
673 + self.assertEqual(os.EX_OK, proc.returncode,
674 + "emerge failed with args %s" % (args,))
675 + finally:
676 + playground.cleanup()
677 diff --git a/pym/portage/util/__init__.py b/pym/portage/util/__init__.py
678 index fe79942..ad3a351 100644
679 --- a/pym/portage/util/__init__.py
680 +++ b/pym/portage/util/__init__.py
681 @@ -1676,13 +1676,36 @@ def new_protect_filename(mydest, newmd5=None, force=False):
682 old_pfile = normalize_path(os.path.join(real_dirname, last_pfile))
683 if last_pfile and newmd5:
684 try:
685 - last_pfile_md5 = portage.checksum._perform_md5_merge(old_pfile)
686 - except FileNotFound:
687 - # The file suddenly disappeared or it's a broken symlink.
688 - pass
689 + old_pfile_st = _os_merge.lstat(old_pfile)
690 + except OSError as e:
691 + if e.errno != errno.ENOENT:
692 + raise
693 else:
694 - if last_pfile_md5 == newmd5:
695 - return old_pfile
696 + if stat.S_ISLNK(old_pfile_st.st_mode):
697 + try:
698 + # Read symlink target as bytes, in case the
699 + # target path has a bad encoding.
700 + pfile_link = _os.readlink(_unicode_encode(old_pfile,
701 + encoding=_encodings['merge'], errors='strict'))
702 + except OSError:
703 + if e.errno != errno.ENOENT:
704 + raise
705 + else:
706 + pfile_link = _unicode_decode(
707 + encoding=_encodings['merge'], errors='replace')
708 + if pfile_link == newmd5:
709 + return old_pfile
710 + else:
711 + try:
712 + last_pfile_md5 = \
713 + portage.checksum._perform_md5_merge(old_pfile)
714 + except FileNotFound:
715 + # The file suddenly disappeared or it's a
716 + # broken symlink.
717 + pass
718 + else:
719 + if last_pfile_md5 == newmd5:
720 + return old_pfile
721 return new_pfile
722
723 def find_updated_config_files(target_root, config_protect):
724 --
725 2.0.4

Replies