diff --git a/backportpackage b/backportpackage index 8a1411c..4120280 100755 --- a/backportpackage +++ b/backportpackage @@ -21,16 +21,15 @@ import shutil import subprocess import sys import tempfile +import urllib from debian.deb822 import Dsc import launchpadlib.launchpad +import lsb_release from ubuntutools.builder import getBuilder from ubuntutools.logger import Logger -devnull = open('/dev/null', 'r+') -lp = None - def error(msg): Logger.error(msg) sys.exit(1) @@ -42,15 +41,15 @@ def check_call(cmd, *args, **kwargs): error('%s returned %d' % (cmd, ret)) def parse(args): - usage = 'Usage: %prog [options] ' + usage = 'Usage: %prog [options] ' p = optparse.OptionParser(usage) - p.add_option('-t', '--to', + p.add_option('-d', '--destination', dest='dest_releases', default=[], action='append', - help='Backport to DEST release (required)', + help='Backport to DEST release (default: current release)', metavar='DEST') - p.add_option('-f', '--from', + p.add_option('-s', '--source', dest='source_release', default=None, help='Backport from SOURCE release (default: devel release)', @@ -79,6 +78,11 @@ def parse(args): 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('-l', '--launchpad', dest='launchpad', default='production', @@ -87,16 +91,16 @@ def parse(args): opts, args = p.parse_args(args) if len(args) != 1: - p.error('You must specify a source package') - if not opts.dest_releases: - p.error('You must specify at least one destination release') + p.error('You must specify a single source package or a .dsc URL/path') + if not opts.upload and not opts.build: + p.error('Nothing to do') return opts, args -def find_release_package(package, opts): +def find_release_package(lp, package, version, source_release): ubuntu = lp.distributions['ubuntu'] archive = ubuntu.main_archive - series = ubuntu.getSeries(name_or_version=opts.source_release) + series = ubuntu.getSeries(name_or_version=source_release) status = 'Published' for pocket in ('Updates', 'Security', 'Release'): try: @@ -110,50 +114,67 @@ def find_release_package(package, opts): continue else: error('Unable to find package %s in release %s' % - (package, opts.source_release)) + (package, source_release)) - if opts.version and opts.version != srcpkg.source_package_version: + if version and version != srcpkg.source_package_version: error('Requested backport of version %s but %s is at version %s' % - (opts.version, package, srcpkg.source_package_version)) + (version, package, srcpkg.source_package_version)) return srcpkg -def find_version_package(package, opts): +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=opts.version)[0] + version=version)[0] except IndexError: - error('Package %s was never published with version %s in Ubuntu' % - (package, opts.version)) + error('Version %s of package %s was never published in Ubuntu' % + (version, package)) -def fetch_package(workdir, package, opts): - # Returns the path to the .dsc file that was fetched - - if not opts.source_release and not opts.version: - opts.source_release = lp.distributions['ubuntu'].current_series.name +def dscurl_from_package(lp, 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 opts.source_release: - srcpkg = find_release_package(package, opts) + if source_release: + srcpkg = find_release_package(lp, package, version, source_release) else: - srcpkg = find_version_package(package, opts) + srcpkg = find_version_package(lp, package, version) for f in srcpkg.sourceFileUrls(): if f.endswith('.dsc'): - check_call(['dget', - '--download-only', - '--allow-unauthenticated', - f], - cwd=workdir) - return os.path.join(workdir, os.path.basename(f)) + return urllib.unquote(f) 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, workdir, package, version, source_release): + # Returns the path to the .dsc file that was fetched + + if package.endswith('.dsc'): + dsc = dscurl_from_dsc(package) + else: + dsc = dscurl_from_package(lp, workdir, package, version, source_release) + + check_call(['dget', + '--download-only', + '--allow-unauthenticated', + dsc], + cwd=workdir) + return os.path.join(workdir, os.path.basename(dsc)) + def get_backport_version(version, upload, release): v = version + ('~%s1' % release) if upload and upload.startswith('ppa:'): @@ -166,21 +187,21 @@ def get_backport_dist(upload, release): else: return release -def do_build(workdir, package, release, bp_version, opts): - builder = getBuilder(opts.builder) +def do_build(workdir, package, release, bp_version, builder): + builder = getBuilder(builder) if not builder: return if opts.update: builder.update(release) - builder.build(os.path.join(workdir, - '%s_%s.dsc' % (package, bp_version)), - release, - workdir) + return builder.build(os.path.join(workdir, + '%s_%s.dsc' % (package, bp_version)), + release, + workdir) -def do_upload(workdir, package, bp_version, opts): - prompt = 'Do you want to upload this to %s? [Y/n]' % opts.upload +def do_upload(workdir, package, bp_version, upload): + prompt = 'Do you want to upload this to %s? [Y/n]' % upload while True: answer = raw_input(prompt).strip().lower() if answer in ('', 'y', 'yes'): @@ -189,16 +210,13 @@ def do_upload(workdir, package, bp_version, opts): return check_call(['dput', - opts.upload, + upload, '%s_%s_source.changes' % (package, bp_version)], cwd=workdir) -def do_backport(workdir, package, dscfile, release, opts): - dsc = Dsc(open(os.path.join(workdir, dscfile))) - v = dsc['Version'] - +def do_backport(workdir, package, dscfile, version, release, build, builder, upload): check_call(['dpkg-source', '-x', dscfile, @@ -206,8 +224,8 @@ def do_backport(workdir, package, dscfile, release, opts): cwd=workdir) srcdir = os.path.join(workdir, package) - bp_version = get_backport_version(v, opts.upload, release) - bp_dist = get_backport_dist(opts.upload, release) + bp_version = get_backport_version(version, upload, release) + bp_dist = get_backport_dist(upload, release) check_call(['dch', '--force-bad-version', @@ -223,32 +241,61 @@ def do_backport(workdir, package, dscfile, release, opts): bp_version = bp_version[bp_version.find(':')+1:] print 'Please check the package in file://%s carefully' % workdir - if opts.build: - do_build(workdir, package, release, bp_version, opts) - if opts.upload: - do_upload(workdir, package, bp_version, opts) + if build: + if 0 != do_build(workdir, package, release, bp_version, builder): + error('Package failed to build; aborting') + if upload: + do_upload(workdir, package, bp_version, upload) shutil.rmtree(srcdir) def main(args): - global lp - os.environ['DEB_VENDOR'] = 'Ubuntu' - opts, (package,) = parse(args[1:]) + opts, (package_or_dsc,) = parse(args[1:]) script_name = os.path.basename(sys.argv[0]) lp = launchpadlib.launchpad.Launchpad.login_anonymously(script_name, opts.launchpad) - workdir = tempfile.mkdtemp(prefix='backportpackage-') + 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(workdir, package, opts) + dscfile = fetch_package(lp, + 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, release, opts) + do_backport(workdir, + package, + dscfile, + version, + release, + opts.build, + opts.builder, + opts.upload) finally: - shutil.rmtree(workdir) + if not opts.workdir: + shutil.rmtree(workdir) if __name__ == '__main__': sys.exit(main(sys.argv)) diff --git a/doc/backportpackage.1 b/doc/backportpackage.1 index 8fce4c7..7d8edbd 100644 --- a/doc/backportpackage.1 +++ b/doc/backportpackage.1 @@ -4,21 +4,19 @@ backportpackage \- helper to test package backports .SH SYNOPSIS .TP .B backportpackage \fR[\fIadditional options\fR] -\-\-to <\fIdest release\fR> -.br \-\-upload <\fIupload target\fR> .br -<\fIsource package\fR> +<\fIsource package name or .dsc URL/file\fR> .PP .B backportpackage \-h .SH OPTIONS .TP -.B \-t \fIDEST\fR, \-\-to=\fIDEST\fR +.B \-d \fIDEST\fR, \-\-destination=\fIDEST\fR \fBRequired\fR. Backport the package to the specified Ubuntu -release. This option may be specified multiple times, but must be -specified at least once. +release. If this option is unspecified, then \fBbackportpackage\fR +defaults to the release on which it is currently running. .TP -.B \-f \fISOURCE\fR, \-\-from=\fISOURCE\fR +.B \-s \fISOURCE\fR, \-\-source=\fISOURCE\fR Backport the package from the specified Ubuntu release. If neither this option nor \fB\-\-version\fR are specified, then \fBbackportpackage\fR defaults to the current Ubuntu development @@ -42,24 +40,32 @@ Update the builder environment before attempting to build. Upload to \fIUPLOAD\fR with \fBdput\fR(1) (after confirmation). .TP .B \-v \fIVERSION\fR, \-\-version=\fIVERSION\fR -If the \fB\-\-from\fR option is specified, then \fBbackportpackage\fR -verifies that the current version of \fIsource package\fR in -\fISOURCE\fR is the same as \fIVERSION\fR. Otherwise, +If the \fB\-\-source\fR option is specified, then +\fBbackportpackage\fR verifies that the current version of \fIsource +package\fR in \fISOURCE\fR is the same as \fIVERSION\fR. Otherwise, \fBbackportpackage\fR finds version \fIVERSION\fR of \fIsource package\fR, regardless of the release in which it was published (or if -that version is still current). +that version is still current). This option is ignored if a .dsc URL +or path is passed in instead of a source package name. +.TP +.B \-w \fIWORKDIR\fR, \-\-workdir=\fIWORKDIR\fR +If \fIWORKDIR\fR is specified, then all files are downloaded, +unpacked, built into, and otherwise manipulated in +\fIWORKDIR\fR. Otherwise, a temporary directory is created, which is +deleted before \fIbackportpackage\fR exits. .TP .B \-l \fIINSTANCE\fR, \-\-launchpad=\fIINSTANCE\fR Use the specified instance of Launchpad (e.g. "staging"), instead of the default of "production". .SH DESCRIPTION -\fBbackportpackage\fR fetches a package from one Ubuntu release and -creates a no-change backport of that package to a previous release, -optionally doing a test build of the package and/or uploading the -resulting backport for testing. +\fBbackportpackage\fR fetches a package from one Ubuntu release or +from a specified .dsc path or URL and creates a no-change backport of +that package to a previous release, optionally doing a test build of +the package and/or uploading the resulting backport for testing. .PP -The backported package is fetched and built in a temporary directory -in \fB/tmp\fR, which is removed once the script finishes running. +Unless a working directory is specified, the backported package is +fetched and built in a temporary directory in \fB/tmp\fR, which is +removed once the script finishes running. .PP \fBbackportpackage\fR is only recommended for testing backports in a PPA, not uploading backports to the Ubuntu archive. @@ -67,9 +73,34 @@ PPA, not uploading backports to the Ubuntu archive. .TP .B UBUNTUTOOLS_BUILDER The default builder for Ubuntu development tools that support it -(including \fBbackportpackage\fR. Supported are \fBpbuilder\fR(8) and +(including \fBbackportpackage\fR). Supported are \fBpbuilder\fR(8) and \fBsbuild\fR(1). If unset and not provided on the command line, \fBpbuilder\fR(8) is used. +.SH EXAMPLES +Test-build in your PPA a backport of znc from the current development +release to your workstation's release, deleting the build products +afterwards: +.IP +.nf +.B backportpackage -u ppa:\fIuser\fR/\fIppa\fB znc +.fi +.PP +Backport squashfs-tools from Maverick to both Karmic and Lucid and +test-build both locally, leaving all build products in the current +working directory: +.IP +.nf +.B backportpackage -b -s maverick -d karmic -d lucid -w . \\\\ +.B " "squashfs-tools +.fi +.PP +Fetch a package from a PPA, backport it to Hardy, then upload it back +to the same PPA: +.IP +.nf +.B backportpackage -d hardy -u ppa:\fIuser\fR/\fIppa\fR \\\\ +.B " "https://launchpad.net/\fIsome/file.dsc\fR +.fi .SH AUTHOR \fBbackportpackage\fR and this manpage were written by Evan Broder