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 |