1*4882a593Smuzhiyun# Script utility functions 2*4882a593Smuzhiyun# 3*4882a593Smuzhiyun# Copyright (C) 2014 Intel Corporation 4*4882a593Smuzhiyun# 5*4882a593Smuzhiyun# SPDX-License-Identifier: GPL-2.0-only 6*4882a593Smuzhiyun# 7*4882a593Smuzhiyun 8*4882a593Smuzhiyunimport argparse 9*4882a593Smuzhiyunimport glob 10*4882a593Smuzhiyunimport logging 11*4882a593Smuzhiyunimport os 12*4882a593Smuzhiyunimport random 13*4882a593Smuzhiyunimport shlex 14*4882a593Smuzhiyunimport shutil 15*4882a593Smuzhiyunimport string 16*4882a593Smuzhiyunimport subprocess 17*4882a593Smuzhiyunimport sys 18*4882a593Smuzhiyunimport tempfile 19*4882a593Smuzhiyunimport threading 20*4882a593Smuzhiyunimport importlib 21*4882a593Smuzhiyunimport importlib.machinery 22*4882a593Smuzhiyunimport importlib.util 23*4882a593Smuzhiyun 24*4882a593Smuzhiyunclass KeepAliveStreamHandler(logging.StreamHandler): 25*4882a593Smuzhiyun def __init__(self, keepalive=True, **kwargs): 26*4882a593Smuzhiyun super().__init__(**kwargs) 27*4882a593Smuzhiyun if keepalive is True: 28*4882a593Smuzhiyun keepalive = 5000 # default timeout 29*4882a593Smuzhiyun self._timeout = threading.Condition() 30*4882a593Smuzhiyun self._stop = False 31*4882a593Smuzhiyun 32*4882a593Smuzhiyun # background thread waits on condition, if the condition does not 33*4882a593Smuzhiyun # happen emit a keep alive message 34*4882a593Smuzhiyun def thread(): 35*4882a593Smuzhiyun while not self._stop: 36*4882a593Smuzhiyun with self._timeout: 37*4882a593Smuzhiyun if not self._timeout.wait(keepalive): 38*4882a593Smuzhiyun self.emit(logging.LogRecord("keepalive", logging.INFO, 39*4882a593Smuzhiyun None, None, "Keepalive message", None, None)) 40*4882a593Smuzhiyun 41*4882a593Smuzhiyun self._thread = threading.Thread(target = thread, daemon = True) 42*4882a593Smuzhiyun self._thread.start() 43*4882a593Smuzhiyun 44*4882a593Smuzhiyun def close(self): 45*4882a593Smuzhiyun # mark the thread to stop and notify it 46*4882a593Smuzhiyun self._stop = True 47*4882a593Smuzhiyun with self._timeout: 48*4882a593Smuzhiyun self._timeout.notify() 49*4882a593Smuzhiyun # wait for it to join 50*4882a593Smuzhiyun self._thread.join() 51*4882a593Smuzhiyun super().close() 52*4882a593Smuzhiyun 53*4882a593Smuzhiyun def emit(self, record): 54*4882a593Smuzhiyun super().emit(record) 55*4882a593Smuzhiyun # trigger timer reset 56*4882a593Smuzhiyun with self._timeout: 57*4882a593Smuzhiyun self._timeout.notify() 58*4882a593Smuzhiyun 59*4882a593Smuzhiyundef logger_create(name, stream=None, keepalive=None): 60*4882a593Smuzhiyun logger = logging.getLogger(name) 61*4882a593Smuzhiyun if keepalive is not None: 62*4882a593Smuzhiyun loggerhandler = KeepAliveStreamHandler(stream=stream, keepalive=keepalive) 63*4882a593Smuzhiyun else: 64*4882a593Smuzhiyun loggerhandler = logging.StreamHandler(stream=stream) 65*4882a593Smuzhiyun loggerhandler.setFormatter(logging.Formatter("%(levelname)s: %(message)s")) 66*4882a593Smuzhiyun logger.addHandler(loggerhandler) 67*4882a593Smuzhiyun logger.setLevel(logging.INFO) 68*4882a593Smuzhiyun return logger 69*4882a593Smuzhiyun 70*4882a593Smuzhiyundef logger_setup_color(logger, color='auto'): 71*4882a593Smuzhiyun from bb.msg import BBLogFormatter 72*4882a593Smuzhiyun 73*4882a593Smuzhiyun for handler in logger.handlers: 74*4882a593Smuzhiyun if (isinstance(handler, logging.StreamHandler) and 75*4882a593Smuzhiyun isinstance(handler.formatter, BBLogFormatter)): 76*4882a593Smuzhiyun if color == 'always' or (color == 'auto' and handler.stream.isatty()): 77*4882a593Smuzhiyun handler.formatter.enable_color() 78*4882a593Smuzhiyun 79*4882a593Smuzhiyun 80*4882a593Smuzhiyundef load_plugins(logger, plugins, pluginpath): 81*4882a593Smuzhiyun 82*4882a593Smuzhiyun def load_plugin(name): 83*4882a593Smuzhiyun logger.debug('Loading plugin %s' % name) 84*4882a593Smuzhiyun spec = importlib.machinery.PathFinder.find_spec(name, path=[pluginpath] ) 85*4882a593Smuzhiyun if spec: 86*4882a593Smuzhiyun mod = importlib.util.module_from_spec(spec) 87*4882a593Smuzhiyun spec.loader.exec_module(mod) 88*4882a593Smuzhiyun return mod 89*4882a593Smuzhiyun 90*4882a593Smuzhiyun def plugin_name(filename): 91*4882a593Smuzhiyun return os.path.splitext(os.path.basename(filename))[0] 92*4882a593Smuzhiyun 93*4882a593Smuzhiyun known_plugins = [plugin_name(p.__name__) for p in plugins] 94*4882a593Smuzhiyun logger.debug('Loading plugins from %s...' % pluginpath) 95*4882a593Smuzhiyun for fn in glob.glob(os.path.join(pluginpath, '*.py')): 96*4882a593Smuzhiyun name = plugin_name(fn) 97*4882a593Smuzhiyun if name != '__init__' and name not in known_plugins: 98*4882a593Smuzhiyun plugin = load_plugin(name) 99*4882a593Smuzhiyun if hasattr(plugin, 'plugin_init'): 100*4882a593Smuzhiyun plugin.plugin_init(plugins) 101*4882a593Smuzhiyun plugins.append(plugin) 102*4882a593Smuzhiyun 103*4882a593Smuzhiyun 104*4882a593Smuzhiyundef git_convert_standalone_clone(repodir): 105*4882a593Smuzhiyun """If specified directory is a git repository, ensure it's a standalone clone""" 106*4882a593Smuzhiyun import bb.process 107*4882a593Smuzhiyun if os.path.exists(os.path.join(repodir, '.git')): 108*4882a593Smuzhiyun alternatesfile = os.path.join(repodir, '.git', 'objects', 'info', 'alternates') 109*4882a593Smuzhiyun if os.path.exists(alternatesfile): 110*4882a593Smuzhiyun # This will have been cloned with -s, so we need to convert it so none 111*4882a593Smuzhiyun # of the contents is shared 112*4882a593Smuzhiyun bb.process.run('git repack -a', cwd=repodir) 113*4882a593Smuzhiyun os.remove(alternatesfile) 114*4882a593Smuzhiyun 115*4882a593Smuzhiyundef _get_temp_recipe_dir(d): 116*4882a593Smuzhiyun # This is a little bit hacky but we need to find a place where we can put 117*4882a593Smuzhiyun # the recipe so that bitbake can find it. We're going to delete it at the 118*4882a593Smuzhiyun # end so it doesn't really matter where we put it. 119*4882a593Smuzhiyun bbfiles = d.getVar('BBFILES').split() 120*4882a593Smuzhiyun fetchrecipedir = None 121*4882a593Smuzhiyun for pth in bbfiles: 122*4882a593Smuzhiyun if pth.endswith('.bb'): 123*4882a593Smuzhiyun pthdir = os.path.dirname(pth) 124*4882a593Smuzhiyun if os.access(os.path.dirname(os.path.dirname(pthdir)), os.W_OK): 125*4882a593Smuzhiyun fetchrecipedir = pthdir.replace('*', 'recipetool') 126*4882a593Smuzhiyun if pthdir.endswith('workspace/recipes/*'): 127*4882a593Smuzhiyun # Prefer the workspace 128*4882a593Smuzhiyun break 129*4882a593Smuzhiyun return fetchrecipedir 130*4882a593Smuzhiyun 131*4882a593Smuzhiyunclass FetchUrlFailure(Exception): 132*4882a593Smuzhiyun def __init__(self, url): 133*4882a593Smuzhiyun self.url = url 134*4882a593Smuzhiyun def __str__(self): 135*4882a593Smuzhiyun return "Failed to fetch URL %s" % self.url 136*4882a593Smuzhiyun 137*4882a593Smuzhiyundef fetch_url(tinfoil, srcuri, srcrev, destdir, logger, preserve_tmp=False, mirrors=False): 138*4882a593Smuzhiyun """ 139*4882a593Smuzhiyun Fetch the specified URL using normal do_fetch and do_unpack tasks, i.e. 140*4882a593Smuzhiyun any dependencies that need to be satisfied in order to support the fetch 141*4882a593Smuzhiyun operation will be taken care of 142*4882a593Smuzhiyun """ 143*4882a593Smuzhiyun 144*4882a593Smuzhiyun import bb 145*4882a593Smuzhiyun 146*4882a593Smuzhiyun checksums = {} 147*4882a593Smuzhiyun fetchrecipepn = None 148*4882a593Smuzhiyun 149*4882a593Smuzhiyun # We need to put our temp directory under ${BASE_WORKDIR} otherwise 150*4882a593Smuzhiyun # we may have problems with the recipe-specific sysroot population 151*4882a593Smuzhiyun tmpparent = tinfoil.config_data.getVar('BASE_WORKDIR') 152*4882a593Smuzhiyun bb.utils.mkdirhier(tmpparent) 153*4882a593Smuzhiyun tmpdir = tempfile.mkdtemp(prefix='recipetool-', dir=tmpparent) 154*4882a593Smuzhiyun try: 155*4882a593Smuzhiyun tmpworkdir = os.path.join(tmpdir, 'work') 156*4882a593Smuzhiyun logger.debug('fetch_url: temp dir is %s' % tmpdir) 157*4882a593Smuzhiyun 158*4882a593Smuzhiyun fetchrecipedir = _get_temp_recipe_dir(tinfoil.config_data) 159*4882a593Smuzhiyun if not fetchrecipedir: 160*4882a593Smuzhiyun logger.error('Searched BBFILES but unable to find a writeable place to put temporary recipe') 161*4882a593Smuzhiyun sys.exit(1) 162*4882a593Smuzhiyun fetchrecipe = None 163*4882a593Smuzhiyun bb.utils.mkdirhier(fetchrecipedir) 164*4882a593Smuzhiyun try: 165*4882a593Smuzhiyun # Generate a dummy recipe so we can follow more or less normal paths 166*4882a593Smuzhiyun # for do_fetch and do_unpack 167*4882a593Smuzhiyun # I'd use tempfile functions here but underscores can be produced by that and those 168*4882a593Smuzhiyun # aren't allowed in recipe file names except to separate the version 169*4882a593Smuzhiyun rndstring = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(8)) 170*4882a593Smuzhiyun fetchrecipe = os.path.join(fetchrecipedir, 'tmp-recipetool-%s.bb' % rndstring) 171*4882a593Smuzhiyun fetchrecipepn = os.path.splitext(os.path.basename(fetchrecipe))[0] 172*4882a593Smuzhiyun logger.debug('Generating initial recipe %s for fetching' % fetchrecipe) 173*4882a593Smuzhiyun with open(fetchrecipe, 'w') as f: 174*4882a593Smuzhiyun # We don't want to have to specify LIC_FILES_CHKSUM 175*4882a593Smuzhiyun f.write('LICENSE = "CLOSED"\n') 176*4882a593Smuzhiyun # We don't need the cross-compiler 177*4882a593Smuzhiyun f.write('INHIBIT_DEFAULT_DEPS = "1"\n') 178*4882a593Smuzhiyun # We don't have the checksums yet so we can't require them 179*4882a593Smuzhiyun f.write('BB_STRICT_CHECKSUM = "ignore"\n') 180*4882a593Smuzhiyun f.write('SRC_URI = "%s"\n' % srcuri) 181*4882a593Smuzhiyun f.write('SRCREV = "%s"\n' % srcrev) 182*4882a593Smuzhiyun f.write('PV = "0.0+${SRCPV}"\n') 183*4882a593Smuzhiyun f.write('WORKDIR = "%s"\n' % tmpworkdir) 184*4882a593Smuzhiyun # Set S out of the way so it doesn't get created under the workdir 185*4882a593Smuzhiyun f.write('S = "%s"\n' % os.path.join(tmpdir, 'emptysrc')) 186*4882a593Smuzhiyun if not mirrors: 187*4882a593Smuzhiyun # We do not need PREMIRRORS since we are almost certainly 188*4882a593Smuzhiyun # fetching new source rather than something that has already 189*4882a593Smuzhiyun # been fetched. Hence, we disable them by default. 190*4882a593Smuzhiyun # However, we provide an option for users to enable it. 191*4882a593Smuzhiyun f.write('PREMIRRORS = ""\n') 192*4882a593Smuzhiyun f.write('MIRRORS = ""\n') 193*4882a593Smuzhiyun 194*4882a593Smuzhiyun logger.info('Fetching %s...' % srcuri) 195*4882a593Smuzhiyun 196*4882a593Smuzhiyun # FIXME this is too noisy at the moment 197*4882a593Smuzhiyun 198*4882a593Smuzhiyun # Parse recipes so our new recipe gets picked up 199*4882a593Smuzhiyun tinfoil.parse_recipes() 200*4882a593Smuzhiyun 201*4882a593Smuzhiyun def eventhandler(event): 202*4882a593Smuzhiyun if isinstance(event, bb.fetch2.MissingChecksumEvent): 203*4882a593Smuzhiyun checksums.update(event.checksums) 204*4882a593Smuzhiyun return True 205*4882a593Smuzhiyun return False 206*4882a593Smuzhiyun 207*4882a593Smuzhiyun # Run the fetch + unpack tasks 208*4882a593Smuzhiyun res = tinfoil.build_targets(fetchrecipepn, 209*4882a593Smuzhiyun 'do_unpack', 210*4882a593Smuzhiyun handle_events=True, 211*4882a593Smuzhiyun extra_events=['bb.fetch2.MissingChecksumEvent'], 212*4882a593Smuzhiyun event_callback=eventhandler) 213*4882a593Smuzhiyun if not res: 214*4882a593Smuzhiyun raise FetchUrlFailure(srcuri) 215*4882a593Smuzhiyun 216*4882a593Smuzhiyun # Remove unneeded directories 217*4882a593Smuzhiyun rd = tinfoil.parse_recipe(fetchrecipepn) 218*4882a593Smuzhiyun if rd: 219*4882a593Smuzhiyun pathvars = ['T', 'RECIPE_SYSROOT', 'RECIPE_SYSROOT_NATIVE'] 220*4882a593Smuzhiyun for pathvar in pathvars: 221*4882a593Smuzhiyun path = rd.getVar(pathvar) 222*4882a593Smuzhiyun if os.path.exists(path): 223*4882a593Smuzhiyun shutil.rmtree(path) 224*4882a593Smuzhiyun finally: 225*4882a593Smuzhiyun if fetchrecipe: 226*4882a593Smuzhiyun try: 227*4882a593Smuzhiyun os.remove(fetchrecipe) 228*4882a593Smuzhiyun except FileNotFoundError: 229*4882a593Smuzhiyun pass 230*4882a593Smuzhiyun try: 231*4882a593Smuzhiyun os.rmdir(fetchrecipedir) 232*4882a593Smuzhiyun except OSError as e: 233*4882a593Smuzhiyun import errno 234*4882a593Smuzhiyun if e.errno != errno.ENOTEMPTY: 235*4882a593Smuzhiyun raise 236*4882a593Smuzhiyun 237*4882a593Smuzhiyun bb.utils.mkdirhier(destdir) 238*4882a593Smuzhiyun for fn in os.listdir(tmpworkdir): 239*4882a593Smuzhiyun shutil.move(os.path.join(tmpworkdir, fn), destdir) 240*4882a593Smuzhiyun 241*4882a593Smuzhiyun finally: 242*4882a593Smuzhiyun if not preserve_tmp: 243*4882a593Smuzhiyun shutil.rmtree(tmpdir) 244*4882a593Smuzhiyun tmpdir = None 245*4882a593Smuzhiyun 246*4882a593Smuzhiyun return checksums, tmpdir 247*4882a593Smuzhiyun 248*4882a593Smuzhiyun 249*4882a593Smuzhiyundef run_editor(fn, logger=None): 250*4882a593Smuzhiyun if isinstance(fn, str): 251*4882a593Smuzhiyun files = [fn] 252*4882a593Smuzhiyun else: 253*4882a593Smuzhiyun files = fn 254*4882a593Smuzhiyun 255*4882a593Smuzhiyun editor = os.getenv('VISUAL', os.getenv('EDITOR', 'vi')) 256*4882a593Smuzhiyun try: 257*4882a593Smuzhiyun #print(shlex.split(editor) + files) 258*4882a593Smuzhiyun return subprocess.check_call(shlex.split(editor) + files) 259*4882a593Smuzhiyun except subprocess.CalledProcessError as exc: 260*4882a593Smuzhiyun logger.error("Execution of '%s' failed: %s" % (editor, exc)) 261*4882a593Smuzhiyun return 1 262*4882a593Smuzhiyun 263*4882a593Smuzhiyundef is_src_url(param): 264*4882a593Smuzhiyun """ 265*4882a593Smuzhiyun Check if a parameter is a URL and return True if so 266*4882a593Smuzhiyun NOTE: be careful about changing this as it will influence how devtool/recipetool command line handling works 267*4882a593Smuzhiyun """ 268*4882a593Smuzhiyun if not param: 269*4882a593Smuzhiyun return False 270*4882a593Smuzhiyun elif '://' in param: 271*4882a593Smuzhiyun return True 272*4882a593Smuzhiyun elif param.startswith('git@') or ('@' in param and param.endswith('.git')): 273*4882a593Smuzhiyun return True 274*4882a593Smuzhiyun return False 275*4882a593Smuzhiyun 276*4882a593Smuzhiyundef filter_src_subdirs(pth): 277*4882a593Smuzhiyun """ 278*4882a593Smuzhiyun Filter out subdirectories of initial unpacked source trees that we do not care about. 279*4882a593Smuzhiyun Used by devtool and recipetool. 280*4882a593Smuzhiyun """ 281*4882a593Smuzhiyun dirlist = os.listdir(pth) 282*4882a593Smuzhiyun filterout = ['git.indirectionsymlink', 'source-date-epoch'] 283*4882a593Smuzhiyun dirlist = [x for x in dirlist if x not in filterout] 284*4882a593Smuzhiyun return dirlist 285