diff options
Diffstat (limited to 'meta')
-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 | |||