forked from Mirrors/flatpak-builder
scripts: Add a script to allow bisecting failures
The CLI is pretty similare to the `git bisect` one and works as follow: $ flatpak-bisect org.app.App start # Update application and get the history $ flatpak-bisect org.app.App bad # Sets current commit as first bad commit $ flatpak-bisect org.app.App checkout GoodHash # Checkout the first known good commit $ flatpak-bisect org.app.App good # Sets GoodHash as first good commit ... Here it starts bisection and checkouts a commit on the way, the user should launch the app to check if the commit is good or bad and run: $ flatpak-bisect org.app.App good # if commit is good $ flatpak-bisect org.app.App bad # if commit is bad flatpak-bisect will tell when the first bad commit is found. Fixes https://github.com/flatpak/flatpak/issues/530tingping/wmclass
parent
6125dabf58
commit
75388609b3
|
@ -147,6 +147,9 @@ pkgconfigdir = $(libdir)/pkgconfig
|
|||
pkgconfig_DATA = flatpak.pc
|
||||
EXTRA_DIST += flatpak.pc.in
|
||||
|
||||
scriptsdir = $(bindir)
|
||||
scripts_SCRIPTS = scripts/flatpak-bisect
|
||||
|
||||
EXTRA_DIST += README.md
|
||||
|
||||
AM_DISTCHECK_CONFIGURE_FLAGS = \
|
||||
|
|
|
@ -0,0 +1,225 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import re
|
||||
import argparse
|
||||
import os
|
||||
import gi
|
||||
import json
|
||||
import subprocess
|
||||
|
||||
gi.require_version('Flatpak', '1.0')
|
||||
from gi.repository import Flatpak
|
||||
from gi.repository import GLib
|
||||
|
||||
def get_bisection_data():
|
||||
return {'ref': None, 'good': None, 'bad': None,
|
||||
'refs': None, 'log': None, 'messages': None}
|
||||
|
||||
class Bisector():
|
||||
def load_cache(self):
|
||||
try:
|
||||
os.makedirs(os.path.join(GLib.get_user_cache_dir(), 'flatpak'))
|
||||
except FileExistsError:
|
||||
pass
|
||||
|
||||
self.cache_path = os.path.join(GLib.get_user_cache_dir(),
|
||||
'flatpak', '%s-%s-bisect.status' % (
|
||||
self.name, self.branch))
|
||||
try:
|
||||
with open(self.cache_path, 'rb') as f:
|
||||
self.data = json.load(f)
|
||||
except FileNotFoundError:
|
||||
self.data = None
|
||||
|
||||
def dump_data(self):
|
||||
with open(self.cache_path, 'w') as f:
|
||||
json.dump(self.data, f)
|
||||
|
||||
def setup_flatpak_app(self):
|
||||
self.installation = Flatpak.Installation.new_user()
|
||||
kind = Flatpak.RefKind.APP
|
||||
if self.runtime:
|
||||
kind = Flatpak.RefKind.RUNTIME
|
||||
try:
|
||||
self.cref = self.installation.get_installed_ref(kind, self.name, None, self.branch, None)
|
||||
except GLib.Error as e:
|
||||
print("%s\n\nMake sure %s is installed as a "
|
||||
"user (flatpak install --user) and specify `--runtime`"
|
||||
" if it is a runtime." % (e, self.name))
|
||||
return -1
|
||||
return 0
|
||||
|
||||
def run(self):
|
||||
self.name = self.name[0]
|
||||
self.load_cache()
|
||||
res = self.setup_flatpak_app()
|
||||
if res:
|
||||
return res
|
||||
|
||||
try:
|
||||
func = getattr(self, self.subparser_name)
|
||||
except AttributeError:
|
||||
print('No action called %s' % self.subparser_name)
|
||||
|
||||
return -1
|
||||
|
||||
res = func()
|
||||
|
||||
if self.data:
|
||||
self.dump_data()
|
||||
|
||||
return res
|
||||
|
||||
def set_reference_commits(self, set_name, check_name):
|
||||
if not self.data:
|
||||
print("You need to first start the bisection")
|
||||
return -1
|
||||
ref = self.cref.get_latest_commit()
|
||||
|
||||
if self.data[check_name] == ref:
|
||||
print('Commit %s is already set as %s...' % (
|
||||
ref, check_name))
|
||||
return 1
|
||||
|
||||
if ref not in self.data['refs']:
|
||||
print("%s is not a known commit." % ref)
|
||||
return -1
|
||||
|
||||
print("Setting %s as %s commit" % (ref, set_name))
|
||||
self.data[set_name] = ref
|
||||
|
||||
if self.data[set_name] and self.data[check_name]:
|
||||
x1 = self.data['refs'].index(self.data['good'])
|
||||
x2 = self.data['refs'].index(self.data['bad'])
|
||||
|
||||
refs = self.data['refs'][x1:x2]
|
||||
if not refs:
|
||||
print("=========================="
|
||||
"First bad commit is:\n%s"
|
||||
"==========================" % self.data['message'][self.data['bad']])
|
||||
exit(0)
|
||||
ref = refs[int(len(refs) / 2)]
|
||||
if self.data['good'] == ref:
|
||||
print("\n==========================\n"
|
||||
"First bad commit is:\n\n%s"
|
||||
"==========================" % self.data['messages'][self.data['bad']])
|
||||
exit(0)
|
||||
|
||||
return self.checkout(ref)
|
||||
|
||||
return -1
|
||||
|
||||
def load_refs(self):
|
||||
repodir, refname = self.download_history()
|
||||
history = subprocess.check_output(['ostree', 'log', '--repo', repodir, refname]).decode()
|
||||
|
||||
refs = []
|
||||
messages = {}
|
||||
message = ""
|
||||
_hash = ''
|
||||
for l in history.split('\n'):
|
||||
rehash = re.search('(?<=^commit )\w+', l)
|
||||
if rehash:
|
||||
if message:
|
||||
messages[_hash] = message
|
||||
_hash = rehash.group(0)
|
||||
refs.insert(0, _hash)
|
||||
message = ""
|
||||
message += l + '\n'
|
||||
|
||||
if message:
|
||||
messages[_hash] = message
|
||||
|
||||
self.data['refs'] = refs
|
||||
self.data['log'] = history
|
||||
self.data['messages'] = messages
|
||||
|
||||
def good(self):
|
||||
if not self.data['bad']:
|
||||
print("Set the bad commit first")
|
||||
exit(-1)
|
||||
return self.set_reference_commits('good', 'bad')
|
||||
|
||||
def bad(self):
|
||||
return self.set_reference_commits('bad', 'good')
|
||||
|
||||
def start(self):
|
||||
if self.data:
|
||||
print('Bisection already started')
|
||||
return -1
|
||||
|
||||
print("Updating to %s latest commit" % self.name)
|
||||
self.reset(False)
|
||||
self.data = get_bisection_data()
|
||||
self.load_refs()
|
||||
|
||||
def download_history(self):
|
||||
print("Getting history")
|
||||
appidir = os.path.abspath(os.path.join(self.cref.get_deploy_dir(), '..'))
|
||||
dirname = "app"
|
||||
if self.runtime:
|
||||
dirname = "runtime"
|
||||
appidir = appidir.split('/%s/' % dirname)
|
||||
repodir = os.path.join(appidir[0], 'repo')
|
||||
refname = self.cref.get_origin() + ':' + dirname + '/' + self.cref.get_name() + '/' + self.cref.get_arch() + '/' + self.cref.get_branch()
|
||||
# FIXME Getting `error: Exceeded maximum recursion` in ostree if using --depth=-1 (or > 250)
|
||||
subprocess.call(['ostree', 'pull', '--depth=250', '--commit-metadata-only', '--repo', repodir, refname])
|
||||
|
||||
return repodir, refname
|
||||
|
||||
def log(self):
|
||||
if self.data:
|
||||
cmd = ['echo', self.data['log']]
|
||||
else:
|
||||
repodir, refname = self.download_history()
|
||||
cmd = ['ostree', 'log', '--repo', repodir, refname]
|
||||
pager = os.environ.get('PAGER')
|
||||
if pager:
|
||||
stdout = subprocess.PIPE
|
||||
else:
|
||||
stdout = subprocess.STDOUT
|
||||
p = subprocess.Popen(cmd, stdout=stdout)
|
||||
if pager:
|
||||
ps = subprocess.check_call((pager), stdin=p.stdout)
|
||||
p.wait()
|
||||
|
||||
def checkout(self, commit=None):
|
||||
if not commit:
|
||||
commit = self.commit[0]
|
||||
refname = self.cref.get_name() + '/' + self.cref.get_arch() + '/' + self.cref.get_branch()
|
||||
print("Checking out %s" % commit)
|
||||
return subprocess.call(['flatpak', 'update', '--user', refname, '--commit', commit])
|
||||
|
||||
def reset(self, v=True):
|
||||
if not self.data:
|
||||
if v:
|
||||
print("Not bisecting, nothing to reset")
|
||||
return -1
|
||||
|
||||
refname = self.cref.get_name() + '/' + self.cref.get_arch() + '/' + self.cref.get_branch()
|
||||
print("Removing %s" % self.cache_path)
|
||||
os.remove(self.cache_path)
|
||||
self.data = None
|
||||
return subprocess.call(['flatpak', 'update', '--user', refname])
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('name', nargs=1, help='Application/Runtime to bisect')
|
||||
parser.add_argument('-b', '--branch', default='master', help='The branch to bisect')
|
||||
parser.add_argument('-r', '--runtime', action="store_true", help='Bisecting a runtime not an app')
|
||||
|
||||
subparsers = parser.add_subparsers(dest='subparser_name')
|
||||
subparsers.required = True
|
||||
start_parser = subparsers.add_parser('start', help="Start bisection")
|
||||
bad_parser = subparsers.add_parser('bad', help="Set current version as bad")
|
||||
good_parser = subparsers.add_parser('good', help="Set current version as good")
|
||||
log_parser = subparsers.add_parser('log', help="Download and print application commit history")
|
||||
|
||||
checkout_parser = subparsers.add_parser('checkout', help="Checkout defined commit")
|
||||
checkout_parser.add_argument('commit', nargs=1, help='The commit hash to checkout')
|
||||
|
||||
reset_parser = subparsers.add_parser('reset', help="Reset all bisecting data and go back to latest commit")
|
||||
|
||||
bisector = Bisector()
|
||||
options = parser.parse_args(namespace=bisector)
|
||||
bisector.run()
|
Loading…
Reference in New Issue