Gentoo Archives: gentoo-portage-dev

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

Replies