diff options
| -rw-r--r-- | meta/lib/oe/recipeutils.py | 279 |
1 files changed, 279 insertions, 0 deletions
diff --git a/meta/lib/oe/recipeutils.py b/meta/lib/oe/recipeutils.py new file mode 100644 index 0000000000..1758dcecba --- /dev/null +++ b/meta/lib/oe/recipeutils.py | |||
| @@ -0,0 +1,279 @@ | |||
| 1 | # Utility functions for reading and modifying recipes | ||
| 2 | # | ||
| 3 | # Some code borrowed from the OE layer index | ||
| 4 | # | ||
| 5 | # Copyright (C) 2013-2014 Intel Corporation | ||
| 6 | # | ||
| 7 | |||
| 8 | import sys | ||
| 9 | import os | ||
| 10 | import os.path | ||
| 11 | import tempfile | ||
| 12 | import textwrap | ||
| 13 | import difflib | ||
| 14 | import utils | ||
| 15 | import shutil | ||
| 16 | import re | ||
| 17 | from collections import OrderedDict, defaultdict | ||
| 18 | |||
| 19 | |||
| 20 | # Help us to find places to insert values | ||
| 21 | recipe_progression = ['SUMMARY', 'DESCRIPTION', 'HOMEPAGE', 'BUGTRACKER', 'SECTION', 'LICENSE', 'LIC_FILES_CHKSUM', 'PROVIDES', 'DEPENDS', 'PR', 'PV', 'SRC_URI', 'do_fetch', 'do_unpack', 'do_patch', 'EXTRA_OECONF', 'do_configure', 'EXTRA_OEMAKE', 'do_compile', 'do_install', 'do_populate_sysroot', 'INITSCRIPT', 'USERADD', 'GROUPADD', 'PACKAGES', 'FILES', 'RDEPENDS', 'RRECOMMENDS', 'RSUGGESTS', 'RPROVIDES', 'RREPLACES', 'RCONFLICTS', 'ALLOW_EMPTY', 'do_package', 'do_deploy'] | ||
| 22 | # Variables that sometimes are a bit long but shouldn't be wrapped | ||
| 23 | nowrap_vars = ['SUMMARY', 'HOMEPAGE', 'BUGTRACKER'] | ||
| 24 | list_vars = ['SRC_URI', 'LIC_FILES_CHKSUM'] | ||
| 25 | meta_vars = ['SUMMARY', 'DESCRIPTION', 'HOMEPAGE', 'BUGTRACKER', 'SECTION'] | ||
| 26 | |||
| 27 | |||
| 28 | def pn_to_recipe(cooker, pn): | ||
| 29 | """Convert a recipe name (PN) to the path to the recipe file""" | ||
| 30 | import bb.providers | ||
| 31 | |||
| 32 | if pn in cooker.recipecache.pkg_pn: | ||
| 33 | filenames = cooker.recipecache.pkg_pn[pn] | ||
| 34 | best = bb.providers.findBestProvider(pn, cooker.data, cooker.recipecache, cooker.recipecache.pkg_pn) | ||
| 35 | return best[3] | ||
| 36 | else: | ||
| 37 | return None | ||
| 38 | |||
| 39 | |||
| 40 | def get_unavailable_reasons(cooker, pn): | ||
| 41 | """If a recipe could not be found, find out why if possible""" | ||
| 42 | import bb.taskdata | ||
| 43 | taskdata = bb.taskdata.TaskData(None, skiplist=cooker.skiplist) | ||
| 44 | return taskdata.get_reasons(pn) | ||
| 45 | |||
| 46 | |||
| 47 | def parse_recipe(fn, d): | ||
| 48 | """Parse an individual recipe""" | ||
| 49 | import bb.cache | ||
| 50 | envdata = bb.cache.Cache.loadDataFull(fn, [], d) | ||
| 51 | return envdata | ||
| 52 | |||
| 53 | |||
| 54 | def get_var_files(fn, varlist, d): | ||
| 55 | """Find the file in which each of a list of variables is set. | ||
| 56 | Note: requires variable history to be enabled when parsing. | ||
| 57 | """ | ||
| 58 | envdata = parse_recipe(fn, d) | ||
| 59 | varfiles = {} | ||
| 60 | for v in varlist: | ||
| 61 | history = envdata.varhistory.variable(v) | ||
| 62 | files = [] | ||
| 63 | for event in history: | ||
| 64 | if 'file' in event and not 'flag' in event: | ||
| 65 | files.append(event['file']) | ||
| 66 | if files: | ||
| 67 | actualfile = files[-1] | ||
| 68 | else: | ||
| 69 | actualfile = None | ||
| 70 | varfiles[v] = actualfile | ||
| 71 | |||
| 72 | return varfiles | ||
| 73 | |||
| 74 | |||
| 75 | def patch_recipe_file(fn, values, patch=False, relpath=''): | ||
| 76 | """Update or insert variable values into a recipe file (assuming you | ||
| 77 | have already identified the exact file you want to update.) | ||
| 78 | Note that some manual inspection/intervention may be required | ||
| 79 | since this cannot handle all situations. | ||
| 80 | """ | ||
| 81 | remainingnames = {} | ||
| 82 | for k in values.keys(): | ||
| 83 | remainingnames[k] = recipe_progression.index(k) if k in recipe_progression else -1 | ||
| 84 | remainingnames = OrderedDict(sorted(remainingnames.iteritems(), key=lambda x: x[1])) | ||
| 85 | |||
| 86 | with tempfile.NamedTemporaryFile('w', delete=False) as tf: | ||
| 87 | def outputvalue(name): | ||
| 88 | rawtext = '%s = "%s"\n' % (name, values[name]) | ||
| 89 | if name in nowrap_vars: | ||
| 90 | tf.write(rawtext) | ||
| 91 | elif name in list_vars: | ||
| 92 | splitvalue = values[name].split() | ||
| 93 | if len(splitvalue) > 1: | ||
| 94 | linesplit = ' \\\n' + (' ' * (len(name) + 4)) | ||
| 95 | tf.write('%s = "%s%s"\n' % (name, linesplit.join(splitvalue), linesplit)) | ||
| 96 | else: | ||
| 97 | tf.write(rawtext) | ||
| 98 | else: | ||
| 99 | wrapped = textwrap.wrap(rawtext) | ||
| 100 | for wrapline in wrapped[:-1]: | ||
| 101 | tf.write('%s \\\n' % wrapline) | ||
| 102 | tf.write('%s\n' % wrapped[-1]) | ||
| 103 | |||
| 104 | tfn = tf.name | ||
| 105 | with open(fn, 'r') as f: | ||
| 106 | # First runthrough - find existing names (so we know not to insert based on recipe_progression) | ||
| 107 | # Second runthrough - make the changes | ||
| 108 | existingnames = [] | ||
| 109 | for runthrough in [1, 2]: | ||
| 110 | currname = None | ||
| 111 | for line in f: | ||
| 112 | if not currname: | ||
| 113 | insert = False | ||
| 114 | for k in remainingnames.keys(): | ||
| 115 | for p in recipe_progression: | ||
| 116 | if re.match('^%s[ ?:=]' % p, line): | ||
| 117 | if remainingnames[k] > -1 and recipe_progression.index(p) > remainingnames[k] and runthrough > 1 and not k in existingnames: | ||
| 118 | outputvalue(k) | ||
| 119 | del remainingnames[k] | ||
| 120 | break | ||
| 121 | for k in remainingnames.keys(): | ||
| 122 | if re.match('^%s[ ?:=]' % k, line): | ||
| 123 | currname = k | ||
| 124 | if runthrough == 1: | ||
| 125 | existingnames.append(k) | ||
| 126 | else: | ||
| 127 | del remainingnames[k] | ||
| 128 | break | ||
| 129 | if currname and runthrough > 1: | ||
| 130 | outputvalue(currname) | ||
| 131 | |||
| 132 | if currname: | ||
| 133 | sline = line.rstrip() | ||
| 134 | if not sline.endswith('\\'): | ||
| 135 | currname = None | ||
| 136 | continue | ||
| 137 | if runthrough > 1: | ||
| 138 | tf.write(line) | ||
| 139 | f.seek(0) | ||
| 140 | if remainingnames: | ||
| 141 | tf.write('\n') | ||
| 142 | for k in remainingnames.keys(): | ||
| 143 | outputvalue(k) | ||
| 144 | |||
| 145 | with open(tfn, 'U') as f: | ||
| 146 | tolines = f.readlines() | ||
| 147 | if patch: | ||
| 148 | with open(fn, 'U') as f: | ||
| 149 | fromlines = f.readlines() | ||
| 150 | relfn = os.path.relpath(fn, relpath) | ||
| 151 | diff = difflib.unified_diff(fromlines, tolines, 'a/%s' % relfn, 'b/%s' % relfn) | ||
| 152 | os.remove(tfn) | ||
| 153 | return diff | ||
| 154 | else: | ||
| 155 | with open(fn, 'w') as f: | ||
| 156 | f.writelines(tolines) | ||
| 157 | os.remove(tfn) | ||
| 158 | return None | ||
| 159 | |||
| 160 | def localise_file_vars(fn, varfiles, varlist): | ||
| 161 | """Given a list of variables and variable history (fetched with get_var_files()) | ||
| 162 | find where each variable should be set/changed. This handles for example where a | ||
| 163 | recipe includes an inc file where variables might be changed - in most cases | ||
| 164 | we want to update the inc file when changing the variable value rather than adding | ||
| 165 | it to the recipe itself. | ||
| 166 | """ | ||
| 167 | fndir = os.path.dirname(fn) + os.sep | ||
| 168 | |||
| 169 | first_meta_file = None | ||
| 170 | for v in meta_vars: | ||
| 171 | f = varfiles.get(v, None) | ||
| 172 | if f: | ||
| 173 | actualdir = os.path.dirname(f) + os.sep | ||
| 174 | if actualdir.startswith(fndir): | ||
| 175 | first_meta_file = f | ||
| 176 | break | ||
| 177 | |||
| 178 | filevars = defaultdict(list) | ||
| 179 | for v in varlist: | ||
| 180 | f = varfiles[v] | ||
| 181 | # Only return files that are in the same directory as the recipe or in some directory below there | ||
| 182 | # (this excludes bbclass files and common inc files that wouldn't be appropriate to set the variable | ||
| 183 | # in if we were going to set a value specific to this recipe) | ||
| 184 | if f: | ||
| 185 | actualfile = f | ||
| 186 | else: | ||
| 187 | # Variable isn't in a file, if it's one of the "meta" vars, use the first file with a meta var in it | ||
| 188 | if first_meta_file: | ||
| 189 | actualfile = first_meta_file | ||
| 190 | else: | ||
| 191 | actualfile = fn | ||
| 192 | |||
| 193 | actualdir = os.path.dirname(actualfile) + os.sep | ||
| 194 | if not actualdir.startswith(fndir): | ||
| 195 | actualfile = fn | ||
| 196 | filevars[actualfile].append(v) | ||
| 197 | |||
| 198 | return filevars | ||
| 199 | |||
| 200 | def patch_recipe(d, fn, varvalues, patch=False, relpath=''): | ||
| 201 | """Modify a list of variable values in the specified recipe. Handles inc files if | ||
| 202 | used by the recipe. | ||
| 203 | """ | ||
| 204 | varlist = varvalues.keys() | ||
| 205 | varfiles = get_var_files(fn, varlist, d) | ||
| 206 | locs = localise_file_vars(fn, varfiles, varlist) | ||
| 207 | patches = [] | ||
| 208 | for f,v in locs.iteritems(): | ||
| 209 | vals = {k: varvalues[k] for k in v} | ||
| 210 | patchdata = patch_recipe_file(f, vals, patch, relpath) | ||
| 211 | if patch: | ||
| 212 | patches.append(patchdata) | ||
| 213 | |||
| 214 | if patch: | ||
| 215 | return patches | ||
| 216 | else: | ||
| 217 | return None | ||
| 218 | |||
| 219 | |||
| 220 | |||
| 221 | def copy_recipe_files(d, tgt_dir, whole_dir=False, download=True): | ||
| 222 | """Copy (local) recipe files, including both files included via include/require, | ||
| 223 | and files referred to in the SRC_URI variable.""" | ||
| 224 | import bb.fetch2 | ||
| 225 | import oe.path | ||
| 226 | |||
| 227 | # FIXME need a warning if the unexpanded SRC_URI value contains variable references | ||
| 228 | |||
| 229 | uris = (d.getVar('SRC_URI', True) or "").split() | ||
| 230 | fetch = bb.fetch2.Fetch(uris, d) | ||
| 231 | if download: | ||
| 232 | fetch.download() | ||
| 233 | |||
| 234 | # Copy local files to target directory and gather any remote files | ||
| 235 | bb_dir = os.path.dirname(d.getVar('FILE', True)) + os.sep | ||
| 236 | remotes = [] | ||
| 237 | includes = [path for path in d.getVar('BBINCLUDED', True).split() if | ||
| 238 | path.startswith(bb_dir) and os.path.exists(path)] | ||
| 239 | for path in fetch.localpaths() + includes: | ||
| 240 | # Only import files that are under the meta directory | ||
| 241 | if path.startswith(bb_dir): | ||
| 242 | if not whole_dir: | ||
| 243 | relpath = os.path.relpath(path, bb_dir) | ||
| 244 | subdir = os.path.join(tgt_dir, os.path.dirname(relpath)) | ||
| 245 | if not os.path.exists(subdir): | ||
| 246 | os.makedirs(subdir) | ||
| 247 | shutil.copy2(path, os.path.join(tgt_dir, relpath)) | ||
| 248 | else: | ||
| 249 | remotes.append(path) | ||
| 250 | # Simply copy whole meta dir, if requested | ||
| 251 | if whole_dir: | ||
| 252 | shutil.copytree(bb_dir, tgt_dir) | ||
| 253 | |||
| 254 | return remotes | ||
| 255 | |||
| 256 | |||
| 257 | def get_recipe_patches(d): | ||
| 258 | """Get a list of the patches included in SRC_URI within a recipe.""" | ||
| 259 | patchfiles = [] | ||
| 260 | # Execute src_patches() defined in patch.bbclass - this works since that class | ||
| 261 | # is inherited globally | ||
| 262 | patches = bb.utils.exec_flat_python_func('src_patches', d) | ||
| 263 | for patch in patches: | ||
| 264 | _, _, local, _, _, parm = bb.fetch.decodeurl(patch) | ||
| 265 | patchfiles.append(local) | ||
| 266 | return patchfiles | ||
| 267 | |||
| 268 | |||
| 269 | def validate_pn(pn): | ||
| 270 | """Perform validation on a recipe name (PN) for a new recipe.""" | ||
| 271 | reserved_names = ['forcevariable', 'append', 'prepend', 'remove'] | ||
| 272 | if not re.match('[0-9a-z-]+', pn): | ||
| 273 | return 'Recipe name "%s" is invalid: only characters 0-9, a-z and - are allowed' % pn | ||
| 274 | elif pn in reserved_names: | ||
| 275 | return 'Recipe name "%s" is invalid: is a reserved keyword' % pn | ||
| 276 | elif pn.startswith('pn-'): | ||
| 277 | return 'Recipe name "%s" is invalid: names starting with "pn-" are reserved' % pn | ||
| 278 | return '' | ||
| 279 | |||
