source: lliurex-meta/trunk/fuentes/debian/germinate_update_metapackage.py @ 685

Last change on this file since 685 was 432, checked in by kbut, 5 years ago

first version

File size: 17.7 KB
Line 
1# -*- coding: UTF-8 -*-
2
3# Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2011, 2012 Canonical Ltd.
4# Copyright (C) 2006 Gustavo Franco
5#
6# This file is part of Germinate.
7#
8# Germinate is free software; you can redistribute it and/or modify it
9# under the terms of the GNU General Public License as published by the
10# Free Software Foundation; either version 2, or (at your option) any
11# later version.
12#
13# Germinate is distributed in the hope that it will be useful, but
14# WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16# General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with Germinate; see the file COPYING.  If not, write to the Free
20# Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
21# 02110-1301, USA.
22
23# TODO:
24# - Exclude essential packages from dependencies
25
26from __future__ import print_function
27
28import sys
29import re
30import os
31import optparse
32import logging
33try:
34    # >= 3.0
35    from configparser import NoOptionError, NoSectionError
36    if (sys.version_info[0] < 3 or
37        (sys.version_info[0] == 3 and sys.version_info[1] < 2)):
38        # < 3.2
39        from configparser import SafeConfigParser
40    else:
41        # >= 3.2
42        from configparser import ConfigParser as SafeConfigParser
43except ImportError:
44    # < 3.0
45    from ConfigParser import NoOptionError, NoSectionError, SafeConfigParser
46import subprocess
47from collections import defaultdict
48
49from germinate.germinator import Germinator
50import germinate.archive
51from germinate.log import germinate_logging
52from germinate.seeds import SeedError, SeedStructure
53import germinate.version
54
55__pychecker__ = 'maxlocals=80'
56
57
58def error_exit(message):
59    print("%s: %s" % (sys.argv[0], message), file=sys.stderr)
60    sys.exit(1)
61
62
63def parse_options(argv):
64    description = '''\
65Update metapackage lists for distribution 'dist' as defined in
66update.cfg.'''
67
68    parser = optparse.OptionParser(
69        prog='germinate-update-metapackage',
70        usage='%prog [options] [dist]',
71        version='%prog ' + germinate.version.VERSION,
72        description=description)
73    parser.add_option('-o', '--output-directory', dest='outdir',
74                      default='.', metavar='DIR',
75                      help='output in specific directory')
76    parser.add_option('--nodch', dest='nodch', action='store_true',
77                      default=False,
78                      help="don't modify debian/changelog")
79    parser.add_option('--bzr', dest='bzr', action='store_true', default=False,
80                      help='fetch seeds using bzr (requires bzr to be '
81                           'installed)')
82    parser.add_option('-r','--recursive', dest='recursive', action='store_true',
83                      default=False,
84                      help="expand all seeds dependencies from STRUCTURE")
85    return parser.parse_args(argv[1:])
86
87
88def main(argv):
89    options, args = parse_options(argv)
90
91    if not os.path.exists('debian/control'):
92        error_exit('must be run from the top level of a source package')
93    this_source = None
94    with open('debian/control') as control:
95        for line in control:
96            if line.startswith('Source:'):
97                this_source = line[7:].strip()
98                break
99            elif line == '':
100                break
101    if this_source is None:
102        error_exit('cannot find Source: in debian/control')
103    if not this_source.endswith('-meta'):
104        error_exit('source package name must be *-meta')
105    metapackage = this_source[:-5]
106
107    print("[info] Initialising %s-* package lists update..." % metapackage)
108
109    config = SafeConfigParser()
110    with open('update.cfg') as config_file:
111        try:
112            # >= 3.2
113            config.read_file(config_file)
114        except AttributeError:
115            # < 3.2
116            config.readfp(config_file)
117
118    if len(args) > 0:
119        dist = args[0]
120    else:
121        dist = config.get('DEFAULT', 'dist')
122
123    seeds = config.get(dist, 'seeds').split()
124    try:
125        output_seeds = config.get(dist, 'output_seeds').split()
126    except NoOptionError:
127        output_seeds = list(seeds)
128    architectures = config.get(dist, 'architectures').split()
129    try:
130        archive_base_default = config.get(dist, 'archive_base/default')
131        archive_base_default = re.split(r'[, ]+', archive_base_default)
132    except (NoSectionError, NoOptionError):
133        archive_base_default = None
134
135    archive_base = {}
136    for arch in architectures:
137        try:
138            archive_base[arch] = config.get(dist, 'archive_base/%s' % arch)
139            archive_base[arch] = re.split(r'[, ]+', archive_base[arch])
140        except (NoSectionError, NoOptionError):
141            if archive_base_default is not None:
142                archive_base[arch] = archive_base_default
143            else:
144                error_exit('no archive_base configured for %s' % arch)
145
146    if options.bzr and config.has_option("%s/bzr" % dist, 'seed_base'):
147        seed_base = config.get("%s/bzr" % dist, 'seed_base')
148    else:
149        seed_base = config.get(dist, 'seed_base')
150    seed_base = re.split(r'[, ]+', seed_base)
151    if options.bzr and config.has_option("%s/bzr" % dist, 'seed_dist'):
152        seed_dist = config.get("%s/bzr" % dist, 'seed_dist')
153    elif config.has_option(dist, 'seed_dist'):
154        seed_dist = config.get(dist, 'seed_dist')
155    else:
156        seed_dist = dist
157    if config.has_option(dist, 'dists'):
158        dists = config.get(dist, 'dists').split()
159    else:
160        dists = [dist]
161    components = config.get(dist, 'components').split()
162
163    def seed_packages(germinator_method, structure, seed_name):
164        if config.has_option(dist, "seed_map/%s" % seed_name):
165            mapped_seeds = config.get(dist, "seed_map/%s" % seed_name).split()
166        else:
167            mapped_seeds = []
168            task_seeds_re = re.compile('^Task-Seeds:\s*(.*)', re.I)
169            with structure[seed_name] as seed:
170                for line in seed:
171                    task_seeds_match = task_seeds_re.match(line)
172                    if task_seeds_match is not None:
173                        mapped_seeds = task_seeds_match.group(1).split()
174                        break
175            if seed_name not in mapped_seeds:
176                mapped_seeds.append(seed_name)
177        packages = []
178        if options.recursive:
179            mapped_seeds.extend(structure.inner_seeds(seed_name))
180            mapped_seeds = list(set(mapped_seeds))
181       
182        for mapped_seed in mapped_seeds:
183            packages.extend(germinator_method(structure, mapped_seed))
184        return packages
185
186    def metapackage_name(structure, seed_name):
187        if config.has_option(dist, "metapackage_map/%s" % seed_name):
188            return config.get(dist, "metapackage_map/%s" % seed_name)
189        else:
190            task_meta_re = re.compile('^Task-Metapackage:\s*(.*)', re.I)
191            with structure[seed_name] as seed:
192                for line in seed:
193                    task_meta_match = task_meta_re.match(line)
194                    if task_meta_match is not None:
195                        return task_meta_match.group(1)
196            return "%s-%s" % (metapackage, seed_name)
197
198    debootstrap_version_file = 'debootstrap-version'
199
200    def get_debootstrap_version():
201        version_cmd = subprocess.Popen(
202            ['dpkg-query', '-W', '--showformat', '${Version}', 'debootstrap'],
203            stdout=subprocess.PIPE, universal_newlines=True)
204        version, _ = version_cmd.communicate()
205        if not version:
206            error_exit('debootstrap does not appear to be installed')
207
208        return version
209
210    def debootstrap_packages(arch):
211        env = dict(os.environ)
212        if 'PATH' in env:
213            env['PATH'] = '/usr/sbin:/sbin:%s' % env['PATH']
214        else:
215            env['PATH'] = '/usr/sbin:/sbin:/usr/bin:/bin'
216        debootstrap = subprocess.Popen(
217            ['debootstrap', '--arch', arch,
218             '--components', ','.join(components),
219             '--print-debs', dist, 'debootstrap-dir', archive_base[arch][0]],
220            stdout=subprocess.PIPE, env=env, stderr=subprocess.PIPE,
221            universal_newlines=True)
222        (debootstrap_stdout, debootstrap_stderr) = debootstrap.communicate()
223        if debootstrap.returncode != 0:
224            error_exit('Unable to retrieve package list from debootstrap; '
225                       'stdout: %s\nstderr: %s' %
226                       (debootstrap_stdout, debootstrap_stderr))
227
228        # sometimes debootstrap gives empty packages / multiple separators
229        packages = [pkg for pkg in debootstrap_stdout.split() if pkg]
230
231        return sorted(packages)
232
233    def check_debootstrap_version():
234        if os.path.exists(debootstrap_version_file):
235            with open(debootstrap_version_file) as debootstrap:
236                old_debootstrap_version = debootstrap.read().strip()
237            debootstrap_version = get_debootstrap_version()
238            failed = subprocess.call(
239                ['dpkg', '--compare-versions',
240                 debootstrap_version, 'ge', old_debootstrap_version])
241            if failed:
242                error_exit('Installed debootstrap is older than in the '
243                           'previous version! (%s < %s)' %
244                           (debootstrap_version, old_debootstrap_version))
245
246    def update_debootstrap_version():
247        with open(debootstrap_version_file, 'w') as debootstrap:
248            debootstrap.write(get_debootstrap_version() + '\n')
249
250    def format_changes(items):
251        by_arch = defaultdict(set)
252        for pkg, arch in items:
253            by_arch[pkg].add(arch)
254        all_pkgs = sorted(by_arch)
255        chunks = []
256        for pkg in all_pkgs:
257            arches = by_arch[pkg]
258            if set(architectures) - arches:
259                # only some architectures
260                chunks.append('%s [%s]' % (pkg, ' '.join(sorted(arches))))
261            else:
262                # all architectures
263                chunks.append(pkg)
264        return ', '.join(chunks)
265
266    germinate_logging(logging.DEBUG)
267
268    check_debootstrap_version()
269
270    additions = defaultdict(list)
271    removals = defaultdict(list)
272    moves = defaultdict(list)
273    metapackage_map = {}
274    for architecture in architectures:
275        print("[%s] Downloading available package lists..." % architecture)
276        germinator = Germinator(architecture)
277        archive = germinate.archive.TagFile(
278            dists, components, architecture,
279            archive_base[architecture], source_mirrors=archive_base_default,
280            cleanup=True)
281        germinator.parse_archive(archive)
282        debootstrap_base = set(debootstrap_packages(architecture))
283
284        print("[%s] Loading seed lists..." % architecture)
285        try:
286            structure = SeedStructure(seed_dist, seed_base, options.bzr)
287            germinator.plant_seeds(structure, seeds=seeds)
288        except SeedError:
289            sys.exit(1)
290
291        print("[%s] Merging seeds with available package lists..." %
292              architecture)
293        for seed_name in output_seeds:
294            meta_name = metapackage_name(structure, seed_name)
295            metapackage_map[seed_name] = meta_name
296
297            output_filename = os.path.join(
298                options.outdir, '%s-%s' % (seed_name, architecture))
299            old_list = None
300            if os.path.exists(output_filename):
301                with open(output_filename) as output:
302                    old_list = set(map(str.strip, output.readlines()))
303                os.rename(output_filename, output_filename + '.old')
304
305            # work on the depends
306            new_list = []
307            packages = seed_packages(germinator.get_seed_entries,
308                                     structure, seed_name)
309            for package in packages:
310                if package == meta_name:
311                    print("%s/%s: Skipping package %s (metapackage)" %
312                          (seed_name, architecture, package))
313                elif (seed_name == 'minimal' and
314                      package not in debootstrap_base):
315                    print("%s/%s: Skipping package %s (package not in "
316                          "debootstrap)" % (seed_name, architecture, package))
317                elif germinator.is_essential(package):
318                    print("%s/%s: Skipping package %s (essential)" %
319                          (seed_name, architecture, package))
320                else:
321                    new_list.append(package)
322
323            new_list.sort()
324            with open(output_filename, 'w') as output:
325                for package in new_list:
326                    output.write(package)
327                    output.write('\n')
328
329            # work on the recommends
330            old_recommends_list = None
331            new_recommends_list = []
332            packages = seed_packages(germinator.get_seed_recommends_entries,
333                                     structure, seed_name)
334            for package in packages:
335                if package == meta_name:
336                    print("%s/%s: Skipping package %s (metapackage)" %
337                          (seed_name, architecture, package))
338                    continue
339                if seed_name == 'minimal' and package not in debootstrap_base:
340                    print("%s/%s: Skipping package %s (package not in "
341                          "debootstrap)" % (seed_name, architecture, package))
342                else:
343                    new_recommends_list.append(package)
344
345            new_recommends_list.sort()
346            seed_name_recommends = '%s-recommends' % seed_name
347            output_recommends_filename = os.path.join(
348                options.outdir, '%s-%s' % (seed_name_recommends, architecture))
349            if os.path.exists(output_recommends_filename):
350                with open(output_recommends_filename) as output:
351                    old_recommends_list = set(
352                        map(str.strip, output.readlines()))
353                os.rename(
354                    output_recommends_filename,
355                    output_recommends_filename + '.old')
356
357            with open(output_recommends_filename, 'w') as output:
358                for package in new_recommends_list:
359                    output.write(package)
360                    output.write('\n')
361
362            # Calculate deltas
363            merged = defaultdict(int)
364            recommends_merged = defaultdict(int)
365            if old_list is not None:
366                for package in new_list:
367                    merged[package] += 1
368                for package in old_list:
369                    merged[package] -= 1
370            if old_recommends_list is not None:
371                for package in new_recommends_list:
372                    recommends_merged[package] += 1
373                for package in old_recommends_list:
374                    recommends_merged[package] -= 1
375
376            mergeditems = sorted(merged.items())
377            for package, value in mergeditems:
378                #print(package, value)
379                if value == 1:
380                    if recommends_merged.get(package, 0) == -1:
381                        moves[package].append([seed_name, architecture])
382                        recommends_merged[package] += 1
383                    else:
384                        additions[package].append([seed_name, architecture])
385                elif value == -1:
386                    if recommends_merged.get(package, 0) == 1:
387                        moves[package].append([seed_name_recommends,
388                                               architecture])
389                        recommends_merged[package] -= 1
390                    else:
391                        removals[package].append([seed_name, architecture])
392
393            mergedrecitems = sorted(recommends_merged.items())
394            for package, value in mergedrecitems:
395                #print(package, value)
396                if value == 1:
397                    additions[package].append([seed_name_recommends,
398                                               architecture])
399                elif value == -1:
400                    removals[package].append([seed_name_recommends,
401                                              architecture])
402
403    with open('metapackage-map', 'w') as metapackage_map_file:
404        for seed_name in output_seeds:
405            print(seed_name, metapackage_map[seed_name],
406                  file=metapackage_map_file)
407
408    if not options.nodch and (additions or removals or moves):
409        dch_help = subprocess.Popen(['dch', '--help'], stdout=subprocess.PIPE,
410                                    universal_newlines=True)
411        try:
412            have_U = '-U' in dch_help.stdout.read()
413        finally:
414            if dch_help.stdout:
415                dch_help.stdout.close()
416            dch_help.wait()
417        if have_U:
418            subprocess.check_call(['dch', '-iU', 'Refreshed dependencies'])
419        else:
420            subprocess.check_call(['dch', '-i', 'Refreshed dependencies'])
421        changes = []
422        for package in sorted(additions):
423            changes.append('Added %s to %s' %
424                           (package, format_changes(additions[package])))
425        for package in sorted(removals):
426            changes.append('Removed %s from %s' %
427                           (package, format_changes(removals[package])))
428        for package in sorted(moves):
429            # TODO: We should really list where it moved from as well, but
430            # that gets wordy very quickly, and at the moment this is only
431            # implemented for depends->recommends or vice versa. In future,
432            # using this for moves between seeds might also be useful.
433            changes.append('Moved %s to %s' %
434                           (package, format_changes(moves[package])))
435        for change in changes:
436            print(change)
437            subprocess.check_call(['dch', '-a', change])
438        update_debootstrap_version()
439    else:
440        if not options.nodch:
441            print("No changes found")
442
443    return 0
Note: See TracBrowser for help on using the repository browser.