blob: 58e4028aede074bb377dd69d4d095d77dbecca0c [file] [log] [blame]
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001# Utility functions for reading and modifying recipes
2#
3# Some code borrowed from the OE layer index
4#
Patrick Williamsc0f7c042017-02-23 20:41:17 -06005# Copyright (C) 2013-2016 Intel Corporation
Patrick Williamsc124f4f2015-09-15 14:41:29 -05006#
7
8import sys
9import os
10import os.path
11import tempfile
12import textwrap
13import difflib
Patrick Williamsc0f7c042017-02-23 20:41:17 -060014from . import utils
Patrick Williamsc124f4f2015-09-15 14:41:29 -050015import shutil
16import re
17import fnmatch
Patrick Williamsc0f7c042017-02-23 20:41:17 -060018import glob
Patrick Williamsc124f4f2015-09-15 14:41:29 -050019from collections import OrderedDict, defaultdict
20
21
22# Help us to find places to insert values
Patrick Williamsc0f7c042017-02-23 20:41:17 -060023recipe_progression = ['SUMMARY', 'DESCRIPTION', 'HOMEPAGE', 'BUGTRACKER', 'SECTION', 'LICENSE', 'LICENSE_FLAGS', 'LIC_FILES_CHKSUM', 'PROVIDES', 'DEPENDS', 'PR', 'PV', 'SRCREV', 'SRCPV', 'SRC_URI', 'S', 'do_fetch()', 'do_unpack()', 'do_patch()', 'EXTRA_OECONF', 'EXTRA_OECMAKE', 'EXTRA_OESCONS', 'do_configure()', 'EXTRA_OEMAKE', 'do_compile()', 'do_install()', 'do_populate_sysroot()', 'INITSCRIPT', 'USERADD', 'GROUPADD', 'PACKAGES', 'FILES', 'RDEPENDS', 'RRECOMMENDS', 'RSUGGESTS', 'RPROVIDES', 'RREPLACES', 'RCONFLICTS', 'ALLOW_EMPTY', 'populate_packages()', 'do_package()', 'do_deploy()']
Patrick Williamsc124f4f2015-09-15 14:41:29 -050024# Variables that sometimes are a bit long but shouldn't be wrapped
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050025nowrap_vars = ['SUMMARY', 'HOMEPAGE', 'BUGTRACKER', 'SRC_URI[md5sum]', 'SRC_URI[sha256sum]']
Patrick Williamsc124f4f2015-09-15 14:41:29 -050026list_vars = ['SRC_URI', 'LIC_FILES_CHKSUM']
27meta_vars = ['SUMMARY', 'DESCRIPTION', 'HOMEPAGE', 'BUGTRACKER', 'SECTION']
28
29
Patrick Williamsc0f7c042017-02-23 20:41:17 -060030def pn_to_recipe(cooker, pn, mc=''):
Patrick Williamsc124f4f2015-09-15 14:41:29 -050031 """Convert a recipe name (PN) to the path to the recipe file"""
32 import bb.providers
33
Patrick Williamsc0f7c042017-02-23 20:41:17 -060034 if pn in cooker.recipecaches[mc].pkg_pn:
35 best = bb.providers.findBestProvider(pn, cooker.data, cooker.recipecaches[mc], cooker.recipecaches[mc].pkg_pn)
Patrick Williamsc124f4f2015-09-15 14:41:29 -050036 return best[3]
Patrick Williamsc0f7c042017-02-23 20:41:17 -060037 elif pn in cooker.recipecaches[mc].providers:
38 filenames = cooker.recipecaches[mc].providers[pn]
39 eligible, foundUnique = bb.providers.filterProviders(filenames, pn, cooker.expanded_data, cooker.recipecaches[mc])
Patrick Williamsf1e5d692016-03-30 15:21:19 -050040 filename = eligible[0]
41 return filename
Patrick Williamsc124f4f2015-09-15 14:41:29 -050042 else:
43 return None
44
45
46def get_unavailable_reasons(cooker, pn):
47 """If a recipe could not be found, find out why if possible"""
48 import bb.taskdata
49 taskdata = bb.taskdata.TaskData(None, skiplist=cooker.skiplist)
50 return taskdata.get_reasons(pn)
51
52
Patrick Williamsc0f7c042017-02-23 20:41:17 -060053def parse_recipe(cooker, fn, appendfiles):
Patrick Williamsc124f4f2015-09-15 14:41:29 -050054 """
55 Parse an individual recipe file, optionally with a list of
56 bbappend files.
57 """
58 import bb.cache
Patrick Williamsc0f7c042017-02-23 20:41:17 -060059 parser = bb.cache.NoCache(cooker.databuilder)
60 envdata = parser.loadDataFull(fn, appendfiles)
Patrick Williamsc124f4f2015-09-15 14:41:29 -050061 return envdata
62
63
64def parse_recipe_simple(cooker, pn, d, appends=True):
65 """
66 Parse a recipe and optionally all bbappends that apply to it
67 in the current configuration.
68 """
69 import bb.providers
70
71 recipefile = pn_to_recipe(cooker, pn)
72 if not recipefile:
73 skipreasons = get_unavailable_reasons(cooker, pn)
74 # We may as well re-use bb.providers.NoProvider here
75 if skipreasons:
76 raise bb.providers.NoProvider(skipreasons)
77 else:
78 raise bb.providers.NoProvider('Unable to find any recipe file matching %s' % pn)
79 if appends:
80 appendfiles = cooker.collection.get_file_appends(recipefile)
Patrick Williamsf1e5d692016-03-30 15:21:19 -050081 else:
82 appendfiles = None
Patrick Williamsc0f7c042017-02-23 20:41:17 -060083 return parse_recipe(cooker, recipefile, appendfiles)
Patrick Williamsc124f4f2015-09-15 14:41:29 -050084
85
86def get_var_files(fn, varlist, d):
87 """Find the file in which each of a list of variables is set.
88 Note: requires variable history to be enabled when parsing.
89 """
90 varfiles = {}
91 for v in varlist:
92 history = d.varhistory.variable(v)
93 files = []
94 for event in history:
95 if 'file' in event and not 'flag' in event:
96 files.append(event['file'])
97 if files:
98 actualfile = files[-1]
99 else:
100 actualfile = None
101 varfiles[v] = actualfile
102
103 return varfiles
104
105
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500106def split_var_value(value, assignment=True):
107 """
108 Split a space-separated variable's value into a list of items,
109 taking into account that some of the items might be made up of
110 expressions containing spaces that should not be split.
111 Parameters:
112 value:
113 The string value to split
114 assignment:
115 True to assume that the value represents an assignment
116 statement, False otherwise. If True, and an assignment
117 statement is passed in the first item in
118 the returned list will be the part of the assignment
119 statement up to and including the opening quote character,
120 and the last item will be the closing quote.
121 """
122 inexpr = 0
123 lastchar = None
124 out = []
125 buf = ''
126 for char in value:
127 if char == '{':
128 if lastchar == '$':
129 inexpr += 1
130 elif char == '}':
131 inexpr -= 1
132 elif assignment and char in '"\'' and inexpr == 0:
133 if buf:
134 out.append(buf)
135 out.append(char)
136 char = ''
137 buf = ''
138 elif char.isspace() and inexpr == 0:
139 char = ''
140 if buf:
141 out.append(buf)
142 buf = ''
143 buf += char
144 lastchar = char
145 if buf:
146 out.append(buf)
147
148 # Join together assignment statement and opening quote
149 outlist = out
150 if assignment:
151 assigfound = False
152 for idx, item in enumerate(out):
153 if '=' in item:
154 assigfound = True
155 if assigfound:
156 if '"' in item or "'" in item:
157 outlist = [' '.join(out[:idx+1])]
158 outlist.extend(out[idx+1:])
159 break
160 return outlist
161
162
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600163def patch_recipe_lines(fromlines, values, trailing_newline=True):
164 """Update or insert variable values into lines from a recipe.
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500165 Note that some manual inspection/intervention may be required
166 since this cannot handle all situations.
167 """
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500168
169 import bb.utils
170
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600171 if trailing_newline:
172 newline = '\n'
173 else:
174 newline = ''
175
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500176 recipe_progression_res = []
177 recipe_progression_restrs = []
178 for item in recipe_progression:
179 if item.endswith('()'):
180 key = item[:-2]
181 else:
182 key = item
183 restr = '%s(_[a-zA-Z0-9-_$(){}]+|\[[^\]]*\])?' % key
184 if item.endswith('()'):
185 recipe_progression_restrs.append(restr + '()')
186 else:
187 recipe_progression_restrs.append(restr)
188 recipe_progression_res.append(re.compile('^%s$' % restr))
189
190 def get_recipe_pos(variable):
191 for i, p in enumerate(recipe_progression_res):
192 if p.match(variable):
193 return i
194 return -1
195
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500196 remainingnames = {}
197 for k in values.keys():
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500198 remainingnames[k] = get_recipe_pos(k)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600199 remainingnames = OrderedDict(sorted(remainingnames.items(), key=lambda x: x[1]))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500200
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500201 modifying = False
202
203 def outputvalue(name, lines, rewindcomments=False):
204 if values[name] is None:
205 return
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600206 rawtext = '%s = "%s"%s' % (name, values[name], newline)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500207 addlines = []
208 if name in nowrap_vars:
209 addlines.append(rawtext)
210 elif name in list_vars:
211 splitvalue = split_var_value(values[name], assignment=False)
212 if len(splitvalue) > 1:
213 linesplit = ' \\\n' + (' ' * (len(name) + 4))
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600214 addlines.append('%s = "%s%s"%s' % (name, linesplit.join(splitvalue), linesplit, newline))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500215 else:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500216 addlines.append(rawtext)
217 else:
218 wrapped = textwrap.wrap(rawtext)
219 for wrapline in wrapped[:-1]:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600220 addlines.append('%s \\%s' % (wrapline, newline))
221 addlines.append('%s%s' % (wrapped[-1], newline))
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500222 if rewindcomments:
223 # Ensure we insert the lines before any leading comments
224 # (that we'd want to ensure remain leading the next value)
225 for i, ln in reversed(list(enumerate(lines))):
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600226 if not ln.startswith('#'):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500227 lines[i+1:i+1] = addlines
228 break
229 else:
230 lines.extend(addlines)
231 else:
232 lines.extend(addlines)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500233
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500234 existingnames = []
235 def patch_recipe_varfunc(varname, origvalue, op, newlines):
236 if modifying:
237 # Insert anything that should come before this variable
238 pos = get_recipe_pos(varname)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600239 for k in list(remainingnames):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500240 if remainingnames[k] > -1 and pos >= remainingnames[k] and not k in existingnames:
241 outputvalue(k, newlines, rewindcomments=True)
242 del remainingnames[k]
243 # Now change this variable, if it needs to be changed
244 if varname in existingnames and op in ['+=', '=', '=+']:
245 if varname in remainingnames:
246 outputvalue(varname, newlines)
247 del remainingnames[varname]
248 return None, None, 0, True
249 else:
250 if varname in values:
251 existingnames.append(varname)
252 return origvalue, None, 0, True
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500253
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500254 # First run - establish which values we want to set are already in the file
255 varlist = [re.escape(item) for item in values.keys()]
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600256 bb.utils.edit_metadata(fromlines, varlist, patch_recipe_varfunc)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500257 # Second run - actually set everything
258 modifying = True
259 varlist.extend(recipe_progression_restrs)
260 changed, tolines = bb.utils.edit_metadata(fromlines, varlist, patch_recipe_varfunc, match_overrides=True)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500261
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500262 if remainingnames:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600263 if tolines and tolines[-1].strip() != '':
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500264 tolines.append('\n')
265 for k in remainingnames.keys():
266 outputvalue(k, tolines)
267
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600268 return changed, tolines
269
270
271def patch_recipe_file(fn, values, patch=False, relpath=''):
272 """Update or insert variable values into a recipe file (assuming you
273 have already identified the exact file you want to update.)
274 Note that some manual inspection/intervention may be required
275 since this cannot handle all situations.
276 """
277
278 with open(fn, 'r') as f:
279 fromlines = f.readlines()
280
281 _, tolines = patch_recipe_lines(fromlines, values)
282
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500283 if patch:
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500284 relfn = os.path.relpath(fn, relpath)
285 diff = difflib.unified_diff(fromlines, tolines, 'a/%s' % relfn, 'b/%s' % relfn)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500286 return diff
287 else:
288 with open(fn, 'w') as f:
289 f.writelines(tolines)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500290 return None
291
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500292
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500293def localise_file_vars(fn, varfiles, varlist):
294 """Given a list of variables and variable history (fetched with get_var_files())
295 find where each variable should be set/changed. This handles for example where a
296 recipe includes an inc file where variables might be changed - in most cases
297 we want to update the inc file when changing the variable value rather than adding
298 it to the recipe itself.
299 """
300 fndir = os.path.dirname(fn) + os.sep
301
302 first_meta_file = None
303 for v in meta_vars:
304 f = varfiles.get(v, None)
305 if f:
306 actualdir = os.path.dirname(f) + os.sep
307 if actualdir.startswith(fndir):
308 first_meta_file = f
309 break
310
311 filevars = defaultdict(list)
312 for v in varlist:
313 f = varfiles[v]
314 # Only return files that are in the same directory as the recipe or in some directory below there
315 # (this excludes bbclass files and common inc files that wouldn't be appropriate to set the variable
316 # in if we were going to set a value specific to this recipe)
317 if f:
318 actualfile = f
319 else:
320 # Variable isn't in a file, if it's one of the "meta" vars, use the first file with a meta var in it
321 if first_meta_file:
322 actualfile = first_meta_file
323 else:
324 actualfile = fn
325
326 actualdir = os.path.dirname(actualfile) + os.sep
327 if not actualdir.startswith(fndir):
328 actualfile = fn
329 filevars[actualfile].append(v)
330
331 return filevars
332
333def patch_recipe(d, fn, varvalues, patch=False, relpath=''):
334 """Modify a list of variable values in the specified recipe. Handles inc files if
335 used by the recipe.
336 """
337 varlist = varvalues.keys()
338 varfiles = get_var_files(fn, varlist, d)
339 locs = localise_file_vars(fn, varfiles, varlist)
340 patches = []
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600341 for f,v in locs.items():
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500342 vals = {k: varvalues[k] for k in v}
343 patchdata = patch_recipe_file(f, vals, patch, relpath)
344 if patch:
345 patches.append(patchdata)
346
347 if patch:
348 return patches
349 else:
350 return None
351
352
353
354def copy_recipe_files(d, tgt_dir, whole_dir=False, download=True):
355 """Copy (local) recipe files, including both files included via include/require,
356 and files referred to in the SRC_URI variable."""
357 import bb.fetch2
358 import oe.path
359
360 # FIXME need a warning if the unexpanded SRC_URI value contains variable references
361
362 uris = (d.getVar('SRC_URI', True) or "").split()
363 fetch = bb.fetch2.Fetch(uris, d)
364 if download:
365 fetch.download()
366
367 # Copy local files to target directory and gather any remote files
368 bb_dir = os.path.dirname(d.getVar('FILE', True)) + os.sep
369 remotes = []
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600370 copied = []
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500371 includes = [path for path in d.getVar('BBINCLUDED', True).split() if
372 path.startswith(bb_dir) and os.path.exists(path)]
373 for path in fetch.localpaths() + includes:
374 # Only import files that are under the meta directory
375 if path.startswith(bb_dir):
376 if not whole_dir:
377 relpath = os.path.relpath(path, bb_dir)
378 subdir = os.path.join(tgt_dir, os.path.dirname(relpath))
379 if not os.path.exists(subdir):
380 os.makedirs(subdir)
381 shutil.copy2(path, os.path.join(tgt_dir, relpath))
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600382 copied.append(relpath)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500383 else:
384 remotes.append(path)
385 # Simply copy whole meta dir, if requested
386 if whole_dir:
387 shutil.copytree(bb_dir, tgt_dir)
388
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600389 return copied, remotes
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500390
391
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500392def get_recipe_local_files(d, patches=False):
393 """Get a list of local files in SRC_URI within a recipe."""
394 uris = (d.getVar('SRC_URI', True) or "").split()
395 fetch = bb.fetch2.Fetch(uris, d)
396 ret = {}
397 for uri in uris:
398 if fetch.ud[uri].type == 'file':
399 if (not patches and
400 bb.utils.exec_flat_python_func('patch_path', uri, fetch, '')):
401 continue
402 # Skip files that are referenced by absolute path
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600403 fname = fetch.ud[uri].basepath
404 if os.path.isabs(fname):
405 continue
406 # Handle subdir=
407 subdir = fetch.ud[uri].parm.get('subdir', '')
408 if subdir:
409 if os.path.isabs(subdir):
410 continue
411 fname = os.path.join(subdir, fname)
412 ret[fname] = fetch.localpath(uri)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500413 return ret
414
415
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500416def get_recipe_patches(d):
417 """Get a list of the patches included in SRC_URI within a recipe."""
418 patchfiles = []
419 # Execute src_patches() defined in patch.bbclass - this works since that class
420 # is inherited globally
421 patches = bb.utils.exec_flat_python_func('src_patches', d)
422 for patch in patches:
423 _, _, local, _, _, parm = bb.fetch.decodeurl(patch)
424 patchfiles.append(local)
425 return patchfiles
426
427
428def get_recipe_patched_files(d):
429 """
430 Get the list of patches for a recipe along with the files each patch modifies.
431 Params:
432 d: the datastore for the recipe
433 Returns:
434 a dict mapping patch file path to a list of tuples of changed files and
435 change mode ('A' for add, 'D' for delete or 'M' for modify)
436 """
437 import oe.patch
438 # Execute src_patches() defined in patch.bbclass - this works since that class
439 # is inherited globally
440 patches = bb.utils.exec_flat_python_func('src_patches', d)
441 patchedfiles = {}
442 for patch in patches:
443 _, _, patchfile, _, _, parm = bb.fetch.decodeurl(patch)
444 striplevel = int(parm['striplevel'])
445 patchedfiles[patchfile] = oe.patch.PatchSet.getPatchedFiles(patchfile, striplevel, os.path.join(d.getVar('S', True), parm.get('patchdir', '')))
446 return patchedfiles
447
448
449def validate_pn(pn):
450 """Perform validation on a recipe name (PN) for a new recipe."""
451 reserved_names = ['forcevariable', 'append', 'prepend', 'remove']
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600452 if not re.match('^[0-9a-z-.+]+$', pn):
453 return 'Recipe name "%s" is invalid: only characters 0-9, a-z, -, + and . are allowed' % pn
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500454 elif pn in reserved_names:
455 return 'Recipe name "%s" is invalid: is a reserved keyword' % pn
456 elif pn.startswith('pn-'):
457 return 'Recipe name "%s" is invalid: names starting with "pn-" are reserved' % pn
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500458 elif pn.endswith(('.bb', '.bbappend', '.bbclass', '.inc', '.conf')):
459 return 'Recipe name "%s" is invalid: should be just a name, not a file name' % pn
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500460 return ''
461
462
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600463def get_bbfile_path(d, destdir, extrapathhint=None):
464 """
465 Determine the correct path for a recipe within a layer
466 Parameters:
467 d: Recipe-specific datastore
468 destdir: destination directory. Can be the path to the base of the layer or a
469 partial path somewhere within the layer.
470 extrapathhint: a path relative to the base of the layer to try
471 """
472 import bb.cookerdata
473
474 destdir = os.path.abspath(destdir)
475 destlayerdir = find_layerdir(destdir)
476
477 # Parse the specified layer's layer.conf file directly, in case the layer isn't in bblayers.conf
478 confdata = d.createCopy()
479 confdata.setVar('BBFILES', '')
480 confdata.setVar('LAYERDIR', destlayerdir)
481 destlayerconf = os.path.join(destlayerdir, "conf", "layer.conf")
482 confdata = bb.cookerdata.parse_config_file(destlayerconf, confdata)
483 pn = d.getVar('PN', True)
484
485 bbfilespecs = (confdata.getVar('BBFILES', True) or '').split()
486 if destdir == destlayerdir:
487 for bbfilespec in bbfilespecs:
488 if not bbfilespec.endswith('.bbappend'):
489 for match in glob.glob(bbfilespec):
490 splitext = os.path.splitext(os.path.basename(match))
491 if splitext[1] == '.bb':
492 mpn = splitext[0].split('_')[0]
493 if mpn == pn:
494 return os.path.dirname(match)
495
496 # Try to make up a path that matches BBFILES
497 # this is a little crude, but better than nothing
498 bpn = d.getVar('BPN', True)
499 recipefn = os.path.basename(d.getVar('FILE', True))
500 pathoptions = [destdir]
501 if extrapathhint:
502 pathoptions.append(os.path.join(destdir, extrapathhint))
503 if destdir == destlayerdir:
504 pathoptions.append(os.path.join(destdir, 'recipes-%s' % bpn, bpn))
505 pathoptions.append(os.path.join(destdir, 'recipes', bpn))
506 pathoptions.append(os.path.join(destdir, bpn))
507 elif not destdir.endswith(('/' + pn, '/' + bpn)):
508 pathoptions.append(os.path.join(destdir, bpn))
509 closepath = ''
510 for pathoption in pathoptions:
511 bbfilepath = os.path.join(pathoption, 'test.bb')
512 for bbfilespec in bbfilespecs:
513 if fnmatch.fnmatchcase(bbfilepath, bbfilespec):
514 return pathoption
515 return None
516
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500517def get_bbappend_path(d, destlayerdir, wildcardver=False):
518 """Determine how a bbappend for a recipe should be named and located within another layer"""
519
520 import bb.cookerdata
521
522 destlayerdir = os.path.abspath(destlayerdir)
523 recipefile = d.getVar('FILE', True)
524 recipefn = os.path.splitext(os.path.basename(recipefile))[0]
525 if wildcardver and '_' in recipefn:
526 recipefn = recipefn.split('_', 1)[0] + '_%'
527 appendfn = recipefn + '.bbappend'
528
529 # Parse the specified layer's layer.conf file directly, in case the layer isn't in bblayers.conf
530 confdata = d.createCopy()
531 confdata.setVar('BBFILES', '')
532 confdata.setVar('LAYERDIR', destlayerdir)
533 destlayerconf = os.path.join(destlayerdir, "conf", "layer.conf")
534 confdata = bb.cookerdata.parse_config_file(destlayerconf, confdata)
535
536 origlayerdir = find_layerdir(recipefile)
537 if not origlayerdir:
538 return (None, False)
539 # Now join this to the path where the bbappend is going and check if it is covered by BBFILES
540 appendpath = os.path.join(destlayerdir, os.path.relpath(os.path.dirname(recipefile), origlayerdir), appendfn)
541 closepath = ''
542 pathok = True
543 for bbfilespec in confdata.getVar('BBFILES', True).split():
544 if fnmatch.fnmatchcase(appendpath, bbfilespec):
545 # Our append path works, we're done
546 break
547 elif bbfilespec.startswith(destlayerdir) and fnmatch.fnmatchcase('test.bbappend', os.path.basename(bbfilespec)):
548 # Try to find the longest matching path
549 if len(bbfilespec) > len(closepath):
550 closepath = bbfilespec
551 else:
552 # Unfortunately the bbappend layer and the original recipe's layer don't have the same structure
553 if closepath:
554 # bbappend layer's layer.conf at least has a spec that picks up .bbappend files
555 # Now we just need to substitute out any wildcards
556 appendsubdir = os.path.relpath(os.path.dirname(closepath), destlayerdir)
557 if 'recipes-*' in appendsubdir:
558 # Try to copy this part from the original recipe path
559 res = re.search('/recipes-[^/]+/', recipefile)
560 if res:
561 appendsubdir = appendsubdir.replace('/recipes-*/', res.group(0))
562 # This is crude, but we have to do something
563 appendsubdir = appendsubdir.replace('*', recipefn.split('_')[0])
564 appendsubdir = appendsubdir.replace('?', 'a')
565 appendpath = os.path.join(destlayerdir, appendsubdir, appendfn)
566 else:
567 pathok = False
568 return (appendpath, pathok)
569
570
571def bbappend_recipe(rd, destlayerdir, srcfiles, install=None, wildcardver=False, machine=None, extralines=None, removevalues=None):
572 """
573 Writes a bbappend file for a recipe
574 Parameters:
575 rd: data dictionary for the recipe
576 destlayerdir: base directory of the layer to place the bbappend in
577 (subdirectory path from there will be determined automatically)
578 srcfiles: dict of source files to add to SRC_URI, where the value
579 is the full path to the file to be added, and the value is the
580 original filename as it would appear in SRC_URI or None if it
581 isn't already present. You may pass None for this parameter if
582 you simply want to specify your own content via the extralines
583 parameter.
584 install: dict mapping entries in srcfiles to a tuple of two elements:
585 install path (*without* ${D} prefix) and permission value (as a
586 string, e.g. '0644').
587 wildcardver: True to use a % wildcard in the bbappend filename, or
588 False to make the bbappend specific to the recipe version.
589 machine:
590 If specified, make the changes in the bbappend specific to this
591 machine. This will also cause PACKAGE_ARCH = "${MACHINE_ARCH}"
592 to be added to the bbappend.
593 extralines:
594 Extra lines to add to the bbappend. This may be a dict of name
595 value pairs, or simply a list of the lines.
596 removevalues:
597 Variable values to remove - a dict of names/values.
598 """
599
600 if not removevalues:
601 removevalues = {}
602
603 # Determine how the bbappend should be named
604 appendpath, pathok = get_bbappend_path(rd, destlayerdir, wildcardver)
605 if not appendpath:
606 bb.error('Unable to determine layer directory containing %s' % recipefile)
607 return (None, None)
608 if not pathok:
609 bb.warn('Unable to determine correct subdirectory path for bbappend file - check that what %s adds to BBFILES also matches .bbappend files. Using %s for now, but until you fix this the bbappend will not be applied.' % (os.path.join(destlayerdir, 'conf', 'layer.conf'), os.path.dirname(appendpath)))
610
611 appenddir = os.path.dirname(appendpath)
612 bb.utils.mkdirhier(appenddir)
613
614 # FIXME check if the bbappend doesn't get overridden by a higher priority layer?
615
616 layerdirs = [os.path.abspath(layerdir) for layerdir in rd.getVar('BBLAYERS', True).split()]
617 if not os.path.abspath(destlayerdir) in layerdirs:
618 bb.warn('Specified layer is not currently enabled in bblayers.conf, you will need to add it before this bbappend will be active')
619
620 bbappendlines = []
621 if extralines:
622 if isinstance(extralines, dict):
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600623 for name, value in extralines.items():
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500624 bbappendlines.append((name, '=', value))
625 else:
626 # Do our best to split it
627 for line in extralines:
628 if line[-1] == '\n':
629 line = line[:-1]
630 splitline = line.split(None, 2)
631 if len(splitline) == 3:
632 bbappendlines.append(tuple(splitline))
633 else:
634 raise Exception('Invalid extralines value passed')
635
636 def popline(varname):
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600637 for i in range(0, len(bbappendlines)):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500638 if bbappendlines[i][0] == varname:
639 line = bbappendlines.pop(i)
640 return line
641 return None
642
643 def appendline(varname, op, value):
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600644 for i in range(0, len(bbappendlines)):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500645 item = bbappendlines[i]
646 if item[0] == varname:
647 bbappendlines[i] = (item[0], item[1], item[2] + ' ' + value)
648 break
649 else:
650 bbappendlines.append((varname, op, value))
651
652 destsubdir = rd.getVar('PN', True)
653 if srcfiles:
654 bbappendlines.append(('FILESEXTRAPATHS_prepend', ':=', '${THISDIR}/${PN}:'))
655
656 appendoverride = ''
657 if machine:
658 bbappendlines.append(('PACKAGE_ARCH', '=', '${MACHINE_ARCH}'))
659 appendoverride = '_%s' % machine
660 copyfiles = {}
661 if srcfiles:
662 instfunclines = []
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600663 for newfile, origsrcfile in srcfiles.items():
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500664 srcfile = origsrcfile
665 srcurientry = None
666 if not srcfile:
667 srcfile = os.path.basename(newfile)
668 srcurientry = 'file://%s' % srcfile
669 # Double-check it's not there already
670 # FIXME do we care if the entry is added by another bbappend that might go away?
671 if not srcurientry in rd.getVar('SRC_URI', True).split():
672 if machine:
673 appendline('SRC_URI_append%s' % appendoverride, '=', ' ' + srcurientry)
674 else:
675 appendline('SRC_URI', '+=', srcurientry)
676 copyfiles[newfile] = srcfile
677 if install:
678 institem = install.pop(newfile, None)
679 if institem:
680 (destpath, perms) = institem
681 instdestpath = replace_dir_vars(destpath, rd)
682 instdirline = 'install -d ${D}%s' % os.path.dirname(instdestpath)
683 if not instdirline in instfunclines:
684 instfunclines.append(instdirline)
685 instfunclines.append('install -m %s ${WORKDIR}/%s ${D}%s' % (perms, os.path.basename(srcfile), instdestpath))
686 if instfunclines:
687 bbappendlines.append(('do_install_append%s()' % appendoverride, '', instfunclines))
688
689 bb.note('Writing append file %s' % appendpath)
690
691 if os.path.exists(appendpath):
692 # Work around lack of nonlocal in python 2
693 extvars = {'destsubdir': destsubdir}
694
695 def appendfile_varfunc(varname, origvalue, op, newlines):
696 if varname == 'FILESEXTRAPATHS_prepend':
697 if origvalue.startswith('${THISDIR}/'):
698 popline('FILESEXTRAPATHS_prepend')
699 extvars['destsubdir'] = rd.expand(origvalue.split('${THISDIR}/', 1)[1].rstrip(':'))
700 elif varname == 'PACKAGE_ARCH':
701 if machine:
702 popline('PACKAGE_ARCH')
703 return (machine, None, 4, False)
704 elif varname.startswith('do_install_append'):
705 func = popline(varname)
706 if func:
707 instfunclines = [line.strip() for line in origvalue.strip('\n').splitlines()]
708 for line in func[2]:
709 if not line in instfunclines:
710 instfunclines.append(line)
711 return (instfunclines, None, 4, False)
712 else:
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500713 splitval = split_var_value(origvalue, assignment=False)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500714 changed = False
715 removevar = varname
716 if varname in ['SRC_URI', 'SRC_URI_append%s' % appendoverride]:
717 removevar = 'SRC_URI'
718 line = popline(varname)
719 if line:
720 if line[2] not in splitval:
721 splitval.append(line[2])
722 changed = True
723 else:
724 line = popline(varname)
725 if line:
726 splitval = [line[2]]
727 changed = True
728
729 if removevar in removevalues:
730 remove = removevalues[removevar]
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600731 if isinstance(remove, str):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500732 if remove in splitval:
733 splitval.remove(remove)
734 changed = True
735 else:
736 for removeitem in remove:
737 if removeitem in splitval:
738 splitval.remove(removeitem)
739 changed = True
740
741 if changed:
742 newvalue = splitval
743 if len(newvalue) == 1:
744 # Ensure it's written out as one line
745 if '_append' in varname:
746 newvalue = ' ' + newvalue[0]
747 else:
748 newvalue = newvalue[0]
749 if not newvalue and (op in ['+=', '.='] or '_append' in varname):
750 # There's no point appending nothing
751 newvalue = None
752 if varname.endswith('()'):
753 indent = 4
754 else:
755 indent = -1
756 return (newvalue, None, indent, True)
757 return (origvalue, None, 4, False)
758
759 varnames = [item[0] for item in bbappendlines]
760 if removevalues:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600761 varnames.extend(list(removevalues.keys()))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500762
763 with open(appendpath, 'r') as f:
764 (updated, newlines) = bb.utils.edit_metadata(f, varnames, appendfile_varfunc)
765
766 destsubdir = extvars['destsubdir']
767 else:
768 updated = False
769 newlines = []
770
771 if bbappendlines:
772 for line in bbappendlines:
773 if line[0].endswith('()'):
774 newlines.append('%s {\n %s\n}\n' % (line[0], '\n '.join(line[2])))
775 else:
776 newlines.append('%s %s "%s"\n\n' % line)
777 updated = True
778
779 if updated:
780 with open(appendpath, 'w') as f:
781 f.writelines(newlines)
782
783 if copyfiles:
784 if machine:
785 destsubdir = os.path.join(destsubdir, machine)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600786 for newfile, srcfile in copyfiles.items():
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500787 filedest = os.path.join(appenddir, destsubdir, os.path.basename(srcfile))
788 if os.path.abspath(newfile) != os.path.abspath(filedest):
789 bb.note('Copying %s to %s' % (newfile, filedest))
790 bb.utils.mkdirhier(os.path.dirname(filedest))
791 shutil.copyfile(newfile, filedest)
792
793 return (appendpath, os.path.join(appenddir, destsubdir))
794
795
796def find_layerdir(fn):
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600797 """ Figure out the path to the base of the layer containing a file (e.g. a recipe)"""
798 pth = fn
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500799 layerdir = ''
800 while pth:
801 if os.path.exists(os.path.join(pth, 'conf', 'layer.conf')):
802 layerdir = pth
803 break
804 pth = os.path.dirname(pth)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600805 if pth == '/':
806 return None
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500807 return layerdir
808
809
810def replace_dir_vars(path, d):
811 """Replace common directory paths with appropriate variable references (e.g. /etc becomes ${sysconfdir})"""
812 dirvars = {}
813 # Sort by length so we get the variables we're interested in first
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600814 for var in sorted(list(d.keys()), key=len):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500815 if var.endswith('dir') and var.lower() == var:
816 value = d.getVar(var, True)
817 if value.startswith('/') and not '\n' in value and value not in dirvars:
818 dirvars[value] = var
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600819 for dirpath in sorted(list(dirvars.keys()), reverse=True):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500820 path = path.replace(dirpath, '${%s}' % dirvars[dirpath])
821 return path
822
823def get_recipe_pv_without_srcpv(pv, uri_type):
824 """
825 Get PV without SRCPV common in SCM's for now only
826 support git.
827
828 Returns tuple with pv, prefix and suffix.
829 """
830 pfx = ''
831 sfx = ''
832
833 if uri_type == 'git':
834 git_regex = re.compile("(?P<pfx>v?)(?P<ver>[^\+]*)((?P<sfx>\+(git)?r?(AUTOINC\+))(?P<rev>.*))?")
835 m = git_regex.match(pv)
836
837 if m:
838 pv = m.group('ver')
839 pfx = m.group('pfx')
840 sfx = m.group('sfx')
841 else:
842 regex = re.compile("(?P<pfx>(v|r)?)(?P<ver>.*)")
843 m = regex.match(pv)
844 if m:
845 pv = m.group('ver')
846 pfx = m.group('pfx')
847
848 return (pv, pfx, sfx)
849
850def get_recipe_upstream_version(rd):
851 """
852 Get upstream version of recipe using bb.fetch2 methods with support for
853 http, https, ftp and git.
854
855 bb.fetch2 exceptions can be raised,
856 FetchError when don't have network access or upstream site don't response.
857 NoMethodError when uri latest_versionstring method isn't implemented.
858
859 Returns a dictonary with version, type and datetime.
860 Type can be A for Automatic, M for Manual and U for Unknown.
861 """
862 from bb.fetch2 import decodeurl
863 from datetime import datetime
864
865 ru = {}
866 ru['version'] = ''
867 ru['type'] = 'U'
868 ru['datetime'] = ''
869
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500870 pv = rd.getVar('PV', True)
871
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500872 # XXX: If don't have SRC_URI means that don't have upstream sources so
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500873 # returns the current recipe version, so that upstream version check
874 # declares a match.
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500875 src_uris = rd.getVar('SRC_URI', True)
876 if not src_uris:
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500877 ru['version'] = pv
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500878 ru['type'] = 'M'
879 ru['datetime'] = datetime.now()
880 return ru
881
882 # XXX: we suppose that the first entry points to the upstream sources
883 src_uri = src_uris.split()[0]
884 uri_type, _, _, _, _, _ = decodeurl(src_uri)
885
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500886 manual_upstream_version = rd.getVar("RECIPE_UPSTREAM_VERSION", True)
887 if manual_upstream_version:
888 # manual tracking of upstream version.
889 ru['version'] = manual_upstream_version
890 ru['type'] = 'M'
891
892 manual_upstream_date = rd.getVar("CHECK_DATE", True)
893 if manual_upstream_date:
894 date = datetime.strptime(manual_upstream_date, "%b %d, %Y")
895 else:
896 date = datetime.now()
897 ru['datetime'] = date
898
899 elif uri_type == "file":
900 # files are always up-to-date
901 ru['version'] = pv
902 ru['type'] = 'A'
903 ru['datetime'] = datetime.now()
904 else:
905 ud = bb.fetch2.FetchData(src_uri, rd)
906 pupver = ud.method.latest_versionstring(ud, rd)
907 (upversion, revision) = pupver
908
909 # format git version version+gitAUTOINC+HASH
910 if uri_type == 'git':
911 (pv, pfx, sfx) = get_recipe_pv_without_srcpv(pv, uri_type)
912
913 # if contains revision but not upversion use current pv
914 if upversion == '' and revision:
915 upversion = pv
916
917 if upversion:
918 tmp = upversion
919 upversion = ''
920
921 if pfx:
922 upversion = pfx + tmp
923 else:
924 upversion = tmp
925
926 if sfx:
927 upversion = upversion + sfx + revision[:10]
928
929 if upversion:
930 ru['version'] = upversion
931 ru['type'] = 'A'
932
933 ru['datetime'] = datetime.now()
934
935 return ru