#!/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 shutil import subprocess import tempfile __version__ = '0.0' class Unperish(cliapp.Application): metafilename = 'project.meta' 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(['basetgz'], 'pbuilder basetgz (%default)', default='/var/cache/pbuilder/base.tgz') self.settings.string(['upload-target'], 'generate debian/changelog entry for uploading ' 'to given target') 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(['arch'], 'architecture for Debian packages') 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') def process_args(self, args): self.meta = self.create_meta() self.autofill_meta() self.read_meta() self.create_build_area() self.already = set() for arg in args: self.run_subcommand(arg) def run_subcommand(self, subcommand): if subcommand not in self.already: self.already.add(subcommand) try: with open(self.join(self.dirname, 'debian', 'control')) as f: self.debian_control = debian.deb822.Deb822(f) except IOError: pass try: with open(self.join(self.dirname, 'debian', 'changelog')) as f: self.debian_changelog = debian.changelog.Changelog(f) except IOError: pass 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 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.meta.get('project', 'name') @property def upstream_version(self): return self.meta.get('project', 'version') @property def upstream_tarball(self): return '%s-%s.tar.gz' % (self.upstream_name, self.upstream_version) @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 self.debian_changelog.get_version() @property def debian_tarball(self): return '%s_%s.orig.tar.gz' % (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): if self.settings['arch']: return self.settings['arch'] else: return self.runcmd(['dpkg', '--print-architecture']).strip() @property def changes(self): return '%s_%s_%s.changes' % (self.debian_source_package, self.debian_version, self.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_dump_meta(self, args): '''Print out contents of project meta file (project.meta).''' self.meta.write(self.output) 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_dch(self, args): '''Add a debian/changelog entry for new upload target.''' self.run_subcommand('export') if self.settings['upload-target']: self.add_debian_changelog_entry() 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('dsc') if not self.already_exists(self.join(self.changes)): argv = ['sudo', 'pbuilder', '--build', '--basetgz', self.settings['basetgz'], '--buildresult', self.settings['build-area'], '--logfile', self.join('pbuilder.log'), '--debbuildopts', '-sa', self.join(self.dsc)] self.runcmd(argv, cwd=self.settings['build-area']) 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') for manpage in glob.glob('*.[1-8]'): fmt = self.runcmd(['man', '-l', manpage]) 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.settings['upload-target'] self.runcmd(['dch', '--force-distribution', '--local', target, '--distribution', target, 'Build for %s.' % target], cwd=self.join(self.dirname)) def create_meta(self): cp = ConfigParser.RawConfigParser() cp.add_section('project') cp.set('project', 'name', '') cp.set('project', 'version', '') return cp def autofill_meta(self): if os.path.exists('setup.py'): name = self.runcmd(['python', 'setup.py', '--name']).strip() if name: self.meta.set('project', 'name', name) version = self.runcmd(['python', 'setup.py', '--version']).strip() if version: self.meta.set('project', 'version', version) def read_meta(self): self.meta.read([self.metafilename]) 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()