summaryrefslogtreecommitdiffstats
path: root/meta/lib/oe/recipeutils.py
diff options
context:
space:
mode:
authorPaul Eggleton <paul.eggleton@linux.intel.com>2014-12-19 11:41:50 +0000
committerRichard Purdie <richard.purdie@linuxfoundation.org>2014-12-21 12:37:56 +0000
commitdb73bcc3b8abedb897fa90d8d2e53393a0f6cec7 (patch)
tree51992b2e0e2f4e312677120e2045f004682c2801 /meta/lib/oe/recipeutils.py
parent12bb32e766d79ed9c6e5ac511f45a5fa40712781 (diff)
downloadpoky-db73bcc3b8abedb897fa90d8d2e53393a0f6cec7.tar.gz
lib/oe: add recipeutils module
Add a module to help provide utility functions for dealing with recipes. This would typically be used by external tools. Substantial portions of this module were borrowed from the OE Layer index code; other functions originally contributed by Markus Lehtonen <markus.lehtonen@intel.com>. (From OE-Core rev: fd970c3b42b39601f379300d83354d42d5a536ba) Signed-off-by: Paul Eggleton <paul.eggleton@linux.intel.com> Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
Diffstat (limited to 'meta/lib/oe/recipeutils.py')
-rw-r--r--meta/lib/oe/recipeutils.py279
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
8import sys
9import os
10import os.path
11import tempfile
12import textwrap
13import difflib
14import utils
15import shutil
16import re
17from collections import OrderedDict, defaultdict
18
19
20# Help us to find places to insert values
21recipe_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
23nowrap_vars = ['SUMMARY', 'HOMEPAGE', 'BUGTRACKER']
24list_vars = ['SRC_URI', 'LIC_FILES_CHKSUM']
25meta_vars = ['SUMMARY', 'DESCRIPTION', 'HOMEPAGE', 'BUGTRACKER', 'SECTION']
26
27
28def 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
40def 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
47def 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
54def 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
75def 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
160def 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
200def 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
221def 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
257def 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
269def 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