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 |