#!/usr/bin/python # Copyright 2011 Lars Wirzenius # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import cliapp import ConfigParser import debian.changelog import debian.deb822 import glob import logging import os import re import shutil import subprocess import tempfile __version__ = '0.0' class Unperish(cliapp.Application): def add_settings(self): self.settings.boolean(['verbose', 'v'], 'print commands that are executed') self.settings.boolean(['no-act', 'dry-run', 'n'], 'don\'t run commands') self.settings.string(['build-area'], 'where should results go? (%default)', default='../build-area') self.settings.string_list(['basetgz'], 'list of pbuilder basetgz tarballs to use, ' 'optionally prefix with upload target and ' 'architechture, separated by colons ' '(%default)', default=['/var/cache/pbuilder/base.tgz']) self.settings.string(['dsc'], 'Debian source package (for dget command)', metavar='URL') self.settings.string(['debian-version'], 'Debian version number (typically detected ' 'from debian/changelog)') self.settings.string(['debian-source'], 'Debian source package name (typically detected ' 'from debian/control)') self.settings.string(['web-directory'], 'put files to go on the web in DIR', metavar='DIR') self.settings.string_list(['rsync-glob'], 'publish files matching GLOB with rsync', metavar='GLOB') self.settings.string_list(['rsync-to'], 'publish files with rsync to LOCATION', metavar='LOCATION') self.settings.choice(['full-source'], ['auto', 'yes', 'no'], 'include full source in upload?') self.settings.boolean(['binary-arch'], 'build arch-specific packages only, ' 'not arch:all') self.settings.string(['upstream-name'], 'upstream name for project') self.settings.string(['upstream-version'], 'upstream version for project') self.settings.config_files += ['project.meta'] def process_args(self, args): self.deduce_unset_settings() self.create_build_area() self.already = set() for arg in args: self.run_subcommand(arg) def run_subcommand(self, subcommand, force=False): if force or subcommand not in self.already: self.already.add(subcommand) if subcommand in self.subcommands: method = self.subcommands[subcommand] if self.settings['verbose']: self.output.write('command: %s\n' % subcommand) if not self.settings['no-act']: method([]) else: raise cliapp.AppException('unknown command %s' % subcommand) def deduce_unset_settings(self): def deduce_upstream_name(): if os.path.exists('setup.py'): return self.runcmd(['python', 'setup.py', '--name']).strip() return '' def deduce_upstream_version(): if os.path.exists('setup.py'): return self.runcmd(['python', 'setup.py', '--version']).strip() return '' table = { 'upstream-name': deduce_upstream_name, 'upstream-version': deduce_upstream_version, } for name in table: if not self.settings[name]: self.settings[name] = table[name]() def create_build_area(self): if not os.path.exists(self.settings['build-area']): os.mkdir(self.settings['build-area']) @property def upstream_name(self): return self.settings['upstream-name'] @property def upstream_version(self): return self.settings['upstream-version'] @property def upstream_tarball(self): return '%s-%s.tar.gz' % (self.upstream_name, self.upstream_version) @property def debian_control(self): with open(self.join(self.dirname, 'debian', 'control')) as f: return debian.deb822.Deb822(f) @property def debian_changelog(self): with open(self.join(self.dirname, 'debian', 'changelog')) as f: return debian.changelog.Changelog(f) @property def debian_source_package(self): if self.settings['debian-source']: return self.settings['debian-source'] return self.debian_control['Source'] @property def debian_version(self): if self.settings['debian-version']: return self.settings['debian-version'] return str(self.debian_changelog.get_version()) @property def debian_tarball(self): is_native = '-' not in self.debian_version if is_native: pattern = '%s_%s.tar.gz' else: pattern = '%s_%s.orig.tar.gz' return pattern % (self.debian_source_package, self.upstream_version) @property def dirname(self): return '%s-%s' % (self.upstream_name, self.upstream_version) @property def dsc(self): return '%s_%s.dsc' % (self.debian_source_package, self.debian_version) @property def arch(self): return self.runcmd(['dpkg', '--print-architecture']).strip() def changes(self, arch): return '%s_%s_%s.changes' % (self.debian_source_package, self.debian_version, arch) def already_exists(self, filename): '''Does a file already exist?''' if os.path.exists(filename): logging.debug('Already exists: %s' % filename) return True else: logging.debug('Does not already exist: %s' % filename) return False def cmd_dget(self, args): '''Retrieve a Debian source package (.dsc and other files). Put the files in the build area. ''' if not self.settings['dsc']: raise cliapp.AppException('Need --dsc option for dget') basename = os.path.basename(self.settings['dsc']) if not self.already_exists(self.join(basename)): self.runcmd(['dget', '--download-only', self.settings['dsc']], cwd=self.settings['build-area']) def cmd_export(self, args): '''Export unpacked source directory to build area.''' if not self.already_exists(self.join(self.dirname)): self.runcmd(['bzr', 'export', self.join(self.dirname)]) def cmd_debian_tarball(self, args): '''Generate Debian tarball (.orig.tar.gz) in build area.''' origtar = self.join(self.debian_tarball) if not self.already_exists(origtar): self.runcmd(['bzr', 'export', origtar]) def cmd_dsc(self, args): '''Create Debian source package (.dsc) in build area.''' self.run_subcommand('export') self.run_subcommand('debian-tarball') if not self.already_exists(self.join(self.dsc)): self.runcmd(['dpkg-source', '-b', self.dirname], cwd=self.settings['build-area']) def cmd_deb(self, args): '''Build Debian binary packages (.deb) in build area.''' self.run_subcommand('export') for spec in self.settings['basetgz']: target, arch, path = self.parse_basetgz(spec) if target: self.add_debian_changelog_entry(target) self.run_subcommand('dsc', force=True) if not self.already_exists(self.join(self.changes(arch))): argv = ['sudo', 'pbuilder', '--build', '--basetgz', path, '--buildresult', self.settings['build-area'], '--logfile', self.join('pbuilder.log')] if self.include_source(): argv.extend(['--debbuildopts', '-sa']) if self.settings['binary-arch']: argv.append('--binary-arch') argv.append(self.join(self.dsc)) self.runcmd(argv, cwd=self.settings['build-area']) def include_source(self): '''Should the upload include full source?''' if self.settings['full-source'] == 'yes': return True if self.settings['full-source'] == 'no': return False pat = r'-1$|[a-z]1$|^[^-]*$' return re.search(pat, self.debian_version) is not None def parse_basetgz(self, spec): parts = spec.split(':', 3) n = len(parts) if n == 1 or '/' in parts[0]: target = None arch = self.arch path = parts[0] elif n == 2: target = parts[0] arch = self.arch path = parts[1] elif n == 3 and '/' in parts[1]: target = parts[0] arch = self.arch path = ':'.join(parts[1:]) else: target = parts[0] arch = parts[1] path = parts[2] return target, arch, path def cmd_lintian(self, args): '''Run lintian on .changes/.deb/.dsc files.''' def find_them(suffixes): return [os.path.join(self.settings['build-area'], x) for x in os.listdir(self.settings['build-area']) if os.path.splitext(x)[1] in suffixes] files = find_them(['.changes']) if not files: files = find_them(['.deb', '.dsc']) out = self.runcmd(['lintian', '-i'] + files, ignore_fail=True, cwd=self.settings['build-area']) self.output.write(out) def cmd_publish_docs(self, args): '''Publish docs related to this project.''' def publish(source, target_base): target = os.path.join(self.settings['web-directory'], target_base) if self.settings['verbose']: print 'Copying %s to %s' % (source, target) shutil.copyfile(source, target) if not self.settings['web-directory']: raise cliapp.AppException('Need --web-directory ' 'for publish-docs.') docs = ['README', 'NEWS'] for doc in docs: if os.path.exists(doc): publish(doc, doc + '.mdwn') env = dict(os.environ) env['LC_ALL'] = 'C' for manpage in glob.glob('*.[1-8]'): fmt = self.runcmd(['man', '-l', manpage], env=env) text = self.runcmd(['col', '-b'], stdin=fmt) fd, name = tempfile.mkstemp() os.write(fd, text) os.close(fd) publish(name, manpage + '.txt') os.remove(name) def cmd_rsync_publish(self, args): '''Publish files via rsync.''' filenames = [] for pattern in self.settings['rsync-glob']: filenames += glob.glob(pattern) logging.debug('filenames: %s' % filenames) self.runcmd(['rsync', '-av', '--delete-after'] + filenames + self.settings['rsync-to']) def cmd_clean(self, args): '''Clean up the build-area (remove everything except the dir).''' area = self.settings['build-area'] if os.path.isdir(area): for x in os.listdir(area): pathname = os.path.join(area, x) if os.path.isdir(pathname): shutil.rmtree(pathname) else: os.remove(pathname) def add_debian_changelog_entry(self, target): self.runcmd(['dch', '--force-distribution', '--local', target, '--distribution', target, '--preserve', 'Build for %s.' % target], cwd=self.join(self.dirname)) def join(self, *components): components = (self.settings['build-area'],) + components return os.path.join(*components) def runcmd(self, argv, *args, **kwargs): logging.debug('runcmd: argv: %s' % repr(argv)) logging.debug('runcmd: args: %s' % repr(args)) logging.debug('runcmd: kwargs: %s' % repr(kwargs)) if self.settings['verbose']: self.output.write('run: %s\n' % ' '.join(argv)) return cliapp.Application.runcmd(self, argv, *args, **kwargs) if __name__ == '__main__': Unperish(version=__version__).run()