9b22363f061c9a61182224badfffd3dcd3885b5e
[portage.git] / bin / egencache
1 #!/usr/bin/python
2 # Copyright 2009-2013 Gentoo Foundation
3 # Distributed under the terms of the GNU General Public License v2
4
5 # unicode_literals for compat with TextIOWrapper in Python 2
6 from __future__ import print_function, unicode_literals
7
8 import platform
9 import signal
10 import sys
11 # This block ensures that ^C interrupts are handled quietly.
12 try:
13
14         def exithandler(signum, _frame):
15                 signal.signal(signal.SIGINT, signal.SIG_IGN)
16                 signal.signal(signal.SIGTERM, signal.SIG_IGN)
17                 sys.exit(128 + signum)
18
19         signal.signal(signal.SIGINT, exithandler)
20         signal.signal(signal.SIGTERM, exithandler)
21
22 except KeyboardInterrupt:
23         sys.exit(128 + signal.SIGINT)
24
25 def debug_signal(_signum, _frame):
26         import pdb
27         pdb.set_trace()
28
29 if platform.python_implementation() == 'Jython':
30         debug_signum = signal.SIGUSR2 # bug #424259
31 else:
32         debug_signum = signal.SIGUSR1
33
34 signal.signal(debug_signum, debug_signal)
35
36 import io
37 import logging
38 import subprocess
39 import time
40 import textwrap
41 import re
42
43 from os import path as osp
44 pym_path = osp.join(osp.dirname(osp.dirname(osp.realpath(__file__))), "pym")
45 sys.path.insert(0, pym_path)
46 import portage
47 portage._internal_caller = True
48 from portage import os, _encodings, _unicode_encode, _unicode_decode
49 from _emerge.MetadataRegen import MetadataRegen
50 from portage.cache.cache_errors import CacheError, StatCollision
51 from portage.const import TIMESTAMP_FORMAT
52 from portage.manifest import guessManifestFileType
53 from portage.package.ebuild._parallel_manifest.ManifestScheduler import ManifestScheduler
54 from portage.util import cmp_sort_key, writemsg_level
55 from portage.util._argparse import ArgumentParser
56 from portage.util._async.run_main_scheduler import run_main_scheduler
57 from portage.util._eventloop.global_event_loop import global_event_loop
58 from portage import cpv_getkey
59 from portage.dep import Atom, isjustname
60 from portage.versions import pkgsplit, vercmp
61
62 try:
63         from xml.etree import ElementTree
64 except ImportError:
65         pass
66 else:
67         try:
68                 from xml.parsers.expat import ExpatError
69         except ImportError:
70                 pass
71         else:
72                 from repoman.utilities import parse_metadata_use
73
74 from repoman.utilities import FindVCS
75
76 if sys.hexversion >= 0x3000000:
77         # pylint: disable=W0622
78         long = int
79
80 def parse_args(args):
81         usage = "egencache [options] <action> ... [atom] ..."
82         parser = ArgumentParser(usage=usage)
83
84         actions = parser.add_argument_group('Actions')
85         actions.add_argument("--update",
86                 action="store_true",
87                 help="update metadata/md5-cache/ (generate as necessary)")
88         actions.add_argument("--update-use-local-desc",
89                 action="store_true",
90                 help="update the use.local.desc file from metadata.xml")
91         actions.add_argument("--update-changelogs",
92                 action="store_true",
93                 help="update the ChangeLog files from SCM logs")
94         actions.add_argument("--update-manifests",
95                 action="store_true",
96                 help="update manifests")
97
98         common = parser.add_argument_group('Common options')
99         common.add_argument("--repo",
100                 action="store",
101                 help="name of repo to operate on")
102         common.add_argument("--config-root",
103                 help="location of portage config files",
104                 dest="portage_configroot")
105         common.add_argument("--gpg-dir",
106                 help="override the PORTAGE_GPG_DIR variable",
107                 dest="gpg_dir")
108         common.add_argument("--gpg-key",
109                 help="override the PORTAGE_GPG_KEY variable",
110                 dest="gpg_key")
111         common.add_argument("--portdir",
112                 help="override the PORTDIR variable (deprecated in favor of --repositories-configuration)",
113                 dest="portdir")
114         common.add_argument("--portdir-overlay",
115                 help="override the PORTDIR_OVERLAY variable (deprecated in favor of --repositories-configuration)",
116                 dest="portdir_overlay")
117         common.add_argument("--repositories-configuration",
118                 help="override configuration of repositories (in format of repos.conf)",
119                 dest="repositories_configuration")
120         common.add_argument("--sign-manifests",
121                 choices=('y', 'n'),
122                 metavar="<y|n>",
123                 help="manually override layout.conf sign-manifests setting")
124         common.add_argument("--strict-manifests",
125                 choices=('y', 'n'),
126                 metavar="<y|n>",
127                 help="manually override \"strict\" FEATURES setting")
128         common.add_argument("--thin-manifests",
129                 choices=('y', 'n'),
130                 metavar="<y|n>",
131                 help="manually override layout.conf thin-manifests setting")
132         common.add_argument("--tolerant",
133                 action="store_true",
134                 help="exit successfully if only minor errors occurred")
135         common.add_argument("--ignore-default-opts",
136                 action="store_true",
137                 help="do not use the EGENCACHE_DEFAULT_OPTS environment variable")
138         common.add_argument("--write-timestamp",
139                 action="store_true",
140                 help="write metadata/timestamp.chk as required for rsync repositories")
141
142         update = parser.add_argument_group('--update options')
143         update.add_argument("--cache-dir",
144                 help="location of the metadata cache",
145                 dest="cache_dir")
146         update.add_argument("-j", "--jobs",
147                 type=int,
148                 action="store",
149                 help="max ebuild processes to spawn")
150         update.add_argument("--load-average",
151                 type=float,
152                 action="store",
153                 help="max load allowed when spawning multiple jobs",
154                 dest="load_average")
155         update.add_argument("--rsync",
156                 action="store_true",
157                 help="enable rsync stat collision workaround " + \
158                         "for bug 139134 (use with --update)")
159
160         uld = parser.add_argument_group('--update-use-local-desc options')
161         uld.add_argument("--preserve-comments",
162                 action="store_true",
163                 help="preserve the comments from the existing use.local.desc file")
164         uld.add_argument("--use-local-desc-output",
165                 help="output file for use.local.desc data (or '-' for stdout)",
166                 dest="uld_output")
167
168         options, args = parser.parse_known_args(args)
169
170         if options.jobs:
171                 jobs = None
172                 try:
173                         jobs = int(options.jobs)
174                 except ValueError:
175                         jobs = -1
176
177                 if jobs < 1:
178                         parser.error("Invalid: --jobs='%s'" % \
179                                 (options.jobs,))
180
181                 options.jobs = jobs
182
183         else:
184                 options.jobs = None
185
186         if options.load_average:
187                 try:
188                         load_average = float(options.load_average)
189                 except ValueError:
190                         load_average = 0.0
191
192                 if load_average <= 0.0:
193                         parser.error("Invalid: --load-average='%s'" % \
194                                 (options.load_average,))
195
196                 options.load_average = load_average
197
198         else:
199                 options.load_average = None
200
201         options.config_root = options.portage_configroot
202         if options.config_root is not None and \
203                 not os.path.isdir(options.config_root):
204                 parser.error("Not a directory: --config-root='%s'" % \
205                         (options.config_root,))
206
207         if options.cache_dir is not None:
208                 if not os.path.isdir(options.cache_dir):
209                         parser.error("Not a directory: --cache-dir='%s'" % \
210                                 (options.cache_dir,))
211                 if not os.access(options.cache_dir, os.W_OK):
212                         parser.error("Write access denied: --cache-dir='%s'" % \
213                                 (options.cache_dir,))
214
215         if options.portdir is not None:
216                 writemsg_level("egencache: warning: --portdir option is deprecated in favor of --repositories-configuration option\n",
217                         level=logging.WARNING, noiselevel=-1)
218         if options.portdir_overlay is not None:
219                 writemsg_level("egencache: warning: --portdir-overlay option is deprecated in favor of --repositories-configuration option\n",
220                         level=logging.WARNING, noiselevel=-1)
221
222         for atom in args:
223                 try:
224                         atom = portage.dep.Atom(atom)
225                 except portage.exception.InvalidAtom:
226                         parser.error('Invalid atom: %s' % (atom,))
227
228                 if not isjustname(atom):
229                         parser.error('Atom is too specific: %s' % (atom,))
230
231         if options.update_use_local_desc:
232                 try:
233                         ElementTree
234                         ExpatError
235                 except NameError:
236                         parser.error('--update-use-local-desc requires python with USE=xml!')
237
238         if options.uld_output == '-' and options.preserve_comments:
239                 parser.error('--preserve-comments can not be used when outputting to stdout')
240
241         return parser, options, args
242
243 class GenCache(object):
244         def __init__(self, portdb, cp_iter=None, max_jobs=None, max_load=None,
245                 rsync=False):
246                 # The caller must set portdb.porttrees in order to constrain
247                 # findname, cp_list, and cpv_list to the desired tree.
248                 tree = portdb.porttrees[0]
249                 self._portdb = portdb
250                 self._eclass_db = portdb.repositories.get_repo_for_location(tree).eclass_db
251                 self._auxdbkeys = portdb._known_keys
252                 # We can globally cleanse stale cache only if we
253                 # iterate over every single cp.
254                 self._global_cleanse = cp_iter is None
255                 if cp_iter is not None:
256                         self._cp_set = set(cp_iter)
257                         cp_iter = iter(self._cp_set)
258                         self._cp_missing = self._cp_set.copy()
259                 else:
260                         self._cp_set = None
261                         self._cp_missing = set()
262                 write_auxdb = "metadata-transfer" in portdb.settings.features
263                 self._regen = MetadataRegen(portdb, cp_iter=cp_iter,
264                         consumer=self._metadata_callback,
265                         max_jobs=max_jobs, max_load=max_load,
266                         write_auxdb=write_auxdb, main=True)
267                 self.returncode = os.EX_OK
268                 conf = portdb.repositories.get_repo_for_location(tree)
269                 self._trg_caches = tuple(conf.iter_pregenerated_caches(
270                         self._auxdbkeys, force=True, readonly=False))
271                 if not self._trg_caches:
272                         raise Exception("cache formats '%s' aren't supported" %
273                                 (" ".join(conf.cache_formats),))
274
275                 if rsync:
276                         for trg_cache in self._trg_caches:
277                                 if hasattr(trg_cache, 'raise_stat_collision'):
278                                         trg_cache.raise_stat_collision = True
279                                         # Make _metadata_callback write this cache first, in case
280                                         # it raises a StatCollision and triggers mtime
281                                         # modification.
282                                         self._trg_caches = tuple([trg_cache] +
283                                                 [x for x in self._trg_caches if x is not trg_cache])
284
285                 self._existing_nodes = set()
286
287         def _metadata_callback(self, cpv, repo_path, metadata,
288                 ebuild_hash, eapi_supported):
289                 self._existing_nodes.add(cpv)
290                 self._cp_missing.discard(cpv_getkey(cpv))
291
292                 # Since we're supposed to be able to efficiently obtain the
293                 # EAPI from _parse_eapi_ebuild_head, we don't write cache
294                 # entries for unsupported EAPIs.
295                 if metadata is not None and eapi_supported:
296                         if metadata.get('EAPI') == '0':
297                                 del metadata['EAPI']
298                         for trg_cache in self._trg_caches:
299                                 self._write_cache(trg_cache,
300                                         cpv, repo_path, metadata, ebuild_hash)
301
302         def _write_cache(self, trg_cache, cpv, repo_path, metadata, ebuild_hash):
303
304                 if not hasattr(trg_cache, 'raise_stat_collision'):
305                         # This cache does not avoid redundant writes automatically,
306                         # so check for an identical existing entry before writing.
307                         # This prevents unnecessary disk writes and can also prevent
308                         # unnecessary rsync transfers.
309                         try:
310                                 dest = trg_cache[cpv]
311                         except (KeyError, CacheError):
312                                 pass
313                         else:
314                                 if trg_cache.validate_entry(dest,
315                                         ebuild_hash, self._eclass_db):
316                                         identical = True
317                                         for k in self._auxdbkeys:
318                                                 if dest.get(k, '') != metadata.get(k, ''):
319                                                         identical = False
320                                                         break
321                                         if identical:
322                                                 return
323
324                 try:
325                         chf = trg_cache.validation_chf
326                         metadata['_%s_' % chf] = getattr(ebuild_hash, chf)
327                         try:
328                                 trg_cache[cpv] = metadata
329                         except StatCollision as sc:
330                                 # If the content of a cache entry changes and neither the
331                                 # file mtime nor size changes, it will prevent rsync from
332                                 # detecting changes. Cache backends may raise this
333                                 # exception from _setitem() if they detect this type of stat
334                                 # collision. These exceptions are handled by bumping the
335                                 # mtime on the ebuild (and the corresponding cache entry).
336                                 # See bug #139134. It is convenient to include checks for
337                                 # redundant writes along with the internal StatCollision
338                                 # detection code, so for caches with the
339                                 # raise_stat_collision attribute, we do not need to
340                                 # explicitly check for redundant writes like we do for the
341                                 # other cache types above.
342                                 max_mtime = sc.mtime
343                                 for _ec, ec_hash in metadata['_eclasses_'].items():
344                                         if max_mtime < ec_hash.mtime:
345                                                 max_mtime = ec_hash.mtime
346                                 if max_mtime == sc.mtime:
347                                         max_mtime += 1
348                                 max_mtime = long(max_mtime)
349                                 try:
350                                         os.utime(ebuild_hash.location, (max_mtime, max_mtime))
351                                 except OSError as e:
352                                         self.returncode |= 1
353                                         writemsg_level(
354                                                 "%s writing target: %s\n" % (cpv, e),
355                                                 level=logging.ERROR, noiselevel=-1)
356                                 else:
357                                         ebuild_hash.mtime = max_mtime
358                                         metadata['_mtime_'] = max_mtime
359                                         trg_cache[cpv] = metadata
360                                         self._portdb.auxdb[repo_path][cpv] = metadata
361
362                 except CacheError as ce:
363                         self.returncode |= 1
364                         writemsg_level(
365                                 "%s writing target: %s\n" % (cpv, ce),
366                                 level=logging.ERROR, noiselevel=-1)
367
368         def run(self):
369                 signum = run_main_scheduler(self._regen)
370                 if signum is not None:
371                         sys.exit(128 + signum)
372
373                 self.returncode |= self._regen.returncode
374
375                 for trg_cache in self._trg_caches:
376                         self._cleanse_cache(trg_cache)
377
378         def _cleanse_cache(self, trg_cache):
379                 cp_missing = self._cp_missing
380                 dead_nodes = set()
381                 if self._global_cleanse:
382                         try:
383                                 for cpv in trg_cache:
384                                         cp = cpv_getkey(cpv)
385                                         if cp is None:
386                                                 self.returncode |= 1
387                                                 writemsg_level(
388                                                         "Unable to parse cp for '%s'\n"  % (cpv,),
389                                                         level=logging.ERROR, noiselevel=-1)
390                                         else:
391                                                 dead_nodes.add(cpv)
392                         except CacheError as ce:
393                                 self.returncode |= 1
394                                 writemsg_level(
395                                         "Error listing cache entries for " + \
396                                         "'%s': %s, continuing...\n" % \
397                                         (trg_cache.location, ce),
398                                         level=logging.ERROR, noiselevel=-1)
399
400                 else:
401                         cp_set = self._cp_set
402                         try:
403                                 for cpv in trg_cache:
404                                         cp = cpv_getkey(cpv)
405                                         if cp is None:
406                                                 self.returncode |= 1
407                                                 writemsg_level(
408                                                         "Unable to parse cp for '%s'\n"  % (cpv,),
409                                                         level=logging.ERROR, noiselevel=-1)
410                                         else:
411                                                 cp_missing.discard(cp)
412                                                 if cp in cp_set:
413                                                         dead_nodes.add(cpv)
414                         except CacheError as ce:
415                                 self.returncode |= 1
416                                 writemsg_level(
417                                         "Error listing cache entries for " + \
418                                         "'%s': %s, continuing...\n" % \
419                                         (trg_cache.location, ce),
420                                         level=logging.ERROR, noiselevel=-1)
421
422                 if cp_missing:
423                         self.returncode |= 1
424                         for cp in sorted(cp_missing):
425                                 writemsg_level(
426                                         "No ebuilds or cache entries found for '%s'\n"  % (cp,),
427                                         level=logging.ERROR, noiselevel=-1)
428
429                 if dead_nodes:
430                         dead_nodes.difference_update(self._existing_nodes)
431                         for k in dead_nodes:
432                                 try:
433                                         del trg_cache[k]
434                                 except KeyError:
435                                         pass
436                                 except CacheError as ce:
437                                         self.returncode |= 1
438                                         writemsg_level(
439                                                 "%s deleting stale cache: %s\n" % (k, ce),
440                                                 level=logging.ERROR, noiselevel=-1)
441
442                 if not trg_cache.autocommits:
443                         try:
444                                 trg_cache.commit()
445                         except CacheError as ce:
446                                 self.returncode |= 1
447                                 writemsg_level(
448                                         "committing target: %s\n" % (ce,),
449                                         level=logging.ERROR, noiselevel=-1)
450
451                 if hasattr(trg_cache, '_prune_empty_dirs'):
452                         trg_cache._prune_empty_dirs()
453
454 class GenUseLocalDesc(object):
455         def __init__(self, portdb, output=None,
456                         preserve_comments=False):
457                 self.returncode = os.EX_OK
458                 self._portdb = portdb
459                 self._output = output
460                 self._preserve_comments = preserve_comments
461
462         def run(self):
463                 repo_path = self._portdb.porttrees[0]
464                 ops = {'<':0, '<=':1, '=':2, '>=':3, '>':4}
465
466                 if self._output is None or self._output != '-':
467                         if self._output is None:
468                                 prof_path = os.path.join(repo_path, 'profiles')
469                                 desc_path = os.path.join(prof_path, 'use.local.desc')
470                                 try:
471                                         os.mkdir(prof_path)
472                                 except OSError:
473                                         pass
474                         else:
475                                 desc_path = self._output
476
477                         try:
478                                 if self._preserve_comments:
479                                         # Probe in binary mode, in order to avoid
480                                         # potential character encoding issues.
481                                         output = open(_unicode_encode(desc_path,
482                                                 encoding=_encodings['fs'], errors='strict'), 'r+b')
483                                 else:
484                                         output = io.open(_unicode_encode(desc_path,
485                                                 encoding=_encodings['fs'], errors='strict'),
486                                                 mode='w', encoding=_encodings['repo.content'],
487                                                 errors='backslashreplace')
488                         except IOError as e:
489                                 if not self._preserve_comments or \
490                                         os.path.isfile(desc_path):
491                                         writemsg_level(
492                                                 "ERROR: failed to open output file %s: %s\n" \
493                                                 % (desc_path, e), level=logging.ERROR, noiselevel=-1)
494                                         self.returncode |= 2
495                                         return
496
497                                 # Open in r+b mode failed because the file doesn't
498                                 # exist yet. We can probably recover if we disable
499                                 # preserve_comments mode now.
500                                 writemsg_level(
501                                         "WARNING: --preserve-comments enabled, but " + \
502                                         "output file not found: %s\n" % (desc_path,),
503                                         level=logging.WARNING, noiselevel=-1)
504                                 self._preserve_comments = False
505                                 try:
506                                         output = io.open(_unicode_encode(desc_path,
507                                                 encoding=_encodings['fs'], errors='strict'),
508                                                 mode='w', encoding=_encodings['repo.content'],
509                                                 errors='backslashreplace')
510                                 except IOError as e:
511                                         writemsg_level(
512                                                 "ERROR: failed to open output file %s: %s\n" \
513                                                 % (desc_path, e), level=logging.ERROR, noiselevel=-1)
514                                         self.returncode |= 2
515                                         return
516                 else:
517                         output = sys.stdout
518
519                 if self._preserve_comments:
520                         while True:
521                                 pos = output.tell()
522                                 if not output.readline().startswith(b'#'):
523                                         break
524                         output.seek(pos)
525                         output.truncate()
526                         output.close()
527
528                         # Finished probing comments in binary mode, now append
529                         # in text mode.
530                         output = io.open(_unicode_encode(desc_path,
531                                 encoding=_encodings['fs'], errors='strict'),
532                                 mode='a', encoding=_encodings['repo.content'],
533                                 errors='backslashreplace')
534                         output.write('\n')
535                 else:
536                         output.write(textwrap.dedent('''\
537                                 # This file is deprecated as per GLEP 56 in favor of metadata.xml. Please add
538                                 # your descriptions to your package's metadata.xml ONLY.
539                                 # * generated automatically using egencache *
540
541                                 '''))
542
543                 # The cmp function no longer exists in python3, so we'll
544                 # implement our own here under a slightly different name
545                 # since we don't want any confusion given that we never
546                 # want to rely on the builtin cmp function.
547                 def cmp_func(a, b):
548                         if a is None or b is None:
549                                 # None can't be compared with other types in python3.
550                                 if a is None and b is None:
551                                         return 0
552                                 elif a is None:
553                                         return -1
554                                 else:
555                                         return 1
556                         return (a > b) - (a < b)
557
558                 class _MetadataTreeBuilder(ElementTree.TreeBuilder):
559                         """
560                         Implements doctype() as required to avoid deprecation warnings
561                         since Python >=2.7
562                         """
563                         def doctype(self, name, pubid, system):
564                                 pass
565
566                 for cp in self._portdb.cp_all():
567                         metadata_path = os.path.join(repo_path, cp, 'metadata.xml')
568                         try:
569                                 metadata = ElementTree.parse(_unicode_encode(metadata_path,
570                                         encoding=_encodings['fs'], errors='strict'),
571                                         parser=ElementTree.XMLParser(
572                                         target=_MetadataTreeBuilder()))
573                         except IOError:
574                                 pass
575                         except (ExpatError, EnvironmentError) as e:
576                                 writemsg_level(
577                                         "ERROR: failed parsing %s/metadata.xml: %s\n" % (cp, e),
578                                         level=logging.ERROR, noiselevel=-1)
579                                 self.returncode |= 1
580                         else:
581                                 try:
582                                         usedict = parse_metadata_use(metadata)
583                                 except portage.exception.ParseError as e:
584                                         writemsg_level(
585                                                 "ERROR: failed parsing %s/metadata.xml: %s\n" % (cp, e),
586                                                 level=logging.ERROR, noiselevel=-1)
587                                         self.returncode |= 1
588                                 else:
589                                         for flag in sorted(usedict):
590                                                 def atomcmp(atoma, atomb):
591                                                         # None is better than an atom, that's why we reverse the args
592                                                         if atoma is None or atomb is None:
593                                                                 return cmp_func(atomb, atoma)
594                                                         # Same for plain PNs (.operator is None then)
595                                                         elif atoma.operator is None or atomb.operator is None:
596                                                                 return cmp_func(atomb.operator, atoma.operator)
597                                                         # Version matching
598                                                         elif atoma.cpv != atomb.cpv:
599                                                                 return vercmp(atoma.version, atomb.version)
600                                                         # Versions match, let's fallback to operator matching
601                                                         else:
602                                                                 return cmp_func(ops.get(atoma.operator, -1),
603                                                                         ops.get(atomb.operator, -1))
604
605                                                 def _Atom(key):
606                                                         if key is not None:
607                                                                 return Atom(key)
608                                                         return None
609
610                                                 resdict = usedict[flag]
611                                                 if len(resdict) == 1:
612                                                         resdesc = next(iter(resdict.items()))[1]
613                                                 else:
614                                                         try:
615                                                                 reskeys = dict((_Atom(k), k) for k in resdict)
616                                                         except portage.exception.InvalidAtom as e:
617                                                                 writemsg_level(
618                                                                         "ERROR: failed parsing %s/metadata.xml: %s\n" % (cp, e),
619                                                                         level=logging.ERROR, noiselevel=-1)
620                                                                 self.returncode |= 1
621                                                                 resdesc = next(iter(resdict.items()))[1]
622                                                         else:
623                                                                 resatoms = sorted(reskeys, key=cmp_sort_key(atomcmp))
624                                                                 resdesc = resdict[reskeys[resatoms[-1]]]
625
626                                                 output.write('%s:%s - %s\n' % (cp, flag, resdesc))
627
628                 output.close()
629
630 if sys.hexversion < 0x3000000:
631         _filename_base = unicode
632 else:
633         _filename_base = str
634
635 class _special_filename(_filename_base):
636         """
637         Helps to sort file names by file type and other criteria.
638         """
639         def __new__(cls, status_change, file_name):
640                 return _filename_base.__new__(cls, status_change + file_name)
641
642         def __init__(self, status_change, file_name):
643                 _filename_base.__init__(status_change + file_name)
644                 self.status_change = status_change
645                 self.file_name = file_name
646                 self.file_type = guessManifestFileType(file_name)
647
648         @staticmethod
649         def file_type_lt(a, b):
650                 """
651                 Defines an ordering between file types.
652                 """
653                 first = a.file_type
654                 second = b.file_type
655                 if first == second:
656                         return False
657
658                 if first == "EBUILD":
659                         return True
660                 elif first == "MISC":
661                         return second in ("EBUILD",)
662                 elif first == "AUX":
663                         return second in ("EBUILD", "MISC")
664                 elif first == "DIST":
665                         return second in ("EBUILD", "MISC", "AUX")
666                 elif first is None:
667                         return False
668                 else:
669                         raise ValueError("Unknown file type '%s'" % first)
670
671         def __lt__(self, other):
672                 """
673                 Compare different file names, first by file type and then
674                 for ebuilds by version and lexicographically for others.
675                 EBUILD < MISC < AUX < DIST < None
676                 """
677                 if self.__class__ != other.__class__:
678                         raise NotImplementedError
679
680                 # Sort by file type as defined by file_type_lt().
681                 if self.file_type_lt(self, other):
682                         return True
683                 elif self.file_type_lt(other, self):
684                         return False
685
686                 # Files have the same type.
687                 if self.file_type == "EBUILD":
688                         # Sort by version. Lowest first.
689                         ver = "-".join(pkgsplit(self.file_name[:-7])[1:3])
690                         other_ver = "-".join(pkgsplit(other.file_name[:-7])[1:3])
691                         return vercmp(ver, other_ver) < 0
692                 else:
693                         # Sort lexicographically.
694                         return self.file_name < other.file_name
695
696 class GenChangeLogs(object):
697         def __init__(self, portdb):
698                 self.returncode = os.EX_OK
699                 self._portdb = portdb
700                 self._wrapper = textwrap.TextWrapper(
701                                 width = 78,
702                                 initial_indent = '  ',
703                                 subsequent_indent = '  '
704                         )
705
706         @staticmethod
707         def grab(cmd):
708                 p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
709                 return _unicode_decode(p.communicate()[0],
710                                 encoding=_encodings['stdio'], errors='strict')
711
712         def generate_changelog(self, cp):
713                 try:
714                         output = io.open('ChangeLog',
715                                 mode='w', encoding=_encodings['repo.content'],
716                                 errors='backslashreplace')
717                 except IOError as e:
718                         writemsg_level(
719                                 "ERROR: failed to open ChangeLog for %s: %s\n" % (cp,e,),
720                                 level=logging.ERROR, noiselevel=-1)
721                         self.returncode |= 2
722                         return
723
724                 output.write(textwrap.dedent('''\
725                         # ChangeLog for %s
726                         # Copyright 1999-%s Gentoo Foundation; Distributed under the GPL v2
727                         # $Header: $
728
729                         ''' % (cp, time.strftime('%Y'))))
730
731                 # now grab all the commits
732                 commits = self.grab(['git', 'rev-list', 'HEAD', '--', '.']).split()
733
734                 for c in commits:
735                         # Explaining the arguments:
736                         # --name-status to get a list of added/removed files
737                         # --no-renames to avoid getting more complex records on the list
738                         # --format to get the timestamp, author and commit description
739                         # --root to make it work fine even with the initial commit
740                         # --relative to get paths relative to ebuilddir
741                         # -r (recursive) to get per-file changes
742                         # then the commit-id and path.
743
744                         cinfo = self.grab(['git', 'diff-tree', '--name-status', '--no-renames',
745                                         '--format=%ct %cN <%cE>%n%B', '--root', '--relative', '-r',
746                                         c, '--', '.']).rstrip('\n').split('\n')
747
748                         # Expected output:
749                         # timestamp Author Name <author@email>
750                         # commit message l1
751                         # ...
752                         # commit message ln
753                         #
754                         # status1       filename1
755                         # ...
756                         # statusn       filenamen
757
758                         changed = []
759                         for n, l in enumerate(reversed(cinfo)):
760                                 if not l:
761                                         body = cinfo[1:-n-1]
762                                         break
763                                 else:
764                                         f = l.split()
765                                         if f[1] == 'Manifest':
766                                                 pass # XXX: remanifest commits?
767                                         elif f[1] == 'ChangeLog':
768                                                 pass
769                                         elif f[0].startswith('A'):
770                                                 changed.append(_special_filename("+", f[1]))
771                                         elif f[0].startswith('D'):
772                                                 changed.append(_special_filename("-", f[1]))
773                                         elif f[0].startswith('M'):
774                                                 changed.append(_special_filename("", f[1]))
775                                         else:
776                                                 writemsg_level(
777                                                         "ERROR: unexpected git file status for %s: %s\n" % (cp,f,),
778                                                         level=logging.ERROR, noiselevel=-1)
779                                                 self.returncode |= 1
780
781                         if not changed:
782                                 continue
783
784                         (ts, author) = cinfo[0].split(' ', 1)
785                         date = time.strftime('%d %b %Y', time.gmtime(float(ts)))
786
787                         changed = [str(x) for x in sorted(changed)]
788
789                         wroteheader = False
790                         # Reverse the sort order for headers.
791                         for c in reversed(changed):
792                                 if c.startswith('+') and c.endswith('.ebuild'):
793                                         output.write('*%s (%s)\n' % (c[1:-7], date))
794                                         wroteheader = True
795                         if wroteheader:
796                                 output.write('\n')
797
798                         # strip '<cp>: ', '[<cp>] ', and similar
799                         body[0] = re.sub(r'^\W*' + re.escape(cp) + r'\W+', '', body[0])
800                         # strip trailing newline
801                         if not body[-1]:
802                                 body = body[:-1]
803                         # strip git-svn id
804                         if body[-1].startswith('git-svn-id:') and not body[-2]:
805                                 body = body[:-2]
806                         # strip the repoman version/manifest note
807                         if body[-1] == ' (Signed Manifest commit)' or body[-1] == ' (Unsigned Manifest commit)':
808                                 body = body[:-1]
809                         if body[-1].startswith('(Portage version:') and body[-1].endswith(')'):
810                                 body = body[:-1]
811                                 if not body[-1]:
812                                         body = body[:-1]
813
814                         # don't break filenames on hyphens
815                         self._wrapper.break_on_hyphens = False
816                         output.write(self._wrapper.fill(
817                                 '%s; %s %s:' % (date, author, ', '.join(changed))))
818                         # but feel free to break commit messages there
819                         self._wrapper.break_on_hyphens = True
820                         output.write(
821                                 '\n%s\n\n' % '\n'.join(self._wrapper.fill(x) for x in body))
822
823                 output.close()
824
825         def run(self):
826                 repo_path = self._portdb.porttrees[0]
827                 os.chdir(repo_path)
828
829                 if 'git' not in FindVCS():
830                         writemsg_level(
831                                 "ERROR: --update-changelogs supported only in git repos\n",
832                                 level=logging.ERROR, noiselevel=-1)
833                         self.returncode = 127
834                         return
835
836                 for cp in self._portdb.cp_all():
837                         os.chdir(os.path.join(repo_path, cp))
838                         # Determine whether ChangeLog is up-to-date by comparing
839                         # the newest commit timestamp with the ChangeLog timestamp.
840                         lmod = self.grab(['git', 'log', '--format=%ct', '-1', '.'])
841                         if not lmod:
842                                 # This cp has not been added to the repo.
843                                 continue
844
845                         try:
846                                 cmod = os.stat('ChangeLog').st_mtime
847                         except OSError:
848                                 cmod = 0
849
850                         if float(cmod) < float(lmod):
851                                 self.generate_changelog(cp)
852
853 def egencache_main(args):
854
855         # The calling environment is ignored, so the program is
856         # completely controlled by commandline arguments.
857         env = {}
858
859         if not sys.stdout.isatty():
860                 portage.output.nocolor()
861                 env['NOCOLOR'] = 'true'
862
863         parser, options, atoms = parse_args(args)
864
865         config_root = options.config_root
866
867         if options.repositories_configuration is not None:
868                 env['PORTAGE_REPOSITORIES'] = options.repositories_configuration
869         elif options.portdir_overlay is not None:
870                 env['PORTDIR_OVERLAY'] = options.portdir_overlay
871
872         if options.cache_dir is not None:
873                 env['PORTAGE_DEPCACHEDIR'] = options.cache_dir
874
875         if options.portdir is not None:
876                 env['PORTDIR'] = options.portdir
877
878         settings = portage.config(config_root=config_root,
879                 local_config=False, env=env)
880
881         default_opts = None
882         if not options.ignore_default_opts:
883                 default_opts = portage.util.shlex_split(
884                         settings.get('EGENCACHE_DEFAULT_OPTS', ''))
885
886         if default_opts:
887                 parser, options, args = parse_args(default_opts + args)
888
889                 if options.cache_dir is not None:
890                         env['PORTAGE_DEPCACHEDIR'] = options.cache_dir
891
892                 settings = portage.config(config_root=config_root,
893                         local_config=False, env=env)
894
895         if not (options.update or options.update_use_local_desc or
896                         options.update_changelogs or options.update_manifests):
897                 parser.error('No action specified')
898                 return 1
899
900         if options.repo is None:
901                 if len(settings.repositories.prepos) == 2:
902                         for repo in settings.repositories:
903                                 if repo.name != "DEFAULT":
904                                         options.repo = repo.name
905                                         break
906
907                 if options.repo is None:
908                         parser.error("--repo option is required")
909
910         repo_path = settings.repositories.treemap.get(options.repo)
911         if repo_path is None:
912                 parser.error("Unable to locate repository named '%s'" % (options.repo,))
913                 return 1
914
915         repo_config = settings.repositories.get_repo_for_location(repo_path)
916
917         if options.strict_manifests is not None:
918                 if options.strict_manifests == "y":
919                         settings.features.add("strict")
920                 else:
921                         settings.features.discard("strict")
922
923         if options.update and 'metadata-transfer' not in settings.features:
924                 # Forcibly enable metadata-transfer if portdbapi has a pregenerated
925                 # cache that does not support eclass validation.
926                 cache = repo_config.get_pregenerated_cache(
927                         portage.dbapi.dbapi._known_keys, readonly=True)
928                 if cache is not None and not cache.complete_eclass_entries:
929                         settings.features.add('metadata-transfer')
930                 cache = None
931
932         settings.lock()
933
934         portdb = portage.portdbapi(mysettings=settings)
935
936         # Limit ebuilds to the specified repo.
937         portdb.porttrees = [repo_path]
938
939         if options.update:
940                 if options.cache_dir is not None:
941                         # already validated earlier
942                         pass
943                 else:
944                         # We check write access after the portdbapi constructor
945                         # has had an opportunity to create it. This ensures that
946                         # we don't use the cache in the "volatile" mode which is
947                         # undesirable for egencache.
948                         if not os.access(settings["PORTAGE_DEPCACHEDIR"], os.W_OK):
949                                 writemsg_level("ecachegen: error: " + \
950                                         "write access denied: %s\n" % (settings["PORTAGE_DEPCACHEDIR"],),
951                                         level=logging.ERROR, noiselevel=-1)
952                                 return 1
953
954         if options.sign_manifests is not None:
955                 repo_config.sign_manifest = options.sign_manifests == 'y'
956
957         if options.thin_manifests is not None:
958                 repo_config.thin_manifest = options.thin_manifests == 'y'
959
960         gpg_cmd = None
961         gpg_vars = None
962         force_sign_key = None
963
964         if options.update_manifests:
965                 if repo_config.sign_manifest:
966
967                         sign_problem = False
968                         gpg_dir = None
969                         gpg_cmd = settings.get("PORTAGE_GPG_SIGNING_COMMAND")
970                         if gpg_cmd is None:
971                                 writemsg_level("egencache: error: "
972                                         "PORTAGE_GPG_SIGNING_COMMAND is unset! "
973                                         "Is make.globals missing?\n",
974                                         level=logging.ERROR, noiselevel=-1)
975                                 sign_problem = True
976                         elif "${PORTAGE_GPG_KEY}" in gpg_cmd and \
977                                 options.gpg_key is None and \
978                                 "PORTAGE_GPG_KEY" not in settings:
979                                 writemsg_level("egencache: error: "
980                                         "PORTAGE_GPG_KEY is unset!\n",
981                                         level=logging.ERROR, noiselevel=-1)
982                                 sign_problem = True
983                         elif "${PORTAGE_GPG_DIR}" in gpg_cmd:
984                                 if options.gpg_dir is not None:
985                                         gpg_dir = options.gpg_dir
986                                 elif "PORTAGE_GPG_DIR" not in settings:
987                                         gpg_dir = os.path.expanduser("~/.gnupg")
988                                 else:
989                                         gpg_dir = os.path.expanduser(settings["PORTAGE_GPG_DIR"])
990                                 if not os.access(gpg_dir, os.X_OK):
991                                         writemsg_level(("egencache: error: "
992                                                 "Unable to access directory: "
993                                                 "PORTAGE_GPG_DIR='%s'\n") % gpg_dir,
994                                                 level=logging.ERROR, noiselevel=-1)
995                                         sign_problem = True
996
997                         if sign_problem:
998                                 writemsg_level("egencache: You may disable manifest "
999                                         "signatures with --sign-manifests=n or by setting "
1000                                         "\"sign-manifests = false\" in metadata/layout.conf\n",
1001                                         level=logging.ERROR, noiselevel=-1)
1002                                 return 1
1003
1004                         gpg_vars = {}
1005                         if gpg_dir is not None:
1006                                 gpg_vars["PORTAGE_GPG_DIR"] = gpg_dir
1007                         gpg_var_names = []
1008                         if options.gpg_key is None:
1009                                 gpg_var_names.append("PORTAGE_GPG_KEY")
1010                         else:
1011                                 gpg_vars["PORTAGE_GPG_KEY"] = options.gpg_key
1012
1013                         for k in gpg_var_names:
1014                                 v = settings.get(k)
1015                                 if v is not None:
1016                                         gpg_vars[k] = v
1017
1018                         force_sign_key = gpg_vars.get("PORTAGE_GPG_KEY")
1019
1020         ret = [os.EX_OK]
1021
1022         if options.update:
1023                 cp_iter = None
1024                 if atoms:
1025                         cp_iter = iter(atoms)
1026
1027                 gen_cache = GenCache(portdb, cp_iter=cp_iter,
1028                         max_jobs=options.jobs,
1029                         max_load=options.load_average,
1030                         rsync=options.rsync)
1031                 gen_cache.run()
1032                 if options.tolerant:
1033                         ret.append(os.EX_OK)
1034                 else:
1035                         ret.append(gen_cache.returncode)
1036
1037         if options.update_manifests:
1038
1039                 cp_iter = None
1040                 if atoms:
1041                         cp_iter = iter(atoms)
1042
1043                 event_loop = global_event_loop()
1044                 scheduler = ManifestScheduler(portdb, cp_iter=cp_iter,
1045                         gpg_cmd=gpg_cmd, gpg_vars=gpg_vars,
1046                         force_sign_key=force_sign_key,
1047                         max_jobs=options.jobs,
1048                         max_load=options.load_average,
1049                         event_loop=event_loop)
1050
1051                 signum = run_main_scheduler(scheduler)
1052                 if signum is not None:
1053                         sys.exit(128 + signum)
1054
1055                 if options.tolerant:
1056                         ret.append(os.EX_OK)
1057                 else:
1058                         ret.append(scheduler.returncode)
1059
1060         if options.update_use_local_desc:
1061                 gen_desc = GenUseLocalDesc(portdb,
1062                         output=options.uld_output,
1063                         preserve_comments=options.preserve_comments)
1064                 gen_desc.run()
1065                 ret.append(gen_desc.returncode)
1066
1067         if options.update_changelogs:
1068                 gen_clogs = GenChangeLogs(portdb)
1069                 gen_clogs.run()
1070                 ret.append(gen_clogs.returncode)
1071
1072         if options.write_timestamp:
1073                 timestamp_path = os.path.join(repo_path, 'metadata', 'timestamp.chk')
1074                 try:
1075                         with open(timestamp_path, 'w') as f:
1076                                 f.write(time.strftime('%s\n' % TIMESTAMP_FORMAT, time.gmtime()))
1077                 except IOError:
1078                         ret.append(os.EX_IOERR)
1079                 else:
1080                         ret.append(os.EX_OK)
1081
1082         return max(ret)
1083
1084 if __name__ == "__main__":
1085         portage._disable_legacy_globals()
1086         portage.util.noiselimit = -1
1087         sys.exit(egencache_main(sys.argv[1:]))