egencache: Introduce the --update-use-local-desc action.
[portage.git] / bin / egencache
1 #!/usr/bin/python
2 # Copyright 2009-2010 Gentoo Foundation
3 # Distributed under the terms of the GNU General Public License v2
4
5 from __future__ import print_function
6
7 import sys
8 # This block ensures that ^C interrupts are handled quietly.
9 try:
10         import signal
11
12         def exithandler(signum,frame):
13                 signal.signal(signal.SIGINT, signal.SIG_IGN)
14                 signal.signal(signal.SIGTERM, signal.SIG_IGN)
15                 sys.exit(1)
16
17         signal.signal(signal.SIGINT, exithandler)
18         signal.signal(signal.SIGTERM, exithandler)
19
20 except KeyboardInterrupt:
21         sys.exit(1)
22
23 import logging
24 import optparse
25
26 try:
27         import portage
28 except ImportError:
29         from os import path as osp
30         sys.path.insert(0, osp.join(osp.dirname(osp.dirname(osp.realpath(__file__))), "pym"))
31         import portage
32
33 from portage import os
34 from _emerge.MetadataRegen import MetadataRegen
35 from portage.cache.cache_errors import CacheError, StatCollision
36 from portage.util import writemsg_level
37 from portage import cpv_getkey
38 from portage.dep import isjustname
39
40 try:
41         import xml.etree.ElementTree
42 except ImportError:
43         pass
44 else:
45         from repoman.utilities import parse_metadata_use
46         from xml.parsers.expat import ExpatError
47
48 if sys.hexversion >= 0x3000000:
49         long = int
50
51 def parse_args(args):
52         usage = "egencache [options] <action> ... [atom] ..."
53         parser = optparse.OptionParser(usage=usage)
54
55         actions = optparse.OptionGroup(parser, 'Actions')
56         actions.add_option("--update",
57                 action="store_true",
58                 help="update metadata/cache/ (generate as necessary)")
59         actions.add_option("--update-use-local-desc",
60                 action="store_true",
61                 help="update the use.local.desc file from metadata.xml")
62         parser.add_option_group(actions)
63
64         common = optparse.OptionGroup(parser, 'Common options')
65         common.add_option("--repo",
66                 action="store",
67                 help="name of repo to operate on (default repo is located at $PORTDIR)")
68         common.add_option("--config-root",
69                 help="location of portage config files",
70                 dest="portage_configroot")
71         common.add_option("--portdir",
72                 help="override the portage tree location",
73                 dest="portdir")
74         common.add_option("--tolerant",
75                 action="store_true",
76                 help="exit successfully if only minor errors occurred")
77         common.add_option("--ignore-default-opts",
78                 action="store_true",
79                 help="do not use the EGENCACHE_DEFAULT_OPTS environment variable")
80         parser.add_option_group(common)
81
82         update = optparse.OptionGroup(parser, '--update options')
83         update.add_option("--cache-dir",
84                 help="location of the metadata cache",
85                 dest="cache_dir")
86         update.add_option("--jobs",
87                 action="store",
88                 help="max ebuild processes to spawn")
89         update.add_option("--load-average",
90                 action="store",
91                 help="max load allowed when spawning multiple jobs",
92                 dest="load_average")
93         update.add_option("--rsync",
94                 action="store_true",
95                 help="enable rsync stat collision workaround " + \
96                         "for bug 139134 (use with --update)")
97         parser.add_option_group(update)
98
99         options, args = parser.parse_args(args)
100
101         if options.jobs:
102                 jobs = None
103                 try:
104                         jobs = int(options.jobs)
105                 except ValueError:
106                         jobs = -1
107
108                 if jobs < 1:
109                         parser.error("Invalid: --jobs='%s'" % \
110                                 (options.jobs,))
111
112                 options.jobs = jobs
113
114         else:
115                 options.jobs = None
116
117         if options.load_average:
118                 try:
119                         load_average = float(options.load_average)
120                 except ValueError:
121                         load_average = 0.0
122
123                 if load_average <= 0.0:
124                         parser.error("Invalid: --load-average='%s'" % \
125                                 (options.load_average,))
126
127                 options.load_average = load_average
128
129         else:
130                 options.load_average = None
131
132         options.config_root = options.portage_configroot
133         if options.config_root is not None and \
134                 not os.path.isdir(options.config_root):
135                 parser.error("Not a directory: --config-root='%s'" % \
136                         (options.config_root,))
137
138         if options.cache_dir is not None and not os.path.isdir(options.cache_dir):
139                 parser.error("Not a directory: --cache-dir='%s'" % \
140                         (options.cache_dir,))
141
142         for atom in args:
143                 try:
144                         atom = portage.dep.Atom(atom)
145                 except portage.exception.InvalidAtom:
146                         parser.error('Invalid atom: %s' % (atom,))
147
148                 if not isjustname(atom):
149                         parser.error('Atom is too specific: %s' % (atom,))
150
151         if options.update_use_local_desc:
152                 try:
153                         xml.etree.ElementTree
154                 except NameError:
155                         parser.error('--update-use-local-desc requires python with USE=xml!')
156
157         return parser, options, args
158
159 class GenCache(object):
160         def __init__(self, portdb, cp_iter=None, max_jobs=None, max_load=None,
161                 rsync=False):
162                 self._portdb = portdb
163                 # We can globally cleanse stale cache only if we
164                 # iterate over every single cp.
165                 self._global_cleanse = cp_iter is None
166                 if cp_iter is not None:
167                         self._cp_set = set(cp_iter)
168                         cp_iter = iter(self._cp_set)
169                         self._cp_missing = self._cp_set.copy()
170                 else:
171                         self._cp_set = None
172                         self._cp_missing = set()
173                 self._regen = MetadataRegen(portdb, cp_iter=cp_iter,
174                         consumer=self._metadata_callback,
175                         max_jobs=max_jobs, max_load=max_load)
176                 self.returncode = os.EX_OK
177                 metadbmodule = portdb.settings.load_best_module("portdbapi.metadbmodule")
178                 self._trg_cache = metadbmodule(portdb.porttrees[0],
179                         "metadata/cache", portage.auxdbkeys[:])
180                 if rsync:
181                         self._trg_cache.raise_stat_collision = True
182                 try:
183                         self._trg_cache.ec = \
184                                 portdb._repo_info[portdb.porttrees[0]].eclass_db
185                 except AttributeError:
186                         pass
187                 self._existing_nodes = set()
188
189         def _metadata_callback(self, cpv, ebuild_path, repo_path, metadata):
190                 self._existing_nodes.add(cpv)
191                 self._cp_missing.discard(cpv_getkey(cpv))
192                 if metadata is not None:
193                         if metadata.get('EAPI') == '0':
194                                 del metadata['EAPI']
195                         try:
196                                 try:
197                                         self._trg_cache[cpv] = metadata
198                                 except StatCollision as sc:
199                                         # If the content of a cache entry changes and neither the
200                                         # file mtime nor size changes, it will prevent rsync from
201                                         # detecting changes. Cache backends may raise this
202                                         # exception from _setitem() if they detect this type of stat
203                                         # collision. These exceptions are handled by bumping the
204                                         # mtime on the ebuild (and the corresponding cache entry).
205                                         # See bug #139134.
206                                         max_mtime = sc.mtime
207                                         for ec, (loc, ec_mtime) in metadata['_eclasses_'].items():
208                                                 if max_mtime < ec_mtime:
209                                                         max_mtime = ec_mtime
210                                         if max_mtime == sc.mtime:
211                                                 max_mtime += 1
212                                         max_mtime = long(max_mtime)
213                                         try:
214                                                 os.utime(ebuild_path, (max_mtime, max_mtime))
215                                         except OSError as e:
216                                                 self.returncode |= 1
217                                                 writemsg_level(
218                                                         "%s writing target: %s\n" % (cpv, e),
219                                                         level=logging.ERROR, noiselevel=-1)
220                                         else:
221                                                 metadata['_mtime_'] = max_mtime
222                                                 self._trg_cache[cpv] = metadata
223                                                 self._portdb.auxdb[repo_path][cpv] = metadata
224
225                         except CacheError as ce:
226                                 self.returncode |= 1
227                                 writemsg_level(
228                                         "%s writing target: %s\n" % (cpv, ce),
229                                         level=logging.ERROR, noiselevel=-1)
230
231         def run(self):
232                 self._regen.run()
233                 self.returncode |= self._regen.returncode
234                 cp_missing = self._cp_missing
235
236                 trg_cache = self._trg_cache
237                 dead_nodes = set()
238                 if self._global_cleanse:
239                         try:
240                                 for cpv in trg_cache:
241                                         cp = cpv_getkey(cpv)
242                                         if cp is None:
243                                                 self.returncode |= 1
244                                                 writemsg_level(
245                                                         "Unable to parse cp for '%s'\n"  % (cpv,),
246                                                         level=logging.ERROR, noiselevel=-1)
247                                         else:
248                                                 dead_nodes.add(cpv)
249                         except CacheError as ce:
250                                 self.returncode |= 1
251                                 writemsg_level(
252                                         "Error listing cache entries for " + \
253                                         "'%s/metadata/cache': %s, continuing...\n" % \
254                                         (self._portdb.porttree_root, ce),
255                                         level=logging.ERROR, noiselevel=-1)
256
257                 else:
258                         cp_set = self._cp_set
259                         try:
260                                 for cpv in trg_cache:
261                                         cp = cpv_getkey(cpv)
262                                         if cp is None:
263                                                 self.returncode |= 1
264                                                 writemsg_level(
265                                                         "Unable to parse cp for '%s'\n"  % (cpv,),
266                                                         level=logging.ERROR, noiselevel=-1)
267                                         else:
268                                                 cp_missing.discard(cp)
269                                                 if cp in cp_set:
270                                                         dead_nodes.add(cpv)
271                         except CacheError as ce:
272                                 self.returncode |= 1
273                                 writemsg_level(
274                                         "Error listing cache entries for " + \
275                                         "'%s/metadata/cache': %s, continuing...\n" % \
276                                         (self._portdb.porttree_root, ce),
277                                         level=logging.ERROR, noiselevel=-1)
278
279                 if cp_missing:
280                         self.returncode |= 1
281                         for cp in sorted(cp_missing):
282                                 writemsg_level(
283                                         "No ebuilds or cache entries found for '%s'\n"  % (cp,),
284                                         level=logging.ERROR, noiselevel=-1)
285
286                 if dead_nodes:
287                         dead_nodes.difference_update(self._existing_nodes)
288                         for k in dead_nodes:
289                                 try:
290                                         del trg_cache[k]
291                                 except KeyError:
292                                         pass
293                                 except CacheError as ce:
294                                         self.returncode |= 1
295                                         writemsg_level(
296                                                 "%s deleting stale cache: %s\n" % (k, ce),
297                                                 level=logging.ERROR, noiselevel=-1)
298
299                 if not trg_cache.autocommits:
300                         try:
301                                 trg_cache.commit()
302                         except CacheError as ce:
303                                 self.returncode |= 1
304                                 writemsg_level(
305                                         "committing target: %s\n" % (ce,),
306                                         level=logging.ERROR, noiselevel=-1)
307
308 class GenUseLocalDesc(object):
309         def __init__(self, portdb):
310                 self.returncode = os.EX_OK
311                 self._portdb = portdb
312         
313         def run(self):
314                 repo_path = self._portdb.porttrees[0]
315                 prof_path = os.path.join(repo_path, 'profiles')
316                 desc_path = os.path.join(prof_path, 'use.local.desc')
317
318                 try:
319                         os.mkdir(prof_path)
320                 except OSError:
321                         pass
322                 try:
323                         output = open(desc_path, 'w')
324                 except IOError as e:
325                         writemsg_level(
326                                 "ERROR: failed to open output file: %s\n" % (e,),
327                                 level=logging.ERROR, noiselevel=-1)
328                         self.returncode |= 2
329                         return
330
331                 output.write('''
332 # This file is deprecated as per GLEP 56 in favor of metadata.xml. Please add
333 # your descriptions to your package's metadata.xml ONLY.
334 # * generated automatically using egencache *
335
336 '''.lstrip())
337
338                 for cp in self._portdb.cp_all():
339                         metadata_path = os.path.join(repo_path, cp, 'metadata.xml')
340                         try:
341                                 metadata = xml.etree.ElementTree.parse(metadata_path)
342                         except IOError:
343                                 pass
344                         except (ExpatError, EnvironmentError) as e:
345                                 writemsg_level(
346                                         "ERROR: failed parsing %s/metadata.xml: %s\n" % (cp, e),
347                                         level=logging.ERROR, noiselevel=-1)
348                                 self.returncode |= 1
349                         else:
350                                 usedict = parse_metadata_use(metadata)
351                                 for flag in sorted(usedict.keys()):
352                                         output.write('%s:%s - %s\n' % (cp, flag, usedict[flag]))
353
354                 output.close()
355
356 def egencache_main(args):
357         parser, options, atoms = parse_args(args)
358
359         config_root = options.config_root
360         if config_root is None:
361                 config_root = '/'
362
363         # The calling environment is ignored, so the program is
364         # completely controlled by commandline arguments.
365         env = {}
366
367         if options.repo is None:
368                 env['PORTDIR_OVERLAY'] = ''
369
370         if options.cache_dir is not None:
371                 env['PORTAGE_DEPCACHEDIR'] = options.cache_dir
372
373         if options.portdir is not None:
374                 env['PORTDIR'] = options.portdir
375
376         settings = portage.config(config_root=config_root,
377                 target_root='/', local_config=False, env=env)
378
379         default_opts = None
380         if not options.ignore_default_opts:
381                 default_opts = settings.get('EGENCACHE_DEFAULT_OPTS', '').split()
382
383         if default_opts:
384                 parser, options, args = parse_args(default_opts + args)
385
386                 if options.config_root is not None:
387                         config_root = options.config_root
388
389                 if options.cache_dir is not None:
390                         env['PORTAGE_DEPCACHEDIR'] = options.cache_dir
391
392                 settings = portage.config(config_root=config_root,
393                         target_root='/', local_config=False, env=env)
394
395         if not options.update and not options.update_use_local_desc:
396                 parser.error('No action specified (--update ' + \
397                         'and/or --update-use-local-desc)')
398                 return 1
399
400         if 'metadata-transfer' not in settings.features:
401                 writemsg_level("ecachegen: warning: " + \
402                         "automatically enabling FEATURES=metadata-transfer\n",
403                         level=logging.WARNING, noiselevel=-1)
404                 settings.features.add('metadata-transfer')
405
406         settings.lock()
407
408         portdb = portage.portdbapi(mysettings=settings)
409         if options.repo is not None:
410                 repo_path = portdb.getRepositoryPath(options.repo)
411                 if repo_path is None:
412                         parser.error("Unable to locate repository named '%s'" % \
413                                 (options.repo,))
414                         return 1
415
416                 # Limit ebuilds to the specified repo.
417                 portdb.porttrees = [repo_path]
418
419         ret = [os.EX_OK]
420
421         if options.update:
422                 cp_iter = None
423                 if atoms:
424                         cp_iter = iter(atoms)
425
426                 gen_cache = GenCache(portdb, cp_iter=cp_iter,
427                         max_jobs=options.jobs,
428                         max_load=options.load_average,
429                         rsync=options.rsync)
430                 gen_cache.run()
431                 ret.append(gen_cache.returncode)
432
433         if options.update_use_local_desc:
434                 gen_desc = GenUseLocalDesc(portdb)
435                 gen_desc.run()
436                 ret.append(gen_desc.returncode)
437
438         if options.tolerant:
439                 return ret[0]
440         return max(ret)
441
442 if __name__ == "__main__":
443         portage._disable_legacy_globals()
444         portage.util.noiselimit = -1
445         sys.exit(egencache_main(sys.argv[1:]))