blob: 3c60c3a1e6c649124a954d5b2c9b7ebebd6b9f46 [file] [log] [blame]
rjw91288f92022-11-01 13:59:36 +08001# Script utility functions
2#
3# Copyright (C) 2014 Intel Corporation
4#
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU General Public License version 2 as
7# published by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License along
15# with this program; if not, write to the Free Software Foundation, Inc.,
16# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
18import argparse
19import glob
20import logging
21import os
22import random
23import shlex
24import shutil
25import string
26import subprocess
27import sys
28import tempfile
29import importlib
30from importlib import machinery
31
32def logger_create(name, stream=None):
33 logger = logging.getLogger(name)
34 loggerhandler = logging.StreamHandler(stream=stream)
35 loggerhandler.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
36 logger.addHandler(loggerhandler)
37 logger.setLevel(logging.INFO)
38 return logger
39
40def logger_setup_color(logger, color='auto'):
41 from bb.msg import BBLogFormatter
42 console = logging.StreamHandler(sys.stdout)
43 formatter = BBLogFormatter("%(levelname)s: %(message)s")
44 console.setFormatter(formatter)
45 logger.handlers = [console]
46 if color == 'always' or (color=='auto' and console.stream.isatty()):
47 formatter.enable_color()
48
49
50def load_plugins(logger, plugins, pluginpath):
51 import imp
52
53 def load_plugin(name):
54 logger.debug('Loading plugin %s' % name)
55 spec = importlib.machinery.PathFinder.find_spec(name, path=[pluginpath] )
56 if spec:
57 return spec.loader.load_module()
58
59 def plugin_name(filename):
60 return os.path.splitext(os.path.basename(filename))[0]
61
62 known_plugins = [plugin_name(p.__name__) for p in plugins]
63 logger.debug('Loading plugins from %s...' % pluginpath)
64 for fn in glob.glob(os.path.join(pluginpath, '*.py')):
65 name = plugin_name(fn)
66 if name != '__init__' and name not in known_plugins:
67 plugin = load_plugin(name)
68 if hasattr(plugin, 'plugin_init'):
69 plugin.plugin_init(plugins)
70 plugins.append(plugin)
71
72def git_convert_standalone_clone(repodir):
73 """If specified directory is a git repository, ensure it's a standalone clone"""
74 import bb.process
75 if os.path.exists(os.path.join(repodir, '.git')):
76 alternatesfile = os.path.join(repodir, '.git', 'objects', 'info', 'alternates')
77 if os.path.exists(alternatesfile):
78 # This will have been cloned with -s, so we need to convert it so none
79 # of the contents is shared
80 bb.process.run('git repack -a', cwd=repodir)
81 os.remove(alternatesfile)
82
83def _get_temp_recipe_dir(d):
84 # This is a little bit hacky but we need to find a place where we can put
85 # the recipe so that bitbake can find it. We're going to delete it at the
86 # end so it doesn't really matter where we put it.
87 bbfiles = d.getVar('BBFILES').split()
88 fetchrecipedir = None
89 for pth in bbfiles:
90 if pth.endswith('.bb'):
91 pthdir = os.path.dirname(pth)
92 if os.access(os.path.dirname(os.path.dirname(pthdir)), os.W_OK):
93 fetchrecipedir = pthdir.replace('*', 'recipetool')
94 if pthdir.endswith('workspace/recipes/*'):
95 # Prefer the workspace
96 break
97 return fetchrecipedir
98
99class FetchUrlFailure(Exception):
100 def __init__(self, url):
101 self.url = url
102 def __str__(self):
103 return "Failed to fetch URL %s" % self.url
104
105def fetch_url(tinfoil, srcuri, srcrev, destdir, logger, preserve_tmp=False, mirrors=False):
106 """
107 Fetch the specified URL using normal do_fetch and do_unpack tasks, i.e.
108 any dependencies that need to be satisfied in order to support the fetch
109 operation will be taken care of
110 """
111
112 import bb
113
114 checksums = {}
115 fetchrecipepn = None
116
117 # We need to put our temp directory under ${BASE_WORKDIR} otherwise
118 # we may have problems with the recipe-specific sysroot population
119 tmpparent = tinfoil.config_data.getVar('BASE_WORKDIR')
120 bb.utils.mkdirhier(tmpparent)
121 tmpdir = tempfile.mkdtemp(prefix='recipetool-', dir=tmpparent)
122 try:
123 tmpworkdir = os.path.join(tmpdir, 'work')
124 logger.debug('fetch_url: temp dir is %s' % tmpdir)
125
126 fetchrecipedir = _get_temp_recipe_dir(tinfoil.config_data)
127 if not fetchrecipedir:
128 logger.error('Searched BBFILES but unable to find a writeable place to put temporary recipe')
129 sys.exit(1)
130 fetchrecipe = None
131 bb.utils.mkdirhier(fetchrecipedir)
132 try:
133 # Generate a dummy recipe so we can follow more or less normal paths
134 # for do_fetch and do_unpack
135 # I'd use tempfile functions here but underscores can be produced by that and those
136 # aren't allowed in recipe file names except to separate the version
137 rndstring = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(8))
138 fetchrecipe = os.path.join(fetchrecipedir, 'tmp-recipetool-%s.bb' % rndstring)
139 fetchrecipepn = os.path.splitext(os.path.basename(fetchrecipe))[0]
140 logger.debug('Generating initial recipe %s for fetching' % fetchrecipe)
141 with open(fetchrecipe, 'w') as f:
142 # We don't want to have to specify LIC_FILES_CHKSUM
143 f.write('LICENSE = "CLOSED"\n')
144 # We don't need the cross-compiler
145 f.write('INHIBIT_DEFAULT_DEPS = "1"\n')
146 # We don't have the checksums yet so we can't require them
147 f.write('BB_STRICT_CHECKSUM = "ignore"\n')
148 f.write('SRC_URI = "%s"\n' % srcuri)
149 f.write('SRCREV = "%s"\n' % srcrev)
150 f.write('WORKDIR = "%s"\n' % tmpworkdir)
151 # Set S out of the way so it doesn't get created under the workdir
152 f.write('S = "%s"\n' % os.path.join(tmpdir, 'emptysrc'))
153 if not mirrors:
154 # We do not need PREMIRRORS since we are almost certainly
155 # fetching new source rather than something that has already
156 # been fetched. Hence, we disable them by default.
157 # However, we provide an option for users to enable it.
158 f.write('PREMIRRORS = ""\n')
159 f.write('MIRRORS = ""\n')
160
161 logger.info('Fetching %s...' % srcuri)
162
163 # FIXME this is too noisy at the moment
164
165 # Parse recipes so our new recipe gets picked up
166 tinfoil.parse_recipes()
167
168 def eventhandler(event):
169 if isinstance(event, bb.fetch2.MissingChecksumEvent):
170 checksums.update(event.checksums)
171 return True
172 return False
173
174 # Run the fetch + unpack tasks
175 res = tinfoil.build_targets(fetchrecipepn,
176 'do_unpack',
177 handle_events=True,
178 extra_events=['bb.fetch2.MissingChecksumEvent'],
179 event_callback=eventhandler)
180 if not res:
181 raise FetchUrlFailure(srcuri)
182
183 # Remove unneeded directories
184 rd = tinfoil.parse_recipe(fetchrecipepn)
185 if rd:
186 pathvars = ['T', 'RECIPE_SYSROOT', 'RECIPE_SYSROOT_NATIVE']
187 for pathvar in pathvars:
188 path = rd.getVar(pathvar)
189 shutil.rmtree(path)
190 finally:
191 if fetchrecipe:
192 try:
193 os.remove(fetchrecipe)
194 except FileNotFoundError:
195 pass
196 try:
197 os.rmdir(fetchrecipedir)
198 except OSError as e:
199 import errno
200 if e.errno != errno.ENOTEMPTY:
201 raise
202
203 bb.utils.mkdirhier(destdir)
204 for fn in os.listdir(tmpworkdir):
205 shutil.move(os.path.join(tmpworkdir, fn), destdir)
206
207 finally:
208 if not preserve_tmp:
209 shutil.rmtree(tmpdir)
210 tmpdir = None
211
212 return checksums, tmpdir
213
214
215def run_editor(fn, logger=None):
216 if isinstance(fn, str):
217 files = [fn]
218 else:
219 files = fn
220
221 editor = os.getenv('VISUAL', os.getenv('EDITOR', 'vi'))
222 try:
223 #print(shlex.split(editor) + files)
224 return subprocess.check_call(shlex.split(editor) + files)
225 except subprocess.CalledProcessError as exc:
226 logger.error("Execution of '%s' failed: %s" % (editor, exc))
227 return 1
228
229def is_src_url(param):
230 """
231 Check if a parameter is a URL and return True if so
232 NOTE: be careful about changing this as it will influence how devtool/recipetool command line handling works
233 """
234 if not param:
235 return False
236 elif '://' in param:
237 return True
238 elif param.startswith('git@') or ('@' in param and param.endswith('.git')):
239 return True
240 return False