# vim: set fileencoding=utf-8 : # # (C) 2011 Guido Günther # (C) 2012 Intel Corporation # 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 # """manage patches in a patch queue""" import configparser import errno import os import shutil import sys import gbp.tmpfile as tempfile from gbp.config import GbpOptionParserBB from gbp.rpm.git import GitRepositoryError, RpmGitRepository from gbp.command_wrappers import GitCommand, CommandExecFailed from gbp.errors import GbpError import gbp.log from gbp.patch_series import PatchSeries, Patch from gbp.rpm import string_to_int from gbp.scripts.common.pq import (is_pq_branch, pq_branch_name, pq_branch_base, apply_and_commit_patch, drop_pq) from gbp.scripts.pq_rpm import (generate_patches, safe_patches, import_extra_files) from gbp.bb import bb, init_tinfoil, parse_bb, pkg_version USAGE_STRING = \ """%prog [options] action - maintain patches on a patch queue branch tions: export Export the patch queue / devel branch associated to the current branch into a patch series in and update the recipe file import Create a patch queue / devel branch from recipe file and patches in current dir. rebase Switch to patch queue / devel branch associated to the current branch and rebase against upstream. drop Drop (delete) the patch queue /devel branch associated to the current branch. apply Apply a patch switch Switch to patch-queue branch and vice versa.""" def rm_patch_files(bbfile): """Delete the patch files listed in the pkg meta data.""" unlinked = set() # Go through local files for path in bbfile.localfiles: if path.endswith('.patch'): gbp.log.debug("Removing patch '%s'" % path) unlinked.add(os.path.basename(path)) try: os.unlink(path) except OSError as err: if err.errno != errno.ENOENT: raise GbpError("Failed to remove patch: %s" % err) else: gbp.log.debug("Patch %s does not exist." % path) else: gbp.log.debug("Unlink skipping non-local/non-patch file %s" % path) uris = (bbfile.getVar('SRC_URI', False) or "").split() return [uri for uri in uris if os.path.basename(uri) not in unlinked] def update_patch_series(repo, bbfile, start, end, options): """Export patches to packaging directory and update recipe file""" squash = options.patch_export_squash_until.split(':', 1) if len(squash) == 1: squash.append(None) else: squash[1] += '.diff' # Unlink old (local) patch files and generate new patches rm_patch_files(bbfile) # Guess patch subdir bb_dir = os.path.dirname(bbfile.getVar('FILE', True)) pkg_name = bbfile.getVar('PN', True) pkg_ver = bbfile.getVar('PV', True) subdir = pkg_name + '-' + pkg_ver if not os.path.isdir(os.path.join(bb_dir, subdir)): if os.path.isdir(os.path.join(bb_dir, pkg_name)): subdir = pkg_name elif os.path.isdir(os.path.join(bb_dir, 'files')): subdir = 'files' tgt_dir = os.path.join(bb_dir, subdir) patches, _commands = generate_patches(repo, start, squash, end, tgt_dir, options) # TODO: implement commands processing (e.g. topic) new_uris = ['file://' + patch for patch in patches] bbfile.substitute_var_val(bbfile.bb_path, 'SRC_URI', r'file://\S+.\.patch', '') bbfile.append_var_val(bbfile.bb_path, 'SRC_URI', new_uris) return patches def var_to_str(var, value): """Create a well formatted string buffer for a variable assignment""" indent = ' ' * (len(var) + 3) linebuf = ['%s = "%s \\\n' % (var, value[0])] for val in value[1:]: linebuf.append(indent + ' ' + val + '\\\n') linebuf.append(indent + '"\n') return linebuf def find_upstream_commit(repo, bbfile, upstream_tag): """Find commit corresponding upstream version""" src_rev = bbfile.getVar('SRCREV', True) if src_rev and src_rev != 'INVALID': return bbfile.getVar('SRCREV', True) # Find tag upstreamversion = bbfile.getVar('PV', True) tag_str_fields = {'upstreamversion': upstreamversion, 'vendor': 'Upstream'} upstream_commit = repo.find_version(upstream_tag, tag_str_fields) if not upstream_commit: raise GbpError("Couldn't find upstream version %s" % upstreamversion) return upstream_commit def export_patches(cfg, repo, options): """Export patches from the pq branch into a packaging branch""" current = repo.get_branch() if is_pq_branch(current, options): base = pq_branch_base(current, options) gbp.log.info("On branch '%s', switching to '%s'" % (current, base)) repo.set_branch(base) bbfile = parse_bb(cfg, options, repo) pq_branch = current else: bbfile = parse_bb(cfg, options, repo) pq_branch = pq_branch_name(current, options, pkg_version(bbfile)) upstream_commit = find_upstream_commit(repo, bbfile, options.upstream_tag) export_treeish = options.export_rev if options.export_rev else pq_branch update_patch_series(repo, bbfile, upstream_commit, export_treeish, options) bb_dir = os.path.dirname(bbfile.getVar('FILE', True)) GitCommand('status')(['--', bb_dir]) def bb_to_patch_series(bbfile): """Get all local patches as a series""" series = PatchSeries() for path in bbfile.localfiles: if path.endswith('.patch'): series.append(Patch(path)) return series def import_bb_patches(cfg, repo, options): """Apply a series of patches in a recipe to branch onto a pq branch""" current = repo.get_branch() if is_pq_branch(current, options): base = pq_branch_base(current, options) raise GbpError("Already on a patch-queue branch '%s' - doing " "nothing." % current) else: bbfile = parse_bb(cfg, options, repo) base = current upstream_commit = find_upstream_commit(repo, bbfile, options.upstream_tag) pq_branch = pq_branch_name(base, options, pkg_version(bbfile)) # Create pq-branch if repo.has_branch(pq_branch) and not options.force: raise GbpError("Patch-queue branch '%s' already exists. " "Try 'rebase' instead." % pq_branch) try: if repo.get_branch() == pq_branch: repo.force_head(upstream_commit, hard=True) else: repo.create_branch(pq_branch, upstream_commit, force=True) except GitRepositoryError as err: raise GbpError("Cannot create patch-queue branch '%s': %s" % (pq_branch, err)) # Put patches in a safe place in_queue = bb_to_patch_series(bbfile) queue = safe_patches(in_queue, options.tmp_dir) # Do import try: gbp.log.info("Switching to branch '%s'" % pq_branch) repo.set_branch(pq_branch) import_extra_files(repo, base, options.import_files) if not queue: return gbp.log.info("Trying to apply patches from branch '%s' onto '%s'" % (base, upstream_commit)) for patch in queue: gbp.log.debug("Applying %s" % patch.path) apply_and_commit_patch(repo, patch, fallback_author=None) except (GbpError, GitRepositoryError) as err: gbp.log.err('Import failed: %s' % err) repo.force_head('HEAD', hard=True) repo.set_branch(base) repo.delete_branch(pq_branch) raise recipe_fn = os.path.basename(bbfile.getVar('FILE', True)) gbp.log.info("Patches listed in '%s' imported on '%s'" % (recipe_fn, pq_branch)) def rebase_pq(cfg, repo, options): """Rebase pq branch on the correct upstream version""" current = repo.get_branch() if is_pq_branch(current, options): base = pq_branch_base(current, options) bbfile = parse_bb(cfg, options, repo, base) else: base = current bbfile = parse_bb(cfg, options, repo) upstream_commit = find_upstream_commit(repo, bbfile, options.upstream_tag) switch_to_pq_branch(cfg, repo, base, options) GitCommand("rebase")([upstream_commit]) def switch_pq(cfg, repo, options): """Switch to patch-queue branch if on base branch and vice versa""" current = repo.get_branch() if is_pq_branch(current, options): base = pq_branch_base(current, options) gbp.log.info("Switching to branch '%s'" % base) repo.checkout(base) else: switch_to_pq_branch(cfg, repo, current, options) def drop_pq_bb(cfg, repo, options): """Remove pq branch""" current = repo.get_branch() if is_pq_branch(current, options): base = pq_branch_base(current, options) bbfile = parse_bb(cfg, options, repo, base) else: bbfile = parse_bb(cfg, options, repo) drop_pq(repo, current, options, pkg_version(bbfile)) def switch_to_pq_branch(cfg, repo, branch, options): """ Switch to patch-queue branch if not already there, create it if it doesn't exist yet """ if is_pq_branch(branch, options): return bbfile = parse_bb(cfg, options, repo, branch) pq_branch = pq_branch_name(branch, options, pkg_version(bbfile)) if not repo.has_branch(pq_branch): raise GbpError("Branch '%s' does not exist" % pq_branch) gbp.log.info("Switching to branch '%s'" % pq_branch) repo.set_branch(pq_branch) def apply_single_patch(cfg, repo, patchfile, options): """Apply a single patch onto the pq branch""" current = repo.get_branch() if not is_pq_branch(current, options): switch_to_pq_branch(cfg, repo, current, options) patch = Patch(patchfile) apply_and_commit_patch(repo, patch, fallback_author=None) def opt_split_cb(option, opt_str, value, parser): """Split option string into a list""" setattr(parser.values, option.dest, value.split(',')) def build_parser(name): """Create command line argument parser""" try: parser = GbpOptionParserBB(command=os.path.basename(name), prefix='', usage=USAGE_STRING) except configparser.ParsingError as err: gbp.log.err(err) return None parser.add_boolean_config_file_option(option_name="patch-numbers", dest="patch_numbers") parser.add_option("-v", "--verbose", action="store_true", dest="verbose", default=False, help="Verbose command execution") parser.add_option("--force", dest="force", action="store_true", default=False, help="In case of import even import if the branch already exists") parser.add_config_file_option(option_name="vendor", action="store", dest="vendor") parser.add_config_file_option(option_name="color", dest="color", type='tristate') parser.add_config_file_option(option_name="color-scheme", dest="color_scheme") parser.add_config_file_option(option_name="tmp-dir", dest="tmp_dir") parser.add_config_file_option(option_name="upstream-tag", dest="upstream_tag") parser.add_config_file_option(option_name="bb-file", dest="bb_file") parser.add_config_file_option(option_name="meta-dir", dest="meta_dir") parser.add_config_file_option(option_name="packaging-branch", dest="packaging_branch", help="Branch the packaging is being maintained on. Only relevant " "if a invariable/single pq-branch is defined, in which case " "this is used as the 'base' branch. Default is " "'%(packaging-branch)s'") parser.add_config_file_option(option_name="pq-branch", dest="pq_branch") parser.add_config_file_option(option_name="import-files", dest="import_files", type="string", action="callback", callback=opt_split_cb) parser.add_option("--export-rev", action="store", dest="export_rev", default="", help="Export patches from treeish object TREEISH instead of head " "of patch-queue branch", metavar="TREEISH") parser.add_config_file_option("patch-export-compress", dest="patch_export_compress") parser.add_config_file_option("patch-export-squash-until", dest="patch_export_squash_until") parser.add_config_file_option("patch-export-ignore-path", dest="patch_export_ignore_path") return parser def parse_args(argv): """Parse command line arguments""" parser = build_parser(argv[0]) if not parser: return None, None options, args = parser.parse_args(argv) gbp.log.setup(options.color, options.verbose, options.color_scheme) options.patch_export_compress = string_to_int(options.patch_export_compress) return options, args def main(argv): """Main function for the gbp pq-rpm command""" retval = 0 if not bb: return 1 options, args = parse_args(argv) if not options: return 1 if len(args) < 2: gbp.log.err("No action given.") return 1 else: action = args[1] if args[1] in ["export", "import", "rebase", "drop", "switch"]: pass elif args[1] in ["apply"]: if len(args) != 3: gbp.log.err("No patch name given.") return 1 else: patchfile = args[2] else: gbp.log.err("Unknown action '%s'." % args[1]) return 1 try: repo = RpmGitRepository(os.path.curdir) except GitRepositoryError: gbp.log.err("%s is not a git repository" % (os.path.abspath('.'))) return 1 if os.path.abspath('.') != repo.path: gbp.log.warn("Switching to topdir before running commands") os.chdir(repo.path) try: # Initialize BitBake tinfoil = init_tinfoil(config_only=True, tracking=True) bb_cfg_data = bb.data.createCopy(tinfoil.config_data) # Create base temporary directory for this run options.tmp_dir = tempfile.mkdtemp(dir=options.tmp_dir, prefix='gbp-pq-bb_') if action == "export": export_patches(bb_cfg_data, repo, options) elif action == "import": import_bb_patches(bb_cfg_data, repo, options) elif action == "drop": drop_pq_bb(bb_cfg_data, repo, options) elif action == "rebase": rebase_pq(bb_cfg_data, repo, options) elif action == "apply": apply_single_patch(bb_cfg_data, repo, patchfile, options) elif action == "switch": switch_pq(bb_cfg_data, repo, options) except CommandExecFailed: retval = 1 except GitRepositoryError as err: gbp.log.err("Git command failed: %s" % err) retval = 1 except GbpError as err: if len(err.__str__()): gbp.log.err(err) retval = 1 finally: shutil.rmtree(options.tmp_dir, ignore_errors=True) return retval if __name__ == '__main__': sys.exit(main(sys.argv))