#!/usr/bin/python # -*- coding: utf-8 -*- # ################################################################## # # Copyright (C) 2010, Evan Broder # Copyright (C) 2010, Benjamin Drung # # 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; version 2. # # 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. # # See file /usr/share/common-licenses/GPL-2 for more details. # # ################################################################## import optparse import os import shutil import subprocess import sys import tempfile import urllib from debian.deb822 import Dsc import launchpadlib.launchpad import lsb_release from ubuntutools.config import UDTConfig, ubu_email from ubuntutools.builder import get_builder from ubuntutools.logger import Logger from ubuntutools.question import YesNoQuestion from ubuntutools.misc import dsc_url def error(msg): Logger.error(msg) sys.exit(1) def check_call(cmd, *args, **kwargs): Logger.command(cmd) ret = subprocess.call(cmd, *args, **kwargs) if ret != 0: error('%s returned %d.' % (cmd, ret)) def parse(args): usage = 'Usage: %prog [options] ' p = optparse.OptionParser(usage) p.add_option('-d', '--destination', dest='dest_releases', default=[], action='append', help='Backport to DEST release (default: current release)', metavar='DEST') p.add_option('-s', '--source', dest='source_release', default=None, help='Backport from SOURCE release (default: devel release)', metavar='SOURCE') p.add_option('-S', '--suffix', dest='suffix', default=None, help='Suffix to append to version number (default: ~ppa1)', metavar='SUFFIX') p.add_option('-b', '--build', dest='build', default=False, action='store_true', help='Build the package before uploading (default: %default)') p.add_option('-B', '--builder', dest='builder', default=None, help='Specify the package builder (default: pbuilder)', metavar='BUILDER') p.add_option('-U', '--update', dest='update', default=False, action='store_true', help='Update the build environment before attempting to build') p.add_option('-u', '--upload', dest='upload', help='Specify an upload destination', metavar='UPLOAD') p.add_option('-y', '--yes', dest='prompt', default=True, action='store_false', help='Do not prompt before uploading to a PPA') p.add_option('-v', '--version', dest='version', default=None, help='Package version to backport (or verify)', metavar='VERSION') p.add_option('-w', '--workdir', dest='workdir', default=None, help='Specify a working directory (default: temporary dir)', metavar='WORKDIR') p.add_option('-m', '--mirror', dest='ubuntu_mirror', default=None, help='Preferred Ubuntu mirror (default: Launchpad)', metavar='INSTANCE') p.add_option('-l', '--lpinstance', dest='lpinstance', default=None, help='Launchpad instance to connect to (default: production)', metavar='INSTANCE') p.add_option('--no-conf', dest='no_conf', default=False, help="Don't read config files or environment variables", action='store_true') opts, args = p.parse_args(args) if len(args) != 1: p.error('You must specify a single source package or a .dsc URL/path.') config = UDTConfig(opts.no_conf) if opts.builder is None: opts.builder = config.get_value('BUILDER') if not opts.update: opts.update = config.get_value('UPDATE_BUILDER', boolean=True) if opts.workdir is None: opts.workdir = config.get_value('WORKDIR') if opts.lpinstance is None: opts.lpinstance = config.get_value('LPINSTANCE') if opts.ubuntu_mirror is None: opts.ubuntu_mirror = config.get_value('UBUNTU_MIRROR') if not opts.upload and not opts.workdir: p.error('Please specify either a working dir or an upload target!') return opts, args def find_release_package(lp, package, version, source_release): ubuntu = lp.distributions['ubuntu'] archive = ubuntu.main_archive series = ubuntu.getSeries(name_or_version=source_release) status = 'Published' for pocket in ('Updates', 'Security', 'Release'): try: srcpkg = archive.getPublishedSources(source_name=package, distro_series=series, pocket=pocket, status=status, exact_match=True)[0] break except IndexError: continue else: error('Unable to find package %s in release %s.' % (package, source_release)) if version and version != srcpkg.source_package_version: error('Requested backport of version %s but %s is at version %s.' % (version, package, srcpkg.source_package_version)) return srcpkg def find_version_package(lp, package, version): ubuntu = lp.distributions['ubuntu'] archive = ubuntu.main_archive try: # Might get more than one (i.e. same version in multiple # releases), but they should all be identical return archive.getPublishedSources(source_name=package, version=version)[0] except IndexError: error('Version %s of package %s was never published in Ubuntu.' % (version, package)) def dscurls_from_package(lp, mirror, workdir, package, version, source_release): if not source_release and not version: source_release = lp.distributions['ubuntu'].current_series.name # If source_release is specified, then version is just for verification if source_release: srcpkg = find_release_package(lp, package, version, source_release) else: srcpkg = find_version_package(lp, package, version) urls = [] if mirror: urls.append(dsc_url(mirror, srcpkg.component_name, package, srcpkg.source_package_version)) for f in srcpkg.sourceFileUrls(): if f.endswith('.dsc'): urls.append(urllib.unquote(f)) return urls else: error('Package %s contains no .dsc file.' % package) def dscurl_from_dsc(package): path = os.path.abspath(os.path.expanduser(package)) if os.path.exists(path): return 'file://%s' % path else: # Can't resolve it as a local path? Let's just hope it's good as-is return package def fetch_package(lp, mirror, workdir, package, version, source_release): # Returns the path to the .dsc file that was fetched if package.endswith('.dsc'): dscs = [dscurl_from_dsc(package)] else: dscs = dscurls_from_package(lp, mirror, workdir, package, version, source_release) for dsc in dscs: cmd = ('dget', '--download-only', '--allow-unauthenticated', dsc) Logger.command(cmd) ret = subprocess.call(cmd, cwd=workdir) if ret == 0: return os.path.join(workdir, os.path.basename(dsc)) def get_backport_version(version, suffix, upload, release): v = version + ('~%s1' % release) if suffix is not None: v += suffix elif upload and upload.startswith('ppa:'): v += '~ppa1' return v def get_backport_dist(upload, release): if not upload or upload == 'ubuntu': return '%s-backports' % release else: return release def do_build(workdir, package, release, bp_version, builder, update): builder = get_builder(builder) if not builder: return if update: if 0 != builder.update(release): sys.exit(1) return builder.build(os.path.join(workdir, '%s_%s.dsc' % (package, bp_version)), release, os.path.join(workdir, "buildresult")) def do_upload(workdir, package, bp_version, upload, prompt): print 'Please check %s %s in file://%s carefully!' % \ (package, bp_version, workdir) if prompt or upload == 'ubuntu': question = 'Do you want to upload the package to %s' % upload answer = YesNoQuestion().ask(question, "yes") if answer == "no": return changes_file = '%s_%s_source.changes' % (package, bp_version) check_call(['dput', upload, changes_file], cwd=workdir) def do_backport(workdir, package, dscfile, version, suffix, release, build, builder, update, upload, prompt): check_call(['dpkg-source', '-x', dscfile, package], cwd=workdir) srcdir = os.path.join(workdir, package) bp_version = get_backport_version(version, suffix, upload, release) bp_dist = get_backport_dist(upload, release) check_call(['dch', '--force-bad-version', '--preserve', '--newversion', bp_version, '--distribution', bp_dist, 'No-change backport to %s' % release], cwd=srcdir) check_call(['debuild', '--no-lintian', '-S', '-sa'], cwd=srcdir) if ':' in bp_version: bp_version = bp_version[bp_version.find(':')+1:] if build: if 0 != do_build(workdir, package, release, bp_version, builder, update): sys.exit(1) if upload: do_upload(workdir, package, bp_version, upload, prompt) shutil.rmtree(srcdir) def main(args): os.environ['DEB_VENDOR'] = 'Ubuntu' ubu_email() opts, (package_or_dsc,) = parse(args[1:]) script_name = os.path.basename(sys.argv[0]) lp = launchpadlib.launchpad.Launchpad.login_anonymously(script_name, opts.lpinstance) if not opts.dest_releases: try: distinfo = lsb_release.get_distro_information() opts.dest_releases = [distinfo['CODENAME']] except: error('No destination release specified and unable to guess yours.') if opts.workdir: workdir = os.path.expanduser(opts.workdir) else: workdir = tempfile.mkdtemp(prefix='backportpackage-') if not os.path.exists(workdir): os.makedirs(workdir) try: dscfile = fetch_package(lp, opts.ubuntu_mirror, workdir, package_or_dsc, opts.version, opts.source_release) dsc = Dsc(open(os.path.join(workdir, dscfile))) package = dsc['Source'] version = dsc['Version'] for release in opts.dest_releases: do_backport(workdir, package, dscfile, version, opts.suffix, release, opts.build, opts.builder, opts.update, opts.upload, opts.prompt) finally: if not opts.workdir: shutil.rmtree(workdir) if __name__ == '__main__': sys.exit(main(sys.argv))