diff options
Diffstat (limited to 'gbp/scripts/buildpackage.py')
-rw-r--r-- | gbp/scripts/buildpackage.py | 571 |
1 files changed, 571 insertions, 0 deletions
diff --git a/gbp/scripts/buildpackage.py b/gbp/scripts/buildpackage.py new file mode 100644 index 0000000..d0ce3c1 --- /dev/null +++ b/gbp/scripts/buildpackage.py @@ -0,0 +1,571 @@ +# vim: set fileencoding=utf-8 : +# +# (C) 2006-2011 Guido Guenther <agx@sigxcpu.org> +# 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 2 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, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +"""run commands to build a debian package out of a git repository""" + +import ConfigParser +import errno +import os, os.path +import pipes +import sys +import time +import tempfile +import shutil +import gbp.deb as du +from gbp.git import (GitRepositoryError, GitRepository, build_tag) +from gbp.command_wrappers import (Command, + RunAtCommand, CommandExecFailed, PristineTar, + RemoveTree, CatenateTarArchive) +from gbp.config import (GbpOptionParser, GbpOptionGroup) +from gbp.errors import GbpError +from glob import glob +import gbp.log +import gbp.notifications + +# when we want to reference the index in a treeish context we call it: +index_name = "INDEX" +# when we want to reference the working copy in treeish context we call it: +wc_name = "WC" +# index file name used to export working copy +wc_index = ".git/gbp_index" + + +def git_archive_submodules(repo, treeish, output, prefix, comp_type, comp_level, comp_opts): + """ + Create tar.gz of an archive with submodules + + since git-archive always writes an end of tarfile trailer we concatenate + the generated archives using tar and compress the result. + + Exception handling is left to the caller. + """ + + tarfile = output.rsplit('.', 1)[0] + tempdir = tempfile.mkdtemp() + submodule_tarfile = os.path.join(tempdir, "submodule.tar") + try: + # generate main tarfile + repo.archive(format='tar', prefix='%s/' % (prefix), + output=tarfile, treeish=treeish) + + # generate each submodule's tarfile and append it to the main archive + for (subdir, commit) in repo.get_submodules(treeish): + tarpath = [subdir, subdir[2:]][subdir.startswith("./")] + + gbp.log.debug("Processing submodule %s (%s)" % (subdir, commit[0:8])) + repo.archive(format='tar', prefix='%s/%s/' % (prefix, tarpath), + output=submodule_tarfile, treeish=commit, cwd=subdir) + CatenateTarArchive(tarfile)(submodule_tarfile) + + # compress the output + ret = os.system("%s -%s %s %s" % (comp_type, comp_level, comp_opts, tarfile)) + if ret: + raise GbpError("Error creating %s: %d" % (output, ret)) + finally: + shutil.rmtree(tempdir) + + +def git_archive_single(treeish, output, prefix, comp_type, comp_level, comp_opts): + """ + Create tar.gz of an archive without submodules + + Exception handling is left to the caller. + """ + pipe = pipes.Template() + pipe.prepend("git archive --format=tar --prefix=%s/ %s" % (prefix, treeish), '.-') + pipe.append('%s -c -%s %s' % (comp_type, comp_level, comp_opts), '--') + ret = pipe.copy('', output) + if ret: + raise GbpError("Error creating %s: %d" % (output, ret)) + + +def git_archive(repo, cp, output_dir, treeish, comp_type, comp_level, with_submodules): + "create a compressed orig tarball in output_dir using git_archive" + try: + comp_opts = du.compressor_opts[comp_type][0] + except KeyError: + raise GbpError, "Unsupported compression type '%s'" % comp_type + + output = os.path.join(output_dir, du.orig_file(cp, comp_type)) + prefix = "%s-%s" % (cp['Source'], cp['Upstream-Version']) + + try: + if repo.has_submodules() and with_submodules: + repo.update_submodules() + git_archive_submodules(repo, treeish, output, prefix, + comp_type, comp_level, comp_opts) + + else: + git_archive_single(treeish, output, prefix, + comp_type, comp_level, comp_opts) + except CommandExecFailed: + gbp.log.err("Error generating submodules' archives") + return False + except OSError, err: + gbp.log.err("Error creating %s: %s" % (output, err[0])) + return False + except GbpError: + raise + except Exception as e: + gbp.log.err("Error creating %s: %s" % (output, e)) + return False + return True + + +def dump_tree(repo, export_dir, treeish, with_submodules): + "dump a tree to output_dir" + output_dir = os.path.dirname(export_dir) + prefix = os.path.basename(export_dir) + + pipe = pipes.Template() + pipe.prepend('git archive --format=tar --prefix=%s/ %s' % (prefix, treeish), '.-') + pipe.append('tar -C %s -xf -' % output_dir, '-.') + top = os.path.abspath(os.path.curdir) + try: + ret = pipe.copy('', '') + if ret: + raise GbpError, "Error in dump_tree archive pipe" + + if with_submodules: + if repo.has_submodules(): + repo.update_submodules() + for (subdir, commit) in repo.get_submodules(treeish): + gbp.log.info("Processing submodule %s (%s)" % (subdir, commit[0:8])) + tarpath = [subdir, subdir[2:]][subdir.startswith("./")] + os.chdir(subdir) + pipe = pipes.Template() + pipe.prepend('git archive --format=tar --prefix=%s/%s/ %s' % + (prefix, tarpath, commit), '.-') + pipe.append('tar -C %s -xf -' % output_dir, '-.') + ret = pipe.copy('', '') + os.chdir(top) + if ret: + raise GbpError, "Error in dump_tree archive pipe in submodule %s" % subdir + except OSError, err: + gbp.log.err("Error dumping tree to %s: %s" % (output_dir, err[0])) + return False + except GbpError, err: + gbp.log.err(err) + return False + except Exception as e: + gbp.log.err("Error dumping tree to %s: %s" % (output_dir, e)) + return False + finally: + os.chdir(top) + return True + + +def move_old_export(target): + """move a build tree away if it exists""" + try: + os.mkdir(target) + except OSError, (e, msg): + if e == errno.EEXIST: + os.rename(target, "%s.obsolete.%s" % (target, time.time())) + + +def prepare_output_dir(dir): + output_dir = dir + if not dir: + output_dir = '..' + output_dir = os.path.abspath(output_dir) + + try: + os.mkdir(output_dir) + except OSError, (e, msg): + if e != errno.EEXIST: + raise GbpError, "Cannot create output dir %s" % output_dir + return output_dir + +def pristine_tar_build_orig(repo, cp, output_dir, options): + """ + build orig using pristine-tar + @return: True: orig.tar.gz build, False: noop + """ + if options.pristine_tar: + pt = PristineTar() + if not repo.has_branch(pt.branch): + gbp.log.warn('Pristine-tar branch "%s" not found' % pt.branch) + pt.checkout(os.path.join(output_dir, du.orig_file(cp, options.comp_type))) + return True + else: + return False + + +def git_archive_build_orig(repo, cp, output_dir, options): + """build orig using git-archive""" + if options.upstream_tree == 'tag': + upstream_tree = build_tag(options.upstream_tag, cp['Upstream-Version']) + elif options.upstream_tree == 'branch': + upstream_tree = options.upstream_branch + else: + raise GbpError, "Unknown value %s" % options.upstream_tree + gbp.log.info("%s does not exist, creating from '%s'" % (du.orig_file(cp, + options.comp_type), + upstream_tree)) + if not repo.has_treeish(upstream_tree): + raise GbpError # git-ls-tree printed an error message already + gbp.log.debug("Building upstream tarball with compression '%s -%s'" % (options.comp_type, + options.comp_level)) + if not git_archive(repo, cp, output_dir, upstream_tree, + options.comp_type, options.comp_level, options.with_submodules): + raise GbpError, "Cannot create upstream tarball at '%s'" % output_dir + + +def write_wc(repo): + """write out the current working copy as a treeish object""" + repo.add_files(repo.path, force=True, index_file=wc_index) + tree = repo.write_tree(index_file=wc_index) + return tree + +def drop_index(): + """drop our custom index""" + if os.path.exists(wc_index): + os.unlink(wc_index) + +def extract_orig(orig_tarball, dest_dir): + """extract orig tarball to export dir before exporting from git""" + gbp.log.info("Extracting %s to '%s'" % (os.path.basename(orig_tarball), dest_dir)) + + move_old_export(dest_dir) + upstream = gbp.deb.UpstreamSource(orig_tarball) + upstream.unpack(dest_dir) + + # Check if tarball extracts into a single folder or not: + if upstream.unpacked != dest_dir: + # If it extracts a single folder, move all of its contents to dest_dir: + r = glob("%s/*" % upstream.unpacked) + r.extend(glob("%s/.*" % upstream.unpacked)) # include hidden files and folders + for f in r: + os.rename(f, os.path.join(dest_dir, os.path.basename(f))) + + # Remove that single folder: + os.rmdir(upstream.unpacked) + + +def guess_comp_type(repo, comp_type, cp, tarball_dir): + """Guess compression type""" + + srcpkg = cp['Source'] + upstream_version = cp['Upstream-Version'] + + if comp_type != 'auto': + comp_type = du.compressor_aliases.get(comp_type, comp_type) + try: + dummy = du.compressor_opts[comp_type] + except KeyError: + gbp.log.warn("Unknown compression type - guessing.") + comp_type = 'auto' + + if comp_type == 'auto': + if not repo.has_branch(PristineTar.branch): + if not tarball_dir: + tarball_dir = '..' + detected = None + for comp in du.compressor_opts.keys(): + if du.has_orig(cp, comp, tarball_dir): + if detected is not None: + raise GbpError, "Multiple orig tarballs found." + detected = comp + if detected is not None: + comp_type = detected + else: + comp_type = 'gzip' + else: + regex = 'pristine-tar .* %s_%s\.orig.tar\.' % (srcpkg, upstream_version) + commits = repo.grep_log(regex, PristineTar.branch) + if commits: + commit = commits[-1] + gbp.log.debug("Found pristine-tar commit at '%s'" % commit) + else: + commit = PristineTar.branch + tarball = repo.get_subject(commit) + comp_type = du.get_compression(tarball) + gbp.log.debug("Determined compression type '%s'" % comp_type) + if not comp_type: + comp_type = 'gzip' + gbp.log.warn("Unknown compression type of %s, assuming %s" % (tarball, comp_type)) + return comp_type + + +def setup_pbuilder(options): + """setup everything to use git-pbuilder""" + if options.use_pbuilder or options.use_qemubuilder: + options.builder = 'git-pbuilder' + options.cleaner = '/bin/true' + os.environ['DIST'] = options.pbuilder_dist + if options.pbuilder_arch: + os.environ['ARCH'] = options.pbuilder_arch + if options.use_qemubuilder: + os.environ['BUILDER'] = "qemubuilder" + + +def parse_args(argv, prefix): + args = [ arg for arg in argv[1:] if arg.find('--%s' % prefix) == 0 ] + dpkg_args = [ arg for arg in argv[1:] if arg.find('--%s' % prefix) == -1 ] + + # We handle these although they don't have a --git- prefix + for arg in [ "--help", "-h", "--version" ]: + if arg in dpkg_args: + args.append(arg) + + try: + parser = GbpOptionParser(command=os.path.basename(argv[0]), prefix=prefix) + except ConfigParser.ParsingError, err: + gbp.log.err(err) + return None, None, None + + tag_group = GbpOptionGroup(parser, "tag options", "options related to git tag creation") + branch_group = GbpOptionGroup(parser, "branch options", "branch layout options") + cmd_group = GbpOptionGroup(parser, "external command options", "how and when to invoke external commands and hooks") + orig_group = GbpOptionGroup(parser, "orig tarball options", "options related to the creation of the orig tarball") + export_group = GbpOptionGroup(parser, "export build-tree options", "alternative build tree related options") + parser.add_option_group(tag_group) + parser.add_option_group(orig_group) + parser.add_option_group(branch_group) + parser.add_option_group(cmd_group) + parser.add_option_group(export_group) + + parser.add_boolean_config_file_option(option_name = "ignore-new", dest="ignore_new") + parser.add_option("--git-verbose", action="store_true", dest="verbose", default=False, + help="verbose command execution") + parser.add_config_file_option(option_name="color", dest="color", type='tristate') + parser.add_config_file_option(option_name="notify", dest="notify", type='tristate') + tag_group.add_option("--git-tag", action="store_true", dest="tag", default=False, + help="create a tag after a successful build") + tag_group.add_option("--git-tag-only", action="store_true", dest="tag_only", default=False, + help="don't build, only tag and run the posttag hook") + tag_group.add_option("--git-retag", action="store_true", dest="retag", default=False, + help="don't fail if the tag already exists") + tag_group.add_boolean_config_file_option(option_name="sign-tags", dest="sign_tags") + tag_group.add_config_file_option(option_name="keyid", dest="keyid") + tag_group.add_config_file_option(option_name="debian-tag", dest="debian_tag") + tag_group.add_config_file_option(option_name="upstream-tag", dest="upstream_tag") + orig_group.add_config_file_option(option_name="upstream-tree", dest="upstream_tree") + orig_group.add_boolean_config_file_option(option_name="pristine-tar", dest="pristine_tar") + orig_group.add_config_file_option(option_name="force-create", dest="force_create", + help="force creation of orig.tar.gz", action="store_true") + orig_group.add_config_file_option(option_name="no-create-orig", dest="no_create_orig", + help="don't create orig.tar.gz", action="store_true") + orig_group.add_config_file_option(option_name="tarball-dir", dest="tarball_dir", type="path", + help="location to look for external tarballs") + orig_group.add_config_file_option(option_name="compression", dest="comp_type", + help="Compression type, default is '%(compression)s'") + orig_group.add_config_file_option(option_name="compression-level", dest="comp_level", + help="Compression level, default is '%(compression-level)s'") + branch_group.add_config_file_option(option_name="upstream-branch", dest="upstream_branch") + branch_group.add_config_file_option(option_name="debian-branch", dest="debian_branch") + branch_group.add_boolean_config_file_option(option_name = "ignore-branch", dest="ignore_branch") + branch_group.add_boolean_config_file_option(option_name = "submodules", dest="with_submodules") + cmd_group.add_config_file_option(option_name="builder", dest="builder", + help="command to build the Debian package, default is '%(builder)s'") + cmd_group.add_config_file_option(option_name="cleaner", dest="cleaner", + help="command to clean the working copy, default is '%(cleaner)s'") + cmd_group.add_config_file_option(option_name="prebuild", dest="prebuild", + help="command to run before a build, default is '%(prebuild)s'") + cmd_group.add_config_file_option(option_name="postbuild", dest="postbuild", + help="hook run after a successful build, default is '%(postbuild)s'") + cmd_group.add_config_file_option(option_name="posttag", dest="posttag", + help="hook run after a successful tag operation, default is '%(posttag)s'") + cmd_group.add_boolean_config_file_option(option_name="pbuilder", dest="use_pbuilder") + cmd_group.add_boolean_config_file_option(option_name="qemubuilder", dest="use_qemubuilder") + cmd_group.add_config_file_option(option_name="dist", dest="pbuilder_dist") + cmd_group.add_config_file_option(option_name="arch", dest="pbuilder_arch") + export_group.add_config_file_option(option_name="export-dir", dest="export_dir", type="path", + help="before building the package export the source into EXPORT_DIR, default is '%(export-dir)s'") + export_group.add_config_file_option("export", dest="export", + help="export treeish object TREEISH, default is '%(export)s'", metavar="TREEISH") + export_group.add_option("--git-dont-purge", action="store_false", dest="purge", default=True, + help="retain exported package build directory") + export_group.add_boolean_config_file_option(option_name="overlay", dest="overlay") + options, args = parser.parse_args(args) + + gbp.log.setup(options.color, options.verbose) + if options.retag: + if not options.tag and not options.tag_only: + gbp.log.err("'--%sretag' needs either '--%stag' or '--%stag-only'" % (prefix, prefix, prefix)) + return None, None, None + + if options.overlay and not options.export_dir: + gbp.log.err("Overlay must be used with --git-export-dir") + return None, None, None + + return options, args, dpkg_args + + +def main(argv): + retval = 0 + changelog = 'debian/changelog' + prefix = "git-" + cp = None + + options, gbp_args, dpkg_args = parse_args(argv, prefix) + if not options: + return 1 + + try: + repo = GitRepository(os.path.curdir) + except GitRepositoryError: + gbp.log.err("%s is not a git repository" % (os.path.abspath('.'))) + return 1 + else: + repo_dir = os.path.abspath(os.path.curdir) + + try: + branch = repo.get_branch() + Command(options.cleaner, shell=True)() + if not options.ignore_new: + (ret, out) = repo.is_clean() + if not ret: + gbp.log.err("You have uncommitted changes in your source tree:") + gbp.log.err(out) + raise GbpError, "Use --git-ignore-new to ignore." + + if not options.ignore_new and not options.ignore_branch: + if branch != options.debian_branch: + gbp.log.err("You are not on branch '%s' but on '%s'" % (options.debian_branch, branch)) + raise GbpError, "Use --git-ignore-branch to ignore or --git-debian-branch to set the branch name." + + try: + cp = du.parse_changelog(filename=changelog) + version = cp['Version'] + version_no_epoch = cp['NoEpoch-Version'] + if du.is_native(cp): + major = cp['Debian-Version'] + else: + major = cp['Upstream-Version'] + except du.NoChangelogError: + raise GbpError, "'%s' does not exist, not a debian package" % changelog + except du.ParseChangeLogError, err: + raise GbpError, "Error parsing Changelog: %s" % err + except KeyError: + raise GbpError, "Can't parse version from changelog" + + if not options.tag_only: + output_dir = prepare_output_dir(options.export_dir) + if options.tarball_dir: + tarball_dir = options.tarball_dir + else: + tarball_dir = output_dir + + # Get/build the orig.tar.gz if necessary: + if not du.is_native(cp): + options.comp_type = guess_comp_type( + repo, options.comp_type, cp, options.tarball_dir) + orig_file = du.orig_file(cp, options.comp_type) + + # look in tarball_dir first, if found force a symlink to it + if options.tarball_dir: + gbp.log.debug("Looking for orig tarball '%s' at '%s'" % (orig_file, tarball_dir)) + if not du.symlink_orig(cp, options.comp_type, tarball_dir, output_dir, force=True): + gbp.log.info("Orig tarball '%s' not found at '%s'" % (orig_file, tarball_dir)) + else: + gbp.log.info("Orig tarball '%s' found at '%s'" % (orig_file, tarball_dir)) + # build an orig unless the user forbids it, always build (and overwrite pre-existing) if user forces it + if options.force_create or (not options.no_create_orig and not du.has_orig(cp, options.comp_type, output_dir)): + if not pristine_tar_build_orig(repo, cp, output_dir, options): + git_archive_build_orig(repo, cp, output_dir, options) + + # Export to another build dir if requested: + if options.export_dir: + # write a tree of the index if necessary: + if options.export == index_name: + tree = repo.write_tree() + elif options.export == wc_name: + tree = write_wc(repo) + else: + tree = options.export + if not repo.has_treeish(tree): + raise GbpError # git-ls-tree printed an error message already + tmp_dir = os.path.join(output_dir, "%s-tmp" % cp['Source']) + + # Extract orig tarball if git-overlay option is selected: + if options.overlay: + if du.is_native(cp): + raise GbpError, "Cannot overlay Debian native package" + extract_orig(os.path.join(output_dir, du.orig_file(cp, options.comp_type)), tmp_dir) + + gbp.log.info("Exporting '%s' to '%s'" % (options.export, tmp_dir)) + if not dump_tree(repo, tmp_dir, tree, options.with_submodules): + raise GbpError + cp = du.parse_changelog(filename=os.path.join(tmp_dir, 'debian', 'changelog')) + export_dir = os.path.join(output_dir, "%s-%s" % (cp['Source'], major)) + gbp.log.info("Moving '%s' to '%s'" % (tmp_dir, export_dir)) + move_old_export(export_dir) + os.rename(tmp_dir, export_dir) + + if options.export_dir: + build_dir = export_dir + else: + build_dir = repo_dir + + if options.prebuild: + RunAtCommand(options.prebuild, shell=True, + extra_env={'GBP_GIT_DIR': repo.git_dir, + 'GBP_BUILD_DIR': build_dir})(dir=build_dir) + + setup_pbuilder(options) + # Finally build the package: + RunAtCommand(options.builder, dpkg_args, shell=True, + extra_env={'GBP_BUILD_DIR': build_dir})(dir=build_dir) + if options.postbuild: + arch = os.getenv('ARCH', None) or du.get_arch() + changes = os.path.abspath("%s/../%s_%s_%s.changes" % + (build_dir, cp['Source'], version_no_epoch, arch)) + gbp.log.debug("Looking for changes file %s" % changes) + if not os.path.exists(changes): + changes = os.path.abspath("%s/../%s_%s_source.changes" % + (build_dir, cp['Source'], version_no_epoch)) + Command(options.postbuild, shell=True, + extra_env={'GBP_CHANGES_FILE': changes, + 'GBP_BUILD_DIR': build_dir})() + if options.tag or options.tag_only: + gbp.log.info("Tagging %s" % version) + tag = build_tag(options.debian_tag, version) + if options.retag and repo.has_tag(tag): + repo.delete_tag(tag) + repo.create_tag(name=tag, msg="Debian release %s" % version, + sign=options.sign_tags, keyid=options.keyid) + if options.posttag: + sha = repo.rev_parse("%s^{}" % tag) + Command(options.posttag, shell=True, + extra_env={'GBP_TAG': tag, + 'GBP_BRANCH': branch, + 'GBP_SHA1': sha})() + except CommandExecFailed: + retval = 1 + except GbpError, err: + if len(err.__str__()): + gbp.log.err(err) + retval = 1 + finally: + drop_index() + + if not options.tag_only: + if options.export_dir and options.purge and not retval: + RemoveTree(export_dir)() + + if cp and not gbp.notifications.notify(cp, not retval, options.notify): + gbp.log.err("Failed to send notification") + retval = 1 + + return retval + +if __name__ == '__main__': + sys.exit(main(sys.argv)) + +# vim:et:ts=4:sw=4:et:sts=4:ai:set list listchars=tab\:»·,trail\:·: |