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