1*4882a593Smuzhiyun#!/usr/bin/env python3 2*4882a593Smuzhiyun 3*4882a593Smuzhiyun# Development tool - utility functions for plugins 4*4882a593Smuzhiyun# 5*4882a593Smuzhiyun# Copyright (C) 2014 Intel Corporation 6*4882a593Smuzhiyun# 7*4882a593Smuzhiyun# SPDX-License-Identifier: GPL-2.0-only 8*4882a593Smuzhiyun# 9*4882a593Smuzhiyun"""Devtool plugins module""" 10*4882a593Smuzhiyun 11*4882a593Smuzhiyunimport os 12*4882a593Smuzhiyunimport sys 13*4882a593Smuzhiyunimport subprocess 14*4882a593Smuzhiyunimport logging 15*4882a593Smuzhiyunimport re 16*4882a593Smuzhiyunimport codecs 17*4882a593Smuzhiyun 18*4882a593Smuzhiyunlogger = logging.getLogger('devtool') 19*4882a593Smuzhiyun 20*4882a593Smuzhiyunclass DevtoolError(Exception): 21*4882a593Smuzhiyun """Exception for handling devtool errors""" 22*4882a593Smuzhiyun def __init__(self, message, exitcode=1): 23*4882a593Smuzhiyun super(DevtoolError, self).__init__(message) 24*4882a593Smuzhiyun self.exitcode = exitcode 25*4882a593Smuzhiyun 26*4882a593Smuzhiyun 27*4882a593Smuzhiyundef exec_build_env_command(init_path, builddir, cmd, watch=False, **options): 28*4882a593Smuzhiyun """Run a program in bitbake build context""" 29*4882a593Smuzhiyun import bb 30*4882a593Smuzhiyun if not 'cwd' in options: 31*4882a593Smuzhiyun options["cwd"] = builddir 32*4882a593Smuzhiyun if init_path: 33*4882a593Smuzhiyun # As the OE init script makes use of BASH_SOURCE to determine OEROOT, 34*4882a593Smuzhiyun # and can't determine it when running under dash, we need to set 35*4882a593Smuzhiyun # the executable to bash to correctly set things up 36*4882a593Smuzhiyun if not 'executable' in options: 37*4882a593Smuzhiyun options['executable'] = 'bash' 38*4882a593Smuzhiyun logger.debug('Executing command: "%s" using init path %s' % (cmd, init_path)) 39*4882a593Smuzhiyun init_prefix = '. %s %s > /dev/null && ' % (init_path, builddir) 40*4882a593Smuzhiyun else: 41*4882a593Smuzhiyun logger.debug('Executing command "%s"' % cmd) 42*4882a593Smuzhiyun init_prefix = '' 43*4882a593Smuzhiyun if watch: 44*4882a593Smuzhiyun if sys.stdout.isatty(): 45*4882a593Smuzhiyun # Fool bitbake into thinking it's outputting to a terminal (because it is, indirectly) 46*4882a593Smuzhiyun cmd = 'script -e -q -c "%s" /dev/null' % cmd 47*4882a593Smuzhiyun return exec_watch('%s%s' % (init_prefix, cmd), **options) 48*4882a593Smuzhiyun else: 49*4882a593Smuzhiyun return bb.process.run('%s%s' % (init_prefix, cmd), **options) 50*4882a593Smuzhiyun 51*4882a593Smuzhiyundef exec_watch(cmd, **options): 52*4882a593Smuzhiyun """Run program with stdout shown on sys.stdout""" 53*4882a593Smuzhiyun import bb 54*4882a593Smuzhiyun if isinstance(cmd, str) and not "shell" in options: 55*4882a593Smuzhiyun options["shell"] = True 56*4882a593Smuzhiyun 57*4882a593Smuzhiyun process = subprocess.Popen( 58*4882a593Smuzhiyun cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, **options 59*4882a593Smuzhiyun ) 60*4882a593Smuzhiyun 61*4882a593Smuzhiyun reader = codecs.getreader('utf-8')(process.stdout) 62*4882a593Smuzhiyun buf = '' 63*4882a593Smuzhiyun while True: 64*4882a593Smuzhiyun out = reader.read(1, 1) 65*4882a593Smuzhiyun if out: 66*4882a593Smuzhiyun sys.stdout.write(out) 67*4882a593Smuzhiyun sys.stdout.flush() 68*4882a593Smuzhiyun buf += out 69*4882a593Smuzhiyun elif out == '' and process.poll() != None: 70*4882a593Smuzhiyun break 71*4882a593Smuzhiyun 72*4882a593Smuzhiyun if process.returncode != 0: 73*4882a593Smuzhiyun raise bb.process.ExecutionError(cmd, process.returncode, buf, None) 74*4882a593Smuzhiyun 75*4882a593Smuzhiyun return buf, None 76*4882a593Smuzhiyun 77*4882a593Smuzhiyundef exec_fakeroot(d, cmd, **kwargs): 78*4882a593Smuzhiyun """Run a command under fakeroot (pseudo, in fact) so that it picks up the appropriate file permissions""" 79*4882a593Smuzhiyun # Grab the command and check it actually exists 80*4882a593Smuzhiyun fakerootcmd = d.getVar('FAKEROOTCMD') 81*4882a593Smuzhiyun if not os.path.exists(fakerootcmd): 82*4882a593Smuzhiyun logger.error('pseudo executable %s could not be found - have you run a build yet? pseudo-native should install this and if you have run any build then that should have been built') 83*4882a593Smuzhiyun return 2 84*4882a593Smuzhiyun # Set up the appropriate environment 85*4882a593Smuzhiyun newenv = dict(os.environ) 86*4882a593Smuzhiyun fakerootenv = d.getVar('FAKEROOTENV') 87*4882a593Smuzhiyun for varvalue in fakerootenv.split(): 88*4882a593Smuzhiyun if '=' in varvalue: 89*4882a593Smuzhiyun splitval = varvalue.split('=', 1) 90*4882a593Smuzhiyun newenv[splitval[0]] = splitval[1] 91*4882a593Smuzhiyun return subprocess.call("%s %s" % (fakerootcmd, cmd), env=newenv, **kwargs) 92*4882a593Smuzhiyun 93*4882a593Smuzhiyundef setup_tinfoil(config_only=False, basepath=None, tracking=False): 94*4882a593Smuzhiyun """Initialize tinfoil api from bitbake""" 95*4882a593Smuzhiyun import scriptpath 96*4882a593Smuzhiyun orig_cwd = os.path.abspath(os.curdir) 97*4882a593Smuzhiyun try: 98*4882a593Smuzhiyun if basepath: 99*4882a593Smuzhiyun os.chdir(basepath) 100*4882a593Smuzhiyun bitbakepath = scriptpath.add_bitbake_lib_path() 101*4882a593Smuzhiyun if not bitbakepath: 102*4882a593Smuzhiyun logger.error("Unable to find bitbake by searching parent directory of this script or PATH") 103*4882a593Smuzhiyun sys.exit(1) 104*4882a593Smuzhiyun 105*4882a593Smuzhiyun import bb.tinfoil 106*4882a593Smuzhiyun tinfoil = bb.tinfoil.Tinfoil(tracking=tracking) 107*4882a593Smuzhiyun try: 108*4882a593Smuzhiyun tinfoil.logger.setLevel(logger.getEffectiveLevel()) 109*4882a593Smuzhiyun tinfoil.prepare(config_only) 110*4882a593Smuzhiyun except bb.tinfoil.TinfoilUIException: 111*4882a593Smuzhiyun tinfoil.shutdown() 112*4882a593Smuzhiyun raise DevtoolError('Failed to start bitbake environment') 113*4882a593Smuzhiyun except: 114*4882a593Smuzhiyun tinfoil.shutdown() 115*4882a593Smuzhiyun raise 116*4882a593Smuzhiyun finally: 117*4882a593Smuzhiyun os.chdir(orig_cwd) 118*4882a593Smuzhiyun return tinfoil 119*4882a593Smuzhiyun 120*4882a593Smuzhiyundef parse_recipe(config, tinfoil, pn, appends, filter_workspace=True): 121*4882a593Smuzhiyun """Parse the specified recipe""" 122*4882a593Smuzhiyun try: 123*4882a593Smuzhiyun recipefile = tinfoil.get_recipe_file(pn) 124*4882a593Smuzhiyun except bb.providers.NoProvider as e: 125*4882a593Smuzhiyun logger.error(str(e)) 126*4882a593Smuzhiyun return None 127*4882a593Smuzhiyun if appends: 128*4882a593Smuzhiyun append_files = tinfoil.get_file_appends(recipefile) 129*4882a593Smuzhiyun if filter_workspace: 130*4882a593Smuzhiyun # Filter out appends from the workspace 131*4882a593Smuzhiyun append_files = [path for path in append_files if 132*4882a593Smuzhiyun not path.startswith(config.workspace_path)] 133*4882a593Smuzhiyun else: 134*4882a593Smuzhiyun append_files = None 135*4882a593Smuzhiyun try: 136*4882a593Smuzhiyun rd = tinfoil.parse_recipe_file(recipefile, appends, append_files) 137*4882a593Smuzhiyun except Exception as e: 138*4882a593Smuzhiyun logger.error(str(e)) 139*4882a593Smuzhiyun return None 140*4882a593Smuzhiyun return rd 141*4882a593Smuzhiyun 142*4882a593Smuzhiyundef check_workspace_recipe(workspace, pn, checksrc=True, bbclassextend=False): 143*4882a593Smuzhiyun """ 144*4882a593Smuzhiyun Check that a recipe is in the workspace and (optionally) that source 145*4882a593Smuzhiyun is present. 146*4882a593Smuzhiyun """ 147*4882a593Smuzhiyun 148*4882a593Smuzhiyun workspacepn = pn 149*4882a593Smuzhiyun 150*4882a593Smuzhiyun for recipe, value in workspace.items(): 151*4882a593Smuzhiyun if recipe == pn: 152*4882a593Smuzhiyun break 153*4882a593Smuzhiyun if bbclassextend: 154*4882a593Smuzhiyun recipefile = value['recipefile'] 155*4882a593Smuzhiyun if recipefile: 156*4882a593Smuzhiyun targets = get_bbclassextend_targets(recipefile, recipe) 157*4882a593Smuzhiyun if pn in targets: 158*4882a593Smuzhiyun workspacepn = recipe 159*4882a593Smuzhiyun break 160*4882a593Smuzhiyun else: 161*4882a593Smuzhiyun raise DevtoolError("No recipe named '%s' in your workspace" % pn) 162*4882a593Smuzhiyun 163*4882a593Smuzhiyun if checksrc: 164*4882a593Smuzhiyun srctree = workspace[workspacepn]['srctree'] 165*4882a593Smuzhiyun if not os.path.exists(srctree): 166*4882a593Smuzhiyun raise DevtoolError("Source tree %s for recipe %s does not exist" % (srctree, workspacepn)) 167*4882a593Smuzhiyun if not os.listdir(srctree): 168*4882a593Smuzhiyun raise DevtoolError("Source tree %s for recipe %s is empty" % (srctree, workspacepn)) 169*4882a593Smuzhiyun 170*4882a593Smuzhiyun return workspacepn 171*4882a593Smuzhiyun 172*4882a593Smuzhiyundef use_external_build(same_dir, no_same_dir, d): 173*4882a593Smuzhiyun """ 174*4882a593Smuzhiyun Determine if we should use B!=S (separate build and source directories) or not 175*4882a593Smuzhiyun """ 176*4882a593Smuzhiyun b_is_s = True 177*4882a593Smuzhiyun if no_same_dir: 178*4882a593Smuzhiyun logger.info('Using separate build directory since --no-same-dir specified') 179*4882a593Smuzhiyun b_is_s = False 180*4882a593Smuzhiyun elif same_dir: 181*4882a593Smuzhiyun logger.info('Using source tree as build directory since --same-dir specified') 182*4882a593Smuzhiyun elif bb.data.inherits_class('autotools-brokensep', d): 183*4882a593Smuzhiyun logger.info('Using source tree as build directory since recipe inherits autotools-brokensep') 184*4882a593Smuzhiyun elif os.path.abspath(d.getVar('B')) == os.path.abspath(d.getVar('S')): 185*4882a593Smuzhiyun logger.info('Using source tree as build directory since that would be the default for this recipe') 186*4882a593Smuzhiyun else: 187*4882a593Smuzhiyun b_is_s = False 188*4882a593Smuzhiyun return b_is_s 189*4882a593Smuzhiyun 190*4882a593Smuzhiyundef setup_git_repo(repodir, version, devbranch, basetag='devtool-base', d=None): 191*4882a593Smuzhiyun """ 192*4882a593Smuzhiyun Set up the git repository for the source tree 193*4882a593Smuzhiyun """ 194*4882a593Smuzhiyun import bb.process 195*4882a593Smuzhiyun import oe.patch 196*4882a593Smuzhiyun if not os.path.exists(os.path.join(repodir, '.git')): 197*4882a593Smuzhiyun bb.process.run('git init', cwd=repodir) 198*4882a593Smuzhiyun bb.process.run('git config --local gc.autodetach 0', cwd=repodir) 199*4882a593Smuzhiyun bb.process.run('git add -f -A .', cwd=repodir) 200*4882a593Smuzhiyun commit_cmd = ['git'] 201*4882a593Smuzhiyun oe.patch.GitApplyTree.gitCommandUserOptions(commit_cmd, d=d) 202*4882a593Smuzhiyun commit_cmd += ['commit', '-q'] 203*4882a593Smuzhiyun stdout, _ = bb.process.run('git status --porcelain', cwd=repodir) 204*4882a593Smuzhiyun if not stdout: 205*4882a593Smuzhiyun commit_cmd.append('--allow-empty') 206*4882a593Smuzhiyun commitmsg = "Initial empty commit with no upstream sources" 207*4882a593Smuzhiyun elif version: 208*4882a593Smuzhiyun commitmsg = "Initial commit from upstream at version %s" % version 209*4882a593Smuzhiyun else: 210*4882a593Smuzhiyun commitmsg = "Initial commit from upstream" 211*4882a593Smuzhiyun commit_cmd += ['-m', commitmsg] 212*4882a593Smuzhiyun bb.process.run(commit_cmd, cwd=repodir) 213*4882a593Smuzhiyun 214*4882a593Smuzhiyun # Ensure singletask.lock (as used by externalsrc.bbclass) is ignored by git 215*4882a593Smuzhiyun gitinfodir = os.path.join(repodir, '.git', 'info') 216*4882a593Smuzhiyun try: 217*4882a593Smuzhiyun os.mkdir(gitinfodir) 218*4882a593Smuzhiyun except FileExistsError: 219*4882a593Smuzhiyun pass 220*4882a593Smuzhiyun excludes = [] 221*4882a593Smuzhiyun excludefile = os.path.join(gitinfodir, 'exclude') 222*4882a593Smuzhiyun try: 223*4882a593Smuzhiyun with open(excludefile, 'r') as f: 224*4882a593Smuzhiyun excludes = f.readlines() 225*4882a593Smuzhiyun except FileNotFoundError: 226*4882a593Smuzhiyun pass 227*4882a593Smuzhiyun if 'singletask.lock\n' not in excludes: 228*4882a593Smuzhiyun excludes.append('singletask.lock\n') 229*4882a593Smuzhiyun with open(excludefile, 'w') as f: 230*4882a593Smuzhiyun for line in excludes: 231*4882a593Smuzhiyun f.write(line) 232*4882a593Smuzhiyun 233*4882a593Smuzhiyun bb.process.run('git checkout -b %s' % devbranch, cwd=repodir) 234*4882a593Smuzhiyun bb.process.run('git tag -f %s' % basetag, cwd=repodir) 235*4882a593Smuzhiyun 236*4882a593Smuzhiyundef recipe_to_append(recipefile, config, wildcard=False): 237*4882a593Smuzhiyun """ 238*4882a593Smuzhiyun Convert a recipe file to a bbappend file path within the workspace. 239*4882a593Smuzhiyun NOTE: if the bbappend already exists, you should be using 240*4882a593Smuzhiyun workspace[args.recipename]['bbappend'] instead of calling this 241*4882a593Smuzhiyun function. 242*4882a593Smuzhiyun """ 243*4882a593Smuzhiyun appendname = os.path.splitext(os.path.basename(recipefile))[0] 244*4882a593Smuzhiyun if wildcard: 245*4882a593Smuzhiyun appendname = re.sub(r'_.*', '_%', appendname) 246*4882a593Smuzhiyun appendpath = os.path.join(config.workspace_path, 'appends') 247*4882a593Smuzhiyun appendfile = os.path.join(appendpath, appendname + '.bbappend') 248*4882a593Smuzhiyun return appendfile 249*4882a593Smuzhiyun 250*4882a593Smuzhiyundef get_bbclassextend_targets(recipefile, pn): 251*4882a593Smuzhiyun """ 252*4882a593Smuzhiyun Cheap function to get BBCLASSEXTEND and then convert that to the 253*4882a593Smuzhiyun list of targets that would result. 254*4882a593Smuzhiyun """ 255*4882a593Smuzhiyun import bb.utils 256*4882a593Smuzhiyun 257*4882a593Smuzhiyun values = {} 258*4882a593Smuzhiyun def get_bbclassextend_varfunc(varname, origvalue, op, newlines): 259*4882a593Smuzhiyun values[varname] = origvalue 260*4882a593Smuzhiyun return origvalue, None, 0, True 261*4882a593Smuzhiyun with open(recipefile, 'r') as f: 262*4882a593Smuzhiyun bb.utils.edit_metadata(f, ['BBCLASSEXTEND'], get_bbclassextend_varfunc) 263*4882a593Smuzhiyun 264*4882a593Smuzhiyun targets = [] 265*4882a593Smuzhiyun bbclassextend = values.get('BBCLASSEXTEND', '').split() 266*4882a593Smuzhiyun if bbclassextend: 267*4882a593Smuzhiyun for variant in bbclassextend: 268*4882a593Smuzhiyun if variant == 'nativesdk': 269*4882a593Smuzhiyun targets.append('%s-%s' % (variant, pn)) 270*4882a593Smuzhiyun elif variant in ['native', 'cross', 'crosssdk']: 271*4882a593Smuzhiyun targets.append('%s-%s' % (pn, variant)) 272*4882a593Smuzhiyun return targets 273*4882a593Smuzhiyun 274*4882a593Smuzhiyundef replace_from_file(path, old, new): 275*4882a593Smuzhiyun """Replace strings on a file""" 276*4882a593Smuzhiyun 277*4882a593Smuzhiyun def read_file(path): 278*4882a593Smuzhiyun data = None 279*4882a593Smuzhiyun with open(path) as f: 280*4882a593Smuzhiyun data = f.read() 281*4882a593Smuzhiyun return data 282*4882a593Smuzhiyun 283*4882a593Smuzhiyun def write_file(path, data): 284*4882a593Smuzhiyun if data is None: 285*4882a593Smuzhiyun return 286*4882a593Smuzhiyun wdata = data.rstrip() + "\n" 287*4882a593Smuzhiyun with open(path, "w") as f: 288*4882a593Smuzhiyun f.write(wdata) 289*4882a593Smuzhiyun 290*4882a593Smuzhiyun # In case old is None, return immediately 291*4882a593Smuzhiyun if old is None: 292*4882a593Smuzhiyun return 293*4882a593Smuzhiyun try: 294*4882a593Smuzhiyun rdata = read_file(path) 295*4882a593Smuzhiyun except IOError as e: 296*4882a593Smuzhiyun # if file does not exit, just quit, otherwise raise an exception 297*4882a593Smuzhiyun if e.errno == errno.ENOENT: 298*4882a593Smuzhiyun return 299*4882a593Smuzhiyun else: 300*4882a593Smuzhiyun raise 301*4882a593Smuzhiyun 302*4882a593Smuzhiyun old_contents = rdata.splitlines() 303*4882a593Smuzhiyun new_contents = [] 304*4882a593Smuzhiyun for old_content in old_contents: 305*4882a593Smuzhiyun try: 306*4882a593Smuzhiyun new_contents.append(old_content.replace(old, new)) 307*4882a593Smuzhiyun except ValueError: 308*4882a593Smuzhiyun pass 309*4882a593Smuzhiyun write_file(path, "\n".join(new_contents)) 310*4882a593Smuzhiyun 311*4882a593Smuzhiyun 312*4882a593Smuzhiyundef update_unlockedsigs(basepath, workspace, fixed_setup, extra=None): 313*4882a593Smuzhiyun """ This function will make unlocked-sigs.inc match the recipes in the 314*4882a593Smuzhiyun workspace plus any extras we want unlocked. """ 315*4882a593Smuzhiyun 316*4882a593Smuzhiyun if not fixed_setup: 317*4882a593Smuzhiyun # Only need to write this out within the eSDK 318*4882a593Smuzhiyun return 319*4882a593Smuzhiyun 320*4882a593Smuzhiyun if not extra: 321*4882a593Smuzhiyun extra = [] 322*4882a593Smuzhiyun 323*4882a593Smuzhiyun confdir = os.path.join(basepath, 'conf') 324*4882a593Smuzhiyun unlockedsigs = os.path.join(confdir, 'unlocked-sigs.inc') 325*4882a593Smuzhiyun 326*4882a593Smuzhiyun # Get current unlocked list if any 327*4882a593Smuzhiyun values = {} 328*4882a593Smuzhiyun def get_unlockedsigs_varfunc(varname, origvalue, op, newlines): 329*4882a593Smuzhiyun values[varname] = origvalue 330*4882a593Smuzhiyun return origvalue, None, 0, True 331*4882a593Smuzhiyun if os.path.exists(unlockedsigs): 332*4882a593Smuzhiyun with open(unlockedsigs, 'r') as f: 333*4882a593Smuzhiyun bb.utils.edit_metadata(f, ['SIGGEN_UNLOCKED_RECIPES'], get_unlockedsigs_varfunc) 334*4882a593Smuzhiyun unlocked = sorted(values.get('SIGGEN_UNLOCKED_RECIPES', [])) 335*4882a593Smuzhiyun 336*4882a593Smuzhiyun # If the new list is different to the current list, write it out 337*4882a593Smuzhiyun newunlocked = sorted(list(workspace.keys()) + extra) 338*4882a593Smuzhiyun if unlocked != newunlocked: 339*4882a593Smuzhiyun bb.utils.mkdirhier(confdir) 340*4882a593Smuzhiyun with open(unlockedsigs, 'w') as f: 341*4882a593Smuzhiyun f.write("# DO NOT MODIFY! YOUR CHANGES WILL BE LOST.\n" + 342*4882a593Smuzhiyun "# This layer was created by the OpenEmbedded devtool" + 343*4882a593Smuzhiyun " utility in order to\n" + 344*4882a593Smuzhiyun "# contain recipes that are unlocked.\n") 345*4882a593Smuzhiyun 346*4882a593Smuzhiyun f.write('SIGGEN_UNLOCKED_RECIPES += "\\\n') 347*4882a593Smuzhiyun for pn in newunlocked: 348*4882a593Smuzhiyun f.write(' ' + pn) 349*4882a593Smuzhiyun f.write('"') 350*4882a593Smuzhiyun 351*4882a593Smuzhiyundef check_prerelease_version(ver, operation): 352*4882a593Smuzhiyun if 'pre' in ver or 'rc' in ver: 353*4882a593Smuzhiyun logger.warning('Version "%s" looks like a pre-release version. ' 354*4882a593Smuzhiyun 'If that is the case, in order to ensure that the ' 355*4882a593Smuzhiyun 'version doesn\'t appear to go backwards when you ' 356*4882a593Smuzhiyun 'later upgrade to the final release version, it is ' 357*4882a593Smuzhiyun 'recommmended that instead you use ' 358*4882a593Smuzhiyun '<current version>+<pre-release version> e.g. if ' 359*4882a593Smuzhiyun 'upgrading from 1.9 to 2.0-rc2 use "1.9+2.0-rc2". ' 360*4882a593Smuzhiyun 'If you prefer not to reset and re-try, you can change ' 361*4882a593Smuzhiyun 'the version after %s succeeds using "devtool rename" ' 362*4882a593Smuzhiyun 'with -V/--version.' % (ver, operation)) 363*4882a593Smuzhiyun 364*4882a593Smuzhiyundef check_git_repo_dirty(repodir): 365*4882a593Smuzhiyun """Check if a git repository is clean or not""" 366*4882a593Smuzhiyun stdout, _ = bb.process.run('git status --porcelain', cwd=repodir) 367*4882a593Smuzhiyun return stdout 368*4882a593Smuzhiyun 369*4882a593Smuzhiyundef check_git_repo_op(srctree, ignoredirs=None): 370*4882a593Smuzhiyun """Check if a git repository is in the middle of a rebase""" 371*4882a593Smuzhiyun stdout, _ = bb.process.run('git rev-parse --show-toplevel', cwd=srctree) 372*4882a593Smuzhiyun topleveldir = stdout.strip() 373*4882a593Smuzhiyun if ignoredirs and topleveldir in ignoredirs: 374*4882a593Smuzhiyun return 375*4882a593Smuzhiyun gitdir = os.path.join(topleveldir, '.git') 376*4882a593Smuzhiyun if os.path.exists(os.path.join(gitdir, 'rebase-merge')): 377*4882a593Smuzhiyun raise DevtoolError("Source tree %s appears to be in the middle of a rebase - please resolve this first" % srctree) 378*4882a593Smuzhiyun if os.path.exists(os.path.join(gitdir, 'rebase-apply')): 379*4882a593Smuzhiyun raise DevtoolError("Source tree %s appears to be in the middle of 'git am' or 'git apply' - please resolve this first" % srctree) 380