xref: /OK3568_Linux_fs/yocto/scripts/lib/scriptutils.py (revision 4882a59341e53eb6f0b4789bf948001014eff981)
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