EbuildPhase: bump WORKDIR timestamp after unpack
[portage.git] / pym / _emerge / EbuildPhase.py
1 # Copyright 1999-2012 Gentoo Foundation
2 # Distributed under the terms of the GNU General Public License v2
3
4 import gzip
5 import io
6 import sys
7 import tempfile
8 import time
9
10 from _emerge.AsynchronousLock import AsynchronousLock
11 from _emerge.BinpkgEnvExtractor import BinpkgEnvExtractor
12 from _emerge.MiscFunctionsProcess import MiscFunctionsProcess
13 from _emerge.EbuildProcess import EbuildProcess
14 from _emerge.CompositeTask import CompositeTask
15 from portage.util import writemsg
16
17 try:
18         from portage.xml.metadata import MetaDataXML
19 except (SystemExit, KeyboardInterrupt):
20         raise
21 except (ImportError, SystemError, RuntimeError, Exception):
22         # broken or missing xml support
23         # http://bugs.python.org/issue14988
24         MetaDataXML = None
25
26 import portage
27 portage.proxy.lazyimport.lazyimport(globals(),
28         'portage.elog:messages@elog_messages',
29         'portage.package.ebuild.doebuild:_check_build_log,' + \
30                 '_post_phase_cmds,_post_phase_userpriv_perms,' + \
31                 '_post_src_install_soname_symlinks,' + \
32                 '_post_src_install_uid_fix,_postinst_bsdflags,' + \
33                 '_post_src_install_write_metadata,' + \
34                 '_preinst_bsdflags'
35 )
36 from portage import os
37 from portage import _encodings
38 from portage import _unicode_encode
39
40 class EbuildPhase(CompositeTask):
41
42         __slots__ = ("actionmap", "phase", "settings") + \
43                 ("_ebuild_lock",)
44
45         # FEATURES displayed prior to setup phase
46         _features_display = (
47                 "ccache", "compressdebug", "distcc", "distcc-pump", "fakeroot",
48                 "installsources", "keeptemp", "keepwork", "nostrip",
49                 "preserve-libs", "sandbox", "selinux", "sesandbox",
50                 "splitdebug", "suidctl", "test", "userpriv",
51                 "usersandbox"
52         )
53
54         # Locked phases
55         _locked_phases = ("setup", "preinst", "postinst", "prerm", "postrm")
56
57         def _start(self):
58
59                 need_builddir = self.phase not in EbuildProcess._phases_without_builddir
60
61                 if need_builddir:
62                         phase_completed_file = os.path.join(
63                                 self.settings['PORTAGE_BUILDDIR'],
64                                 ".%sed" % self.phase.rstrip('e'))
65                         if not os.path.exists(phase_completed_file):
66                                 # If the phase is really going to run then we want
67                                 # to eliminate any stale elog messages that may
68                                 # exist from a previous run.
69                                 try:
70                                         os.unlink(os.path.join(self.settings['T'],
71                                                 'logging', self.phase))
72                                 except OSError:
73                                         pass
74
75                 if self.phase in ('nofetch', 'pretend', 'setup'):
76
77                         use = self.settings.get('PORTAGE_BUILT_USE')
78                         if use is None:
79                                 use = self.settings['PORTAGE_USE']
80
81                         maint_str = ""
82                         upstr_str = ""
83                         metadata_xml_path = os.path.join(os.path.dirname(self.settings['EBUILD']), "metadata.xml")
84                         if MetaDataXML is not None and os.path.isfile(metadata_xml_path):
85                                 herds_path = os.path.join(self.settings['PORTDIR'],
86                                         'metadata/herds.xml')
87                                 try:
88                                         metadata_xml = MetaDataXML(metadata_xml_path, herds_path)
89                                         maint_str = metadata_xml.format_maintainer_string()
90                                         upstr_str = metadata_xml.format_upstream_string()
91                                 except SyntaxError:
92                                         maint_str = "<invalid metadata.xml>"
93
94                         msg = []
95                         msg.append("Package:    %s" % self.settings.mycpv)
96                         if self.settings.get('PORTAGE_REPO_NAME'):
97                                 msg.append("Repository: %s" % self.settings['PORTAGE_REPO_NAME'])
98                         if maint_str:
99                                 msg.append("Maintainer: %s" % maint_str)
100                         if upstr_str:
101                                 msg.append("Upstream:   %s" % upstr_str)
102
103                         msg.append("USE:        %s" % use)
104                         relevant_features = []
105                         enabled_features = self.settings.features
106                         for x in self._features_display:
107                                 if x in enabled_features:
108                                         relevant_features.append(x)
109                         if relevant_features:
110                                 msg.append("FEATURES:   %s" % " ".join(relevant_features))
111
112                         # Force background=True for this header since it's intended
113                         # for the log and it doesn't necessarily need to be visible
114                         # elsewhere.
115                         self._elog('einfo', msg, background=True)
116
117                 if self.phase == 'package':
118                         if 'PORTAGE_BINPKG_TMPFILE' not in self.settings:
119                                 self.settings['PORTAGE_BINPKG_TMPFILE'] = \
120                                         os.path.join(self.settings['PKGDIR'],
121                                         self.settings['CATEGORY'], self.settings['PF']) + '.tbz2'
122
123                 if self.phase in ("pretend", "prerm"):
124                         env_extractor = BinpkgEnvExtractor(background=self.background,
125                                 scheduler=self.scheduler, settings=self.settings)
126                         if env_extractor.saved_env_exists():
127                                 self._start_task(env_extractor, self._env_extractor_exit)
128                                 return
129                         # If the environment.bz2 doesn't exist, then ebuild.sh will
130                         # source the ebuild as a fallback.
131
132                 self._start_lock()
133
134         def _env_extractor_exit(self, env_extractor):
135                 if self._default_exit(env_extractor) != os.EX_OK:
136                         self.wait()
137                         return
138
139                 self._start_lock()
140
141         def _start_lock(self):
142                 if (self.phase in self._locked_phases and
143                         "ebuild-locks" in self.settings.features):
144                         eroot = self.settings["EROOT"]
145                         lock_path = os.path.join(eroot, portage.VDB_PATH + "-ebuild")
146                         if os.access(os.path.dirname(lock_path), os.W_OK):
147                                 self._ebuild_lock = AsynchronousLock(path=lock_path,
148                                         scheduler=self.scheduler)
149                                 self._start_task(self._ebuild_lock, self._lock_exit)
150                                 return
151
152                 self._start_ebuild()
153
154         def _lock_exit(self, ebuild_lock):
155                 if self._default_exit(ebuild_lock) != os.EX_OK:
156                         self.wait()
157                         return
158                 self._start_ebuild()
159
160         def _start_ebuild(self):
161
162                 # Don't open the log file during the clean phase since the
163                 # open file can result in an nfs lock on $T/build.log which
164                 # prevents the clean phase from removing $T.
165                 logfile = None
166                 if self.phase not in ("clean", "cleanrm") and \
167                         self.settings.get("PORTAGE_BACKGROUND") != "subprocess":
168                         logfile = self.settings.get("PORTAGE_LOG_FILE")
169
170                 fd_pipes = None
171                 if not self.background and self.phase == 'nofetch':
172                         # All the pkg_nofetch output goes to stderr since
173                         # it's considered to be an error message.
174                         fd_pipes = {1 : sys.stderr.fileno()}
175
176                 ebuild_process = EbuildProcess(actionmap=self.actionmap,
177                         background=self.background, fd_pipes=fd_pipes, logfile=logfile,
178                         phase=self.phase, scheduler=self.scheduler,
179                         settings=self.settings)
180
181                 self._start_task(ebuild_process, self._ebuild_exit)
182
183         def _ebuild_exit(self, ebuild_process):
184
185                 if self._ebuild_lock is not None:
186                         self._ebuild_lock.unlock()
187                         self._ebuild_lock = None
188
189                 fail = False
190                 if self._default_exit(ebuild_process) != os.EX_OK:
191                         if self.phase == "test" and \
192                                 "test-fail-continue" in self.settings.features:
193                                 pass
194                         else:
195                                 fail = True
196
197                 if not fail:
198                         self.returncode = None
199
200                 logfile = None
201                 if self.settings.get("PORTAGE_BACKGROUND") != "subprocess":
202                         logfile = self.settings.get("PORTAGE_LOG_FILE")
203
204                 if self.phase == "install":
205                         out = io.StringIO()
206                         _check_build_log(self.settings, out=out)
207                         msg = out.getvalue()
208                         self.scheduler.output(msg, log_path=logfile)
209
210                 if fail:
211                         self._die_hooks()
212                         return
213
214                 settings = self.settings
215                 _post_phase_userpriv_perms(settings)
216
217                 if self.phase == "unpack":
218                         # Bump WORKDIR timestamp, in case tar gave it a timestamp
219                         # that will interfere with distfiles / WORKDIR timestamp
220                         # comparisons as reported in bug #332217.
221                         timestamp = time.time()
222                         os.utime(settings["WORKDIR"], (timestamp, timestamp))
223                 elif self.phase == "install":
224                         out = io.StringIO()
225                         _post_src_install_write_metadata(settings)
226                         _post_src_install_uid_fix(settings, out)
227                         msg = out.getvalue()
228                         if msg:
229                                 self.scheduler.output(msg, log_path=logfile)
230                 elif self.phase == "preinst":
231                         _preinst_bsdflags(settings)
232                 elif self.phase == "postinst":
233                         _postinst_bsdflags(settings)
234
235                 post_phase_cmds = _post_phase_cmds.get(self.phase)
236                 if post_phase_cmds is not None:
237                         if logfile is not None and self.phase in ("install",):
238                                 # Log to a temporary file, since the code we are running
239                                 # reads PORTAGE_LOG_FILE for QA checks, and we want to
240                                 # avoid annoying "gzip: unexpected end of file" messages
241                                 # when FEATURES=compress-build-logs is enabled.
242                                 fd, logfile = tempfile.mkstemp()
243                                 os.close(fd)
244                         post_phase = MiscFunctionsProcess(background=self.background,
245                                 commands=post_phase_cmds, logfile=logfile, phase=self.phase,
246                                 scheduler=self.scheduler, settings=settings)
247                         self._start_task(post_phase, self._post_phase_exit)
248                         return
249
250                 # this point is not reachable if there was a failure and
251                 # we returned for die_hooks above, so returncode must
252                 # indicate success (especially if ebuild_process.returncode
253                 # is unsuccessful and test-fail-continue came into play)
254                 self.returncode = os.EX_OK
255                 self._current_task = None
256                 self.wait()
257
258         def _post_phase_exit(self, post_phase):
259
260                 self._assert_current(post_phase)
261
262                 log_path = None
263                 if self.settings.get("PORTAGE_BACKGROUND") != "subprocess":
264                         log_path = self.settings.get("PORTAGE_LOG_FILE")
265
266                 if post_phase.logfile is not None and \
267                         post_phase.logfile != log_path:
268                         # We were logging to a temp file (see above), so append
269                         # temp file to main log and remove temp file.
270                         self._append_temp_log(post_phase.logfile, log_path)
271
272                 if self._final_exit(post_phase) != os.EX_OK:
273                         writemsg("!!! post %s failed; exiting.\n" % self.phase,
274                                 noiselevel=-1)
275                         self._die_hooks()
276                         return
277
278                 if self.phase == "install":
279                         out = io.StringIO()
280                         _post_src_install_soname_symlinks(self.settings, out)
281                         msg = out.getvalue()
282                         if msg:
283                                 self.scheduler.output(msg, log_path=log_path)
284
285                 self._current_task = None
286                 self.wait()
287                 return
288
289         def _append_temp_log(self, temp_log, log_path):
290
291                 temp_file = open(_unicode_encode(temp_log,
292                         encoding=_encodings['fs'], errors='strict'), 'rb')
293
294                 log_file, log_file_real = self._open_log(log_path)
295
296                 for line in temp_file:
297                         log_file.write(line)
298
299                 temp_file.close()
300                 log_file.close()
301                 if log_file_real is not log_file:
302                         log_file_real.close()
303                 os.unlink(temp_log)
304
305         def _open_log(self, log_path):
306
307                 f = open(_unicode_encode(log_path,
308                         encoding=_encodings['fs'], errors='strict'),
309                         mode='ab')
310                 f_real = f
311
312                 if log_path.endswith('.gz'):
313                         f =  gzip.GzipFile(filename='', mode='ab', fileobj=f)
314
315                 return (f, f_real)
316
317         def _die_hooks(self):
318                 self.returncode = None
319                 phase = 'die_hooks'
320                 die_hooks = MiscFunctionsProcess(background=self.background,
321                         commands=[phase], phase=phase,
322                         scheduler=self.scheduler, settings=self.settings)
323                 self._start_task(die_hooks, self._die_hooks_exit)
324
325         def _die_hooks_exit(self, die_hooks):
326                 if self.phase != 'clean' and \
327                         'noclean' not in self.settings.features and \
328                         'fail-clean' in self.settings.features:
329                         self._default_exit(die_hooks)
330                         self._fail_clean()
331                         return
332                 self._final_exit(die_hooks)
333                 self.returncode = 1
334                 self.wait()
335
336         def _fail_clean(self):
337                 self.returncode = None
338                 portage.elog.elog_process(self.settings.mycpv, self.settings)
339                 phase = "clean"
340                 clean_phase = EbuildPhase(background=self.background,
341                         phase=phase, scheduler=self.scheduler, settings=self.settings)
342                 self._start_task(clean_phase, self._fail_clean_exit)
343                 return
344
345         def _fail_clean_exit(self, clean_phase):
346                 self._final_exit(clean_phase)
347                 self.returncode = 1
348                 self.wait()
349
350         def _elog(self, elog_funcname, lines, background=None):
351                 if background is None:
352                         background = self.background
353                 out = io.StringIO()
354                 phase = self.phase
355                 elog_func = getattr(elog_messages, elog_funcname)
356                 global_havecolor = portage.output.havecolor
357                 try:
358                         portage.output.havecolor = \
359                                 self.settings.get('NOCOLOR', 'false').lower() in ('no', 'false')
360                         for line in lines:
361                                 elog_func(line, phase=phase, key=self.settings.mycpv, out=out)
362                 finally:
363                         portage.output.havecolor = global_havecolor
364                 msg = out.getvalue()
365                 if msg:
366                         log_path = None
367                         if self.settings.get("PORTAGE_BACKGROUND") != "subprocess":
368                                 log_path = self.settings.get("PORTAGE_LOG_FILE")
369                         self.scheduler.output(msg, log_path=log_path,
370                                 background=background)